feat(config,handler,server): 支持多静态目录配置,新增路径前缀匹配
- Static 配置从单对象改为数组,支持多个静态目录 - StaticConfig 新增 Path 字段用于路径前缀匹配 - 添加 validateStatics 和 validatePathConflicts 验证函数 - 删除 config.example.yaml 示例文件(配置可通过 --generate 生成) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f2352ab9cc
commit
83e1fe38ba
@ -1,230 +0,0 @@
|
|||||||
# Lolly 配置文件
|
|
||||||
|
|
||||||
# 服务器配置(单服务器模式)
|
|
||||||
server:
|
|
||||||
listen: ":8080" # 监听地址
|
|
||||||
name: "localhost" # 服务器名称(虚拟主机匹配)
|
|
||||||
read_timeout: 30s # 读取超时(0 表示不限制)
|
|
||||||
write_timeout: 30s # 写入超时(0 表示不限制)
|
|
||||||
idle_timeout: 120s # 空闲超时(0 表示不限制)
|
|
||||||
max_conns_per_ip: 1000 # 每 IP 最大连接数(0 表示不限制)
|
|
||||||
max_requests_per_conn: 10000 # 每连接最大请求数(0 表示不限制)
|
|
||||||
|
|
||||||
# 静态文件服务配置
|
|
||||||
static:
|
|
||||||
root: "/var/www/html" # 静态文件根目录
|
|
||||||
index: # 索引文件
|
|
||||||
- "index.html"
|
|
||||||
- "index.htm"
|
|
||||||
|
|
||||||
# 反向代理配置
|
|
||||||
# proxy:
|
|
||||||
# - path: /api # 匹配路径前缀
|
|
||||||
# targets: # 后端目标列表
|
|
||||||
# - url: http://backend1:8080
|
|
||||||
# weight: 3 # 权重(加权轮询时有效)
|
|
||||||
# - url: http://backend2:8080
|
|
||||||
# weight: 1
|
|
||||||
# load_balance: round_robin # 负载均衡算法(有效值: round_robin, weighted_round_robin, least_conn, ip_hash, consistent_hash)
|
|
||||||
# hash_key: ip # 一致性哈希键(仅 load_balance=consistent_hash 时有效,有效值: ip, uri, header:X-Name)
|
|
||||||
# virtual_nodes: 150 # 一致性哈希虚拟节点数(仅 load_balance=consistent_hash 时有效)
|
|
||||||
# health_check: # 健康检查
|
|
||||||
# interval: 10s
|
|
||||||
# path: /health
|
|
||||||
# timeout: 5s
|
|
||||||
# timeout: # 超时配置
|
|
||||||
# connect: 5s # 连接超时
|
|
||||||
# read: 30s # 读取超时
|
|
||||||
# write: 30s # 写入超时
|
|
||||||
# headers: # 头部修改
|
|
||||||
# set_request: {X-Custom: value}
|
|
||||||
# set_response: {X-Server: lolly}
|
|
||||||
# remove: [X-Powered-By]
|
|
||||||
# cache: # 代理缓存
|
|
||||||
# enabled: false
|
|
||||||
# max_age: 60s
|
|
||||||
# cache_lock: true # 防止缓存击穿
|
|
||||||
# stale_while_revalidate: 30s
|
|
||||||
|
|
||||||
# SSL/TLS 配置
|
|
||||||
# ssl:
|
|
||||||
# cert: /path/to/cert.pem # 证书文件
|
|
||||||
# key: /path/to/key.pem # 私钥文件
|
|
||||||
# cert_chain: /path/to/chain.pem # 证书链文件
|
|
||||||
# protocols: # TLS 版本(有效值: TLSv1.2, TLSv1.3)
|
|
||||||
# - "TLSv1.2"
|
|
||||||
# - "TLSv1.3"
|
|
||||||
# ciphers: # 加密套件(仅 TLS 1.2 有效,TLS 1.3 使用内置套件)
|
|
||||||
# - ECDHE-ECDSA-AES256-GCM-SHA384
|
|
||||||
# - ECDHE-RSA-AES256-GCM-SHA384
|
|
||||||
# - ECDHE-ECDSA-CHACHA20-POLY1305
|
|
||||||
# - ECDHE-RSA-CHACHA20-POLY1305
|
|
||||||
# # 拒绝不安全套件:含 RC4、DES、3DES、CBC 的配置将报错
|
|
||||||
# ocsp_stapling: false # OCSP Stapling
|
|
||||||
# hsts: # HTTP Strict Transport Security
|
|
||||||
# max_age: 31536000 # 过期时间(秒)
|
|
||||||
# include_sub_domains: true # 包含子域名
|
|
||||||
# preload: false # 加入 HSTS 预加载列表
|
|
||||||
|
|
||||||
# 安全配置
|
|
||||||
security:
|
|
||||||
# IP 访问控制
|
|
||||||
access:
|
|
||||||
allow: [] # 允许的 IP/CIDR 列表
|
|
||||||
deny: [] # 拒绝的 IP/CIDR 列表
|
|
||||||
default: "allow" # 默认动作(有效值: allow, deny)
|
|
||||||
trusted_proxies: [] # 可信代理 CIDR 列表,用于 X-Forwarded-For 解析
|
|
||||||
|
|
||||||
# 速率限制
|
|
||||||
rate_limit:
|
|
||||||
request_rate: 0 # 每秒请求数(0 表示不限制)
|
|
||||||
burst: 0 # 突发上限
|
|
||||||
conn_limit: 0 # 连接数限制
|
|
||||||
key: "ip" # 限流 key 来源(有效值: ip, header)
|
|
||||||
algorithm: "token_bucket" # 限流算法(有效值: token_bucket, sliding_window)
|
|
||||||
sliding_window_mode: "approximate" # 滑动窗口模式(有效值: approximate, precise,仅 algorithm=sliding_window 时有效)
|
|
||||||
sliding_window: 60 # 滑动窗口大小(秒,仅 algorithm=sliding_window 时有效)
|
|
||||||
|
|
||||||
# 认证配置(type 为空时禁用)
|
|
||||||
auth:
|
|
||||||
type: "" # 认证类型(有效值: basic,空表示禁用)
|
|
||||||
require_tls: true # 启用时强制 HTTPS
|
|
||||||
algorithm: "bcrypt" # 密码哈希算法(有效值: bcrypt, argon2id)
|
|
||||||
users: [] # 用户列表
|
|
||||||
realm: "Restricted Area" # 认证域
|
|
||||||
min_password_length: 8 # 密码最小长度
|
|
||||||
|
|
||||||
# 安全头部
|
|
||||||
headers:
|
|
||||||
x_frame_options: "DENY" # 防止点击劫持(有效值: DENY, SAMEORIGIN, 空表示禁用)
|
|
||||||
x_content_type_options: "nosniff" # 防止 MIME 嗅探(有效值:nosniff,空表示禁用)
|
|
||||||
referrer_policy: "strict-origin-when-cross-origin" # 引用策略(有效值: no-referrer, no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url)
|
|
||||||
# content_security_policy: "default-src 'self'" # 内容安全策略 CSP
|
|
||||||
# permissions_policy: "geolocation=(), microphone=()" # 权限策略
|
|
||||||
|
|
||||||
# URL 重写规则
|
|
||||||
# rewrite:
|
|
||||||
# - pattern: "^/old/(.*)$" # 匹配模式(正则表达式)
|
|
||||||
# replacement: /new/$1 # 替换目标
|
|
||||||
# flag: last # 标志(有效值: last, redirect, permanent, break)
|
|
||||||
|
|
||||||
# 响应压缩配置
|
|
||||||
compression:
|
|
||||||
type: "gzip" # 压缩类型(有效值: gzip, brotli, both,空表示禁用)
|
|
||||||
level: 6 # 压缩级别(范围 0-9,0=不压缩,1=最快,9=最高压缩率)
|
|
||||||
min_size: 1024 # 最小压缩大小(字节,小于此值不压缩)
|
|
||||||
gzip_static: false # 启用预压缩文件支持(自动查找 .gz/.br 文件)
|
|
||||||
gzip_static_extensions: # 预压缩文件扩展名
|
|
||||||
- ".br"
|
|
||||||
- ".gz"
|
|
||||||
types: # 可压缩的 MIME 类型
|
|
||||||
- "text/html"
|
|
||||||
- "text/css"
|
|
||||||
- "text/javascript"
|
|
||||||
- "application/json"
|
|
||||||
- "application/javascript"
|
|
||||||
|
|
||||||
# 多虚拟主机模式(可选,每个虚拟主机支持完整的 server 配置)
|
|
||||||
# servers:
|
|
||||||
# - listen: ":8080" # 监听地址
|
|
||||||
# name: "api.example.com" # 服务器名称(用于虚拟主机匹配)
|
|
||||||
# read_timeout: 30s # 读取超时(0 表示不限制)
|
|
||||||
# write_timeout: 30s # 写入超时(0 表示不限制)
|
|
||||||
# idle_timeout: 120s # 空闲超时(0 表示不限制)
|
|
||||||
# max_conns_per_ip: 1000 # 每 IP 最大连接数(0 表示不限制)
|
|
||||||
# max_requests_per_conn: 10000 # 每连接最大请求数(0 表示不限制)
|
|
||||||
# static: # 静态文件配置
|
|
||||||
# root: /var/www/api
|
|
||||||
# index: [index.html]
|
|
||||||
# proxy: # 反向代理配置
|
|
||||||
# - path: /api
|
|
||||||
# targets:
|
|
||||||
# - url: http://backend:8080
|
|
||||||
# load_balance: round_robin
|
|
||||||
# ssl: # SSL/TLS 配置
|
|
||||||
# cert: /path/to/api.cert.pem
|
|
||||||
# key: /path/to/api.key.pem
|
|
||||||
# protocols: [TLSv1.2, TLSv1.3]
|
|
||||||
# hsts:
|
|
||||||
# max_age: 31536000
|
|
||||||
# include_sub_domains: true
|
|
||||||
# security: # 安全配置
|
|
||||||
# access:
|
|
||||||
# default: allow
|
|
||||||
# rate_limit:
|
|
||||||
# request_rate: 100
|
|
||||||
# headers:
|
|
||||||
# x_frame_options: DENY
|
|
||||||
# compression: # 响应压缩配置
|
|
||||||
# type: gzip
|
|
||||||
# level: 6
|
|
||||||
# - listen: ":8443" # 另一个虚拟主机
|
|
||||||
# name: "static.example.com"
|
|
||||||
# static:
|
|
||||||
# root: /var/www/static
|
|
||||||
# index: [index.html, index.htm]
|
|
||||||
# ssl:
|
|
||||||
# cert: /path/to/static.cert.pem
|
|
||||||
# key: /path/to/static.key.pem
|
|
||||||
# compression:
|
|
||||||
# type: gzip
|
|
||||||
|
|
||||||
# SSL/TLS 默认配置说明(未配置证书时不启用)
|
|
||||||
# 默认 TLS 协议: TLSv1.2, TLSv1.3(不支持 TLSv1.0/1.1)
|
|
||||||
# 默认 HSTS 配置: max_age=31536000(1年), include_sub_domains=true
|
|
||||||
|
|
||||||
# TCP/UDP Stream 代理配置(可选)
|
|
||||||
# stream:
|
|
||||||
# - listen: "3306" # 监听地址
|
|
||||||
# protocol: "tcp" # 协议类型(有效值: tcp, udp)
|
|
||||||
# upstream:
|
|
||||||
# targets: # 上游目标列表
|
|
||||||
# - addr: "mysql1:3306" # 目标地址
|
|
||||||
# weight: 3 # 权重(加权轮询时有效)
|
|
||||||
# - addr: "mysql2:3306"
|
|
||||||
# weight: 1
|
|
||||||
# load_balance: "round_robin" # 负载均衡算法(有效值: round_robin, weighted_round_robin, least_conn, ip_hash)
|
|
||||||
|
|
||||||
# 日志配置
|
|
||||||
logging:
|
|
||||||
format: "text" # 全局日志格式(有效值: text, json),控制启动/停止日志格式
|
|
||||||
access:
|
|
||||||
path: "" # 日志文件路径(空表示输出到 stdout)
|
|
||||||
format: '$remote_addr - $remote_user [$time] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"' # 访问日志格式,近似 nginx combined
|
|
||||||
# 支持变量: $remote_addr, $remote_user, $request, $status, $body_bytes_sent, $request_time, $http_referer, $http_user_agent, $time
|
|
||||||
# 特殊值 "json" 输出结构化 JSON
|
|
||||||
error:
|
|
||||||
path: "" # 日志文件路径(空表示输出到 stderr)
|
|
||||||
level: "info" # 日志级别(有效值: debug, info, warn, error,级别越高日志越少)
|
|
||||||
|
|
||||||
# 性能配置
|
|
||||||
performance:
|
|
||||||
goroutine_pool: # Goroutine 池(处理并发请求)
|
|
||||||
enabled: false # 是否启用
|
|
||||||
max_workers: 1000 # 最大 worker 数
|
|
||||||
min_workers: 10 # 最小 worker 数(预热)
|
|
||||||
idle_timeout: 60s # 空闲超时
|
|
||||||
file_cache: # 静态文件缓存
|
|
||||||
max_entries: 10000 # 最大缓存条目
|
|
||||||
max_size: 268435456 # 内存上限(字节,256MB)
|
|
||||||
inactive: 20s # 未访问淘汰时间
|
|
||||||
transport: # HTTP Transport 连接池
|
|
||||||
max_idle_conns: 100 # 最大空闲连接
|
|
||||||
max_idle_conns_per_host: 32 # 每主机空闲连接
|
|
||||||
idle_conn_timeout: 90s # 空闲超时
|
|
||||||
max_conns_per_host: 0 # 每主机最大连接(0 表示不限制)
|
|
||||||
|
|
||||||
# HTTP/3 (QUIC) 配置(需要 SSL 证书)
|
|
||||||
http3:
|
|
||||||
enabled: false # 是否启用 HTTP/3
|
|
||||||
listen: ":443" # UDP 监听地址
|
|
||||||
max_streams: 100 # 最大并发流
|
|
||||||
idle_timeout: 60s # 空闲超时
|
|
||||||
enable_0rtt: false # 启用 0-RTT(早期数据,可能存在安全风险)
|
|
||||||
|
|
||||||
# 监控配置
|
|
||||||
monitoring:
|
|
||||||
status:
|
|
||||||
path: "/_status" # 状态端点路径
|
|
||||||
allow: # 允许访问的 IP
|
|
||||||
- "127.0.0.1"
|
|
||||||
@ -145,9 +145,9 @@ type ServerConfig struct {
|
|||||||
// 多个服务器可通过 Name 区分不同域名或服务
|
// 多个服务器可通过 Name 区分不同域名或服务
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
|
|
||||||
// Static 静态文件服务配置
|
// Static 静态文件服务配置列表
|
||||||
// 用于提供静态资源,如 HTML、CSS、JS、图片等
|
// 支持多个静态目录,按 path 前缀匹配
|
||||||
Static StaticConfig `yaml:"static"`
|
Static []StaticConfig `yaml:"static"`
|
||||||
|
|
||||||
// Proxy 反向代理规则列表
|
// Proxy 反向代理规则列表
|
||||||
// 按顺序匹配,首个匹配的规则生效
|
// 按顺序匹配,首个匹配的规则生效
|
||||||
@ -192,9 +192,10 @@ type ServerConfig struct {
|
|||||||
|
|
||||||
// StaticConfig 静态文件服务配置。
|
// StaticConfig 静态文件服务配置。
|
||||||
//
|
//
|
||||||
// 用于配置静态文件服务器的行为,包括根目录和索引文件。
|
// 用于配置静态文件服务器的行为,包括路径匹配、根目录和索引文件。
|
||||||
//
|
//
|
||||||
// 注意事项:
|
// 注意事项:
|
||||||
|
// - Path 为路径前缀,匹配的请求将被该静态处理器处理
|
||||||
// - Root 路径可以是相对路径或绝对路径
|
// - Root 路径可以是相对路径或绝对路径
|
||||||
// - 索引文件按顺序查找,第一个存在的文件将被使用
|
// - 索引文件按顺序查找,第一个存在的文件将被使用
|
||||||
// - 目录路径需要确保有读取权限
|
// - 目录路径需要确保有读取权限
|
||||||
@ -202,9 +203,17 @@ type ServerConfig struct {
|
|||||||
// 使用示例:
|
// 使用示例:
|
||||||
//
|
//
|
||||||
// static:
|
// static:
|
||||||
// root: "/var/www/html"
|
// - path: "/"
|
||||||
// index: ["index.html", "index.htm", "default.html"]
|
// root: "/var/www/html"
|
||||||
|
// index: ["index.html", "index.htm"]
|
||||||
|
// - path: "/assets/"
|
||||||
|
// root: "/var/www/assets"
|
||||||
type StaticConfig struct {
|
type StaticConfig struct {
|
||||||
|
// Path 匹配路径前缀
|
||||||
|
// 以此前缀开头的请求将被该静态处理器处理
|
||||||
|
// 默认为 "/",匹配所有路径
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
|
||||||
// Root 静态文件根目录
|
// Root 静态文件根目录
|
||||||
// 所有静态文件请求都将以此目录为基础解析
|
// 所有静态文件请求都将以此目录为基础解析
|
||||||
Root string `yaml:"root"`
|
Root string `yaml:"root"`
|
||||||
|
|||||||
@ -15,9 +15,10 @@ func TestLoad(t *testing.T) {
|
|||||||
server:
|
server:
|
||||||
listen: ":8080"
|
listen: ":8080"
|
||||||
static:
|
static:
|
||||||
root: "/var/www"
|
- path: "/"
|
||||||
index:
|
root: "/var/www"
|
||||||
- "index.html"
|
index:
|
||||||
|
- "index.html"
|
||||||
logging:
|
logging:
|
||||||
access:
|
access:
|
||||||
path: "/var/log/access.log"
|
path: "/var/log/access.log"
|
||||||
@ -49,11 +50,11 @@ monitoring:
|
|||||||
if cfg.Server.Listen != ":8080" {
|
if cfg.Server.Listen != ":8080" {
|
||||||
t.Errorf("Server.Listen = %q, want %q", cfg.Server.Listen, ":8080")
|
t.Errorf("Server.Listen = %q, want %q", cfg.Server.Listen, ":8080")
|
||||||
}
|
}
|
||||||
if cfg.Server.Static.Root != "/var/www" {
|
if cfg.Server.Static[0].Root != "/var/www" {
|
||||||
t.Errorf("Server.Static.Root = %q, want %q", cfg.Server.Static.Root, "/var/www")
|
t.Errorf("Server.Static.Root = %q, want %q", cfg.Server.Static[0].Root, "/var/www")
|
||||||
}
|
}
|
||||||
if len(cfg.Server.Static.Index) != 1 || cfg.Server.Static.Index[0] != "index.html" {
|
if len(cfg.Server.Static[0].Index) != 1 || cfg.Server.Static[0].Index[0] != "index.html" {
|
||||||
t.Errorf("Server.Static.Index = %v, want [index.html]", cfg.Server.Static.Index)
|
t.Errorf("Server.Static.Index = %v, want [index.html]", cfg.Server.Static[0].Index)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -139,7 +140,8 @@ func TestLoadFromString(t *testing.T) {
|
|||||||
server:
|
server:
|
||||||
listen: ":9090"
|
listen: ":9090"
|
||||||
static:
|
static:
|
||||||
root: "/app/public"
|
- path: "/"
|
||||||
|
root: "/app/public"
|
||||||
`
|
`
|
||||||
cfg, err := LoadFromString(yamlStr)
|
cfg, err := LoadFromString(yamlStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -149,8 +151,8 @@ server:
|
|||||||
if cfg.Server.Listen != ":9090" {
|
if cfg.Server.Listen != ":9090" {
|
||||||
t.Errorf("Server.Listen = %q, want %q", cfg.Server.Listen, ":9090")
|
t.Errorf("Server.Listen = %q, want %q", cfg.Server.Listen, ":9090")
|
||||||
}
|
}
|
||||||
if cfg.Server.Static.Root != "/app/public" {
|
if cfg.Server.Static[0].Root != "/app/public" {
|
||||||
t.Errorf("Server.Static.Root = %q, want %q", cfg.Server.Static.Root, "/app/public")
|
t.Errorf("Server.Static.Root = %q, want %q", cfg.Server.Static[0].Root, "/app/public")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -192,10 +194,11 @@ func TestSave(t *testing.T) {
|
|||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
Server: ServerConfig{
|
Server: ServerConfig{
|
||||||
Listen: ":8080",
|
Listen: ":8080",
|
||||||
Static: StaticConfig{
|
Static: []StaticConfig{{
|
||||||
|
Path: "/",
|
||||||
Root: "/var/www",
|
Root: "/var/www",
|
||||||
Index: []string{"index.html"},
|
Index: []string{"index.html"},
|
||||||
},
|
}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,8 +218,8 @@ func TestSave(t *testing.T) {
|
|||||||
if loaded.Server.Listen != cfg.Server.Listen {
|
if loaded.Server.Listen != cfg.Server.Listen {
|
||||||
t.Errorf("loaded.Server.Listen = %q, want %q", loaded.Server.Listen, cfg.Server.Listen)
|
t.Errorf("loaded.Server.Listen = %q, want %q", loaded.Server.Listen, cfg.Server.Listen)
|
||||||
}
|
}
|
||||||
if loaded.Server.Static.Root != cfg.Server.Static.Root {
|
if loaded.Server.Static[0].Root != cfg.Server.Static[0].Root {
|
||||||
t.Errorf("loaded.Server.Static.Root = %q, want %q", loaded.Server.Static.Root, cfg.Server.Static.Root)
|
t.Errorf("loaded.Server.Static[0].Root = %q, want %q", loaded.Server.Static[0].Root, cfg.Server.Static[0].Root)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -263,10 +266,11 @@ func TestSave(t *testing.T) {
|
|||||||
Server: ServerConfig{
|
Server: ServerConfig{
|
||||||
Listen: ":8443",
|
Listen: ":8443",
|
||||||
Name: "default",
|
Name: "default",
|
||||||
Static: StaticConfig{
|
Static: []StaticConfig{{
|
||||||
|
Path: "/",
|
||||||
Root: "/var/www/html",
|
Root: "/var/www/html",
|
||||||
Index: []string{"index.html", "index.htm"},
|
Index: []string{"index.html", "index.htm"},
|
||||||
},
|
}},
|
||||||
Proxy: []ProxyConfig{
|
Proxy: []ProxyConfig{
|
||||||
{
|
{
|
||||||
Path: "/api",
|
Path: "/api",
|
||||||
|
|||||||
@ -45,10 +45,11 @@ func DefaultConfig() *Config {
|
|||||||
IdleTimeout: 120 * time.Second,
|
IdleTimeout: 120 * time.Second,
|
||||||
MaxConnsPerIP: 1000,
|
MaxConnsPerIP: 1000,
|
||||||
MaxRequestsPerConn: 10000,
|
MaxRequestsPerConn: 10000,
|
||||||
Static: StaticConfig{
|
Static: []StaticConfig{{
|
||||||
|
Path: "/",
|
||||||
Root: "/var/www/html",
|
Root: "/var/www/html",
|
||||||
Index: []string{"index.html", "index.htm"},
|
Index: []string{"index.html", "index.htm"},
|
||||||
},
|
}},
|
||||||
SSL: SSLConfig{
|
SSL: SSLConfig{
|
||||||
Protocols: []string{"TLSv1.2", "TLSv1.3"},
|
Protocols: []string{"TLSv1.2", "TLSv1.3"},
|
||||||
OCSPStapling: false,
|
OCSPStapling: false,
|
||||||
@ -183,13 +184,20 @@ func GenerateConfigYAML(cfg *Config) ([]byte, error) {
|
|||||||
buf.WriteString("\n")
|
buf.WriteString("\n")
|
||||||
|
|
||||||
// static 配置
|
// static 配置
|
||||||
buf.WriteString(" # 静态文件服务配置\n")
|
buf.WriteString(" # 静态文件服务配置(支持多个目录)\n")
|
||||||
buf.WriteString(" static:\n")
|
buf.WriteString(" static:\n")
|
||||||
fmt.Fprintf(&buf, " root: \"%s\" # 静态文件根目录\n", cfg.Server.Static.Root)
|
for _, st := range cfg.Server.Static {
|
||||||
buf.WriteString(" index: # 索引文件\n")
|
buf.WriteString(" - path: \"/\" # 匹配路径前缀\n")
|
||||||
for _, idx := range cfg.Server.Static.Index {
|
fmt.Fprintf(&buf, " root: \"%s\" # 静态文件根目录\n", st.Root)
|
||||||
fmt.Fprintf(&buf, " - \"%s\"\n", idx)
|
buf.WriteString(" index: # 索引文件\n")
|
||||||
|
for _, idx := range st.Index {
|
||||||
|
fmt.Fprintf(&buf, " - \"%s\"\n", idx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
buf.WriteString(" # 示例:额外的静态目录\n")
|
||||||
|
buf.WriteString(" # - path: \"/assets/\"\n")
|
||||||
|
buf.WriteString(" # root: \"/var/www/assets\"\n")
|
||||||
|
buf.WriteString(" # index: [\"index.html\"]\n")
|
||||||
buf.WriteString("\n")
|
buf.WriteString("\n")
|
||||||
|
|
||||||
// proxy 配置示例
|
// proxy 配置示例
|
||||||
@ -319,9 +327,10 @@ func GenerateConfigYAML(cfg *Config) ([]byte, error) {
|
|||||||
buf.WriteString("# idle_timeout: 120s # 空闲超时(0 表示不限制)\n")
|
buf.WriteString("# idle_timeout: 120s # 空闲超时(0 表示不限制)\n")
|
||||||
buf.WriteString("# max_conns_per_ip: 1000 # 每 IP 最大连接数(0 表示不限制)\n")
|
buf.WriteString("# max_conns_per_ip: 1000 # 每 IP 最大连接数(0 表示不限制)\n")
|
||||||
buf.WriteString("# max_requests_per_conn: 10000 # 每连接最大请求数(0 表示不限制)\n")
|
buf.WriteString("# max_requests_per_conn: 10000 # 每连接最大请求数(0 表示不限制)\n")
|
||||||
buf.WriteString("# static: # 静态文件配置\n")
|
buf.WriteString("# static: # 静态文件配置(支持多个目录)\n")
|
||||||
buf.WriteString("# root: /var/www/api\n")
|
buf.WriteString("# - path: /\n")
|
||||||
buf.WriteString("# index: [index.html]\n")
|
buf.WriteString("# root: /var/www/api\n")
|
||||||
|
buf.WriteString("# index: [index.html]\n")
|
||||||
buf.WriteString("# proxy: # 反向代理配置\n")
|
buf.WriteString("# proxy: # 反向代理配置\n")
|
||||||
buf.WriteString("# - path: /api\n")
|
buf.WriteString("# - path: /api\n")
|
||||||
buf.WriteString("# targets:\n")
|
buf.WriteString("# targets:\n")
|
||||||
@ -347,8 +356,9 @@ func GenerateConfigYAML(cfg *Config) ([]byte, error) {
|
|||||||
buf.WriteString("# - listen: \":8443\" # 另一个虚拟主机\n")
|
buf.WriteString("# - listen: \":8443\" # 另一个虚拟主机\n")
|
||||||
buf.WriteString("# name: \"static.example.com\"\n")
|
buf.WriteString("# name: \"static.example.com\"\n")
|
||||||
buf.WriteString("# static:\n")
|
buf.WriteString("# static:\n")
|
||||||
buf.WriteString("# root: /var/www/static\n")
|
buf.WriteString("# - path: /\n")
|
||||||
buf.WriteString("# index: [index.html, index.htm]\n")
|
buf.WriteString("# root: /var/www/static\n")
|
||||||
|
buf.WriteString("# index: [index.html, index.htm]\n")
|
||||||
buf.WriteString("# ssl:\n")
|
buf.WriteString("# ssl:\n")
|
||||||
buf.WriteString("# cert: /path/to/static.cert.pem\n")
|
buf.WriteString("# cert: /path/to/static.cert.pem\n")
|
||||||
buf.WriteString("# key: /path/to/static.key.pem\n")
|
buf.WriteString("# key: /path/to/static.key.pem\n")
|
||||||
|
|||||||
@ -56,7 +56,7 @@ func validateServer(s *ServerConfig, isDefault bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证静态文件配置
|
// 验证静态文件配置
|
||||||
if err := validateStatic(&s.Static); err != nil {
|
if err := validateStatics(s.Static); err != nil {
|
||||||
return fmt.Errorf("static: %w", err)
|
return fmt.Errorf("static: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +67,11 @@ func validateServer(s *ServerConfig, isDefault bool) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查 static 和 proxy 路径冲突
|
||||||
|
if err := validatePathConflicts(s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// 验证重写规则
|
// 验证重写规则
|
||||||
for i := range s.Rewrite {
|
for i := range s.Rewrite {
|
||||||
if err := validateRewrite(&s.Rewrite[i]); err != nil {
|
if err := validateRewrite(&s.Rewrite[i]); err != nil {
|
||||||
@ -92,6 +97,69 @@ func validateServer(s *ServerConfig, isDefault bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateStatics 验证静态文件配置数组。
|
||||||
|
//
|
||||||
|
// 检查静态文件配置的路径重复和根目录路径安全性。
|
||||||
|
//
|
||||||
|
// 参数:
|
||||||
|
// - statics: 静态文件配置数组
|
||||||
|
//
|
||||||
|
// 返回值:
|
||||||
|
// - error: 验证失败时返回错误信息,成功返回 nil
|
||||||
|
func validateStatics(statics []StaticConfig) error {
|
||||||
|
if len(statics) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
paths := make(map[string]int)
|
||||||
|
for i, s := range statics {
|
||||||
|
// Path 默认为 "/"
|
||||||
|
path := s.Path
|
||||||
|
if path == "" {
|
||||||
|
path = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查路径重复
|
||||||
|
if idx, exists := paths[path]; exists {
|
||||||
|
return fmt.Errorf("路径 %s 重复定义 (static[%d] 和 static[%d])", path, idx, i)
|
||||||
|
}
|
||||||
|
paths[path] = i
|
||||||
|
|
||||||
|
// 验证根目录路径安全
|
||||||
|
if s.Root != "" && strings.Contains(s.Root, "..") {
|
||||||
|
return fmt.Errorf("static[%d]: 根目录路径不能包含 '..'", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validatePathConflicts 检查 static 和 proxy 路径冲突。
|
||||||
|
//
|
||||||
|
// 确保 static 和 proxy 没有相同的 path 前缀。
|
||||||
|
//
|
||||||
|
// 参数:
|
||||||
|
// - s: 服务器配置对象
|
||||||
|
//
|
||||||
|
// 返回值:
|
||||||
|
// - error: 发现冲突时返回错误信息,成功返回 nil
|
||||||
|
func validatePathConflicts(s *ServerConfig) error {
|
||||||
|
staticPaths := make(map[string]int)
|
||||||
|
for i, st := range s.Static {
|
||||||
|
path := st.Path
|
||||||
|
if path == "" {
|
||||||
|
path = "/"
|
||||||
|
}
|
||||||
|
staticPaths[path] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, p := range s.Proxy {
|
||||||
|
if idx, exists := staticPaths[p.Path]; exists {
|
||||||
|
return fmt.Errorf("路径 %s 同时定义在 static[%d] 和 proxy[%d]", p.Path, idx, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// validateStatic 验证静态文件配置。
|
// validateStatic 验证静态文件配置。
|
||||||
//
|
//
|
||||||
// 检查静态文件根目录路径的安全性,防止路径遍历攻击。
|
// 检查静态文件根目录路径的安全性,防止路径遍历攻击。
|
||||||
|
|||||||
@ -32,7 +32,7 @@ func TestValidateServer(t *testing.T) {
|
|||||||
name: "有效配置",
|
name: "有效配置",
|
||||||
config: ServerConfig{
|
config: ServerConfig{
|
||||||
Listen: ":8080",
|
Listen: ":8080",
|
||||||
Static: StaticConfig{Root: "/var/www"},
|
Static: []StaticConfig{{Path: "/", Root: "/var/www"}},
|
||||||
Proxy: []ProxyConfig{
|
Proxy: []ProxyConfig{
|
||||||
{Path: "/api", Targets: []ProxyTarget{{URL: "http://backend:8080"}}},
|
{Path: "/api", Targets: []ProxyTarget{{URL: "http://backend:8080"}}},
|
||||||
},
|
},
|
||||||
@ -43,7 +43,7 @@ func TestValidateServer(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "默认服务器可省略Listen",
|
name: "默认服务器可省略Listen",
|
||||||
config: ServerConfig{
|
config: ServerConfig{
|
||||||
Static: StaticConfig{Root: "/var/www"},
|
Static: []StaticConfig{{Path: "/", Root: "/var/www"}},
|
||||||
},
|
},
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
@ -51,7 +51,7 @@ func TestValidateServer(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "非默认服务器Listen缺失",
|
name: "非默认服务器Listen缺失",
|
||||||
config: ServerConfig{
|
config: ServerConfig{
|
||||||
Static: StaticConfig{Root: "/var/www"},
|
Static: []StaticConfig{{Path: "/", Root: "/var/www"}},
|
||||||
},
|
},
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
@ -70,7 +70,7 @@ func TestValidateServer(t *testing.T) {
|
|||||||
name: "静态根目录含..",
|
name: "静态根目录含..",
|
||||||
config: ServerConfig{
|
config: ServerConfig{
|
||||||
Listen: ":8080",
|
Listen: ":8080",
|
||||||
Static: StaticConfig{Root: "/var/../www"},
|
Static: []StaticConfig{{Path: "/", Root: "/var/../www"}},
|
||||||
},
|
},
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
|
|||||||
@ -41,6 +41,9 @@ type StaticHandler struct {
|
|||||||
// root 静态文件根目录
|
// root 静态文件根目录
|
||||||
root string
|
root string
|
||||||
|
|
||||||
|
// pathPrefix 路径前缀,会被剥离后拼接 root
|
||||||
|
pathPrefix string
|
||||||
|
|
||||||
// index 索引文件列表,当请求目录时依次查找
|
// index 索引文件列表,当请求目录时依次查找
|
||||||
index []string
|
index []string
|
||||||
|
|
||||||
@ -60,6 +63,7 @@ type StaticHandler struct {
|
|||||||
//
|
//
|
||||||
// 参数:
|
// 参数:
|
||||||
// - root: 静态文件根目录路径
|
// - root: 静态文件根目录路径
|
||||||
|
// - pathPrefix: 路径前缀,会被剥离后拼接 root
|
||||||
// - index: 索引文件列表,当请求目录时依次查找(如 ["index.html", "index.htm"])
|
// - index: 索引文件列表,当请求目录时依次查找(如 ["index.html", "index.htm"])
|
||||||
// - useSendfile: 是否启用零拷贝传输(大文件优化)
|
// - useSendfile: 是否启用零拷贝传输(大文件优化)
|
||||||
//
|
//
|
||||||
@ -68,10 +72,11 @@ type StaticHandler struct {
|
|||||||
//
|
//
|
||||||
// 使用示例:
|
// 使用示例:
|
||||||
//
|
//
|
||||||
// handler := handler.NewStaticHandler("/var/www", []string{"index.html"}, true)
|
// handler := handler.NewStaticHandler("/var/www", "/", []string{"index.html"}, true)
|
||||||
func NewStaticHandler(root string, index []string, useSendfile bool) *StaticHandler {
|
func NewStaticHandler(root, pathPrefix string, index []string, useSendfile bool) *StaticHandler {
|
||||||
return &StaticHandler{
|
return &StaticHandler{
|
||||||
root: root,
|
root: root,
|
||||||
|
pathPrefix: pathPrefix,
|
||||||
index: index,
|
index: index,
|
||||||
useSendfile: useSendfile,
|
useSendfile: useSendfile,
|
||||||
}
|
}
|
||||||
@ -126,16 +131,24 @@ func (h *StaticHandler) SetGzipStatic(enabled bool, extensions []string) {
|
|||||||
// 6. 大文件使用零拷贝传输
|
// 6. 大文件使用零拷贝传输
|
||||||
// 7. 读取文件并存入缓存
|
// 7. 读取文件并存入缓存
|
||||||
func (h *StaticHandler) Handle(ctx *fasthttp.RequestCtx) {
|
func (h *StaticHandler) Handle(ctx *fasthttp.RequestCtx) {
|
||||||
path := string(ctx.Path())
|
reqPath := string(ctx.Path())
|
||||||
|
|
||||||
// 安全检查:防止目录遍历
|
// 安全检查:防止目录遍历
|
||||||
if strings.Contains(path, "..") {
|
if strings.Contains(reqPath, "..") {
|
||||||
ctx.Error("Forbidden", fasthttp.StatusForbidden)
|
ctx.Error("Forbidden", fasthttp.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 剥离路径前缀
|
||||||
|
if h.pathPrefix != "" && h.pathPrefix != "/" {
|
||||||
|
reqPath = strings.TrimPrefix(reqPath, h.pathPrefix)
|
||||||
|
if !strings.HasPrefix(reqPath, "/") {
|
||||||
|
reqPath = "/" + reqPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 拼接文件路径
|
// 拼接文件路径
|
||||||
filePath := filepath.Join(h.root, path)
|
filePath := filepath.Join(h.root, reqPath)
|
||||||
|
|
||||||
// 检查文件/目录是否存在
|
// 检查文件/目录是否存在
|
||||||
info, err := os.Stat(filePath)
|
info, err := os.Stat(filePath)
|
||||||
|
|||||||
@ -28,7 +28,7 @@ import (
|
|||||||
// newTestHandler 创建测试用的静态文件处理器
|
// newTestHandler 创建测试用的静态文件处理器
|
||||||
func newTestHandler(t *testing.T, root string) *StaticHandler {
|
func newTestHandler(t *testing.T, root string) *StaticHandler {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
return NewStaticHandler(root, []string{"index.html", "index.htm"}, false) // 测试时禁用 sendfile
|
return NewStaticHandler(root, "/", []string{"index.html", "index.htm"}, false) // 测试时禁用 sendfile
|
||||||
}
|
}
|
||||||
|
|
||||||
// newTestContext 创建测试用的 fasthttp 请求上下文
|
// newTestContext 创建测试用的 fasthttp 请求上下文
|
||||||
@ -391,7 +391,7 @@ func TestNewStaticHandler(t *testing.T) {
|
|||||||
t.Run("正常创建", func(t *testing.T) {
|
t.Run("正常创建", func(t *testing.T) {
|
||||||
root := "/var/www"
|
root := "/var/www"
|
||||||
index := []string{"index.html", "index.htm"}
|
index := []string{"index.html", "index.htm"}
|
||||||
handler := NewStaticHandler(root, index, true)
|
handler := NewStaticHandler(root, "/", index, true)
|
||||||
|
|
||||||
if handler == nil {
|
if handler == nil {
|
||||||
t.Fatal("NewStaticHandler() 返回 nil")
|
t.Fatal("NewStaticHandler() 返回 nil")
|
||||||
@ -405,7 +405,7 @@ func TestNewStaticHandler(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("空索引列表", func(t *testing.T) {
|
t.Run("空索引列表", func(t *testing.T) {
|
||||||
handler := NewStaticHandler("/var/www", nil, false)
|
handler := NewStaticHandler("/var/www", "/", nil, false)
|
||||||
if handler == nil {
|
if handler == nil {
|
||||||
t.Fatal("NewStaticHandler() 返回 nil")
|
t.Fatal("NewStaticHandler() 返回 nil")
|
||||||
}
|
}
|
||||||
@ -417,7 +417,7 @@ func TestNewStaticHandler(t *testing.T) {
|
|||||||
|
|
||||||
// TestStaticHandler_SetFileCache 测试设置文件缓存
|
// TestStaticHandler_SetFileCache 测试设置文件缓存
|
||||||
func TestStaticHandler_SetFileCache(t *testing.T) {
|
func TestStaticHandler_SetFileCache(t *testing.T) {
|
||||||
handler := NewStaticHandler("/var/www", nil, false)
|
handler := NewStaticHandler("/var/www", "/", nil, false)
|
||||||
|
|
||||||
// 设置 nil 缓存
|
// 设置 nil 缓存
|
||||||
handler.SetFileCache(nil)
|
handler.SetFileCache(nil)
|
||||||
@ -431,7 +431,7 @@ func TestStaticHandler_SetFileCache(t *testing.T) {
|
|||||||
|
|
||||||
// TestStaticHandler_SetGzipStatic 测试设置 Gzip 静态文件
|
// TestStaticHandler_SetGzipStatic 测试设置 Gzip 静态文件
|
||||||
func TestStaticHandler_SetGzipStatic(t *testing.T) {
|
func TestStaticHandler_SetGzipStatic(t *testing.T) {
|
||||||
handler := NewStaticHandler("/var/www", nil, false)
|
handler := NewStaticHandler("/var/www", "/", nil, false)
|
||||||
|
|
||||||
// 启用 gzip
|
// 启用 gzip
|
||||||
handler.SetGzipStatic(true, []string{".gz", ".gzip"})
|
handler.SetGzipStatic(true, []string{".gz", ".gzip"})
|
||||||
@ -509,7 +509,7 @@ func TestStaticHandler_Handle_Precompressed(t *testing.T) {
|
|||||||
t.Fatalf("创建 gzip 文件失败: %v", err)
|
t.Fatalf("创建 gzip 文件失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := NewStaticHandler(tmpDir, nil, false)
|
handler := NewStaticHandler(tmpDir, "/", nil, false)
|
||||||
handler.SetGzipStatic(true, []string{".gz"})
|
handler.SetGzipStatic(true, []string{".gz"})
|
||||||
|
|
||||||
ctx := &fasthttp.RequestCtx{}
|
ctx := &fasthttp.RequestCtx{}
|
||||||
@ -540,7 +540,7 @@ func TestStaticHandler_Handle_LargeFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用 sendfile 启用的处理器
|
// 使用 sendfile 启用的处理器
|
||||||
handler := NewStaticHandler(tmpDir, []string{"index.html"}, true)
|
handler := NewStaticHandler(tmpDir, "/", []string{"index.html"}, true)
|
||||||
|
|
||||||
ctx := &fasthttp.RequestCtx{}
|
ctx := &fasthttp.RequestCtx{}
|
||||||
ctx.Request.SetRequestURI("/large.bin")
|
ctx.Request.SetRequestURI("/large.bin")
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -341,7 +342,7 @@ func (s *Server) startSingleMode() error {
|
|||||||
s.registerProxyRoutes(router, &s.config.Server)
|
s.registerProxyRoutes(router, &s.config.Server)
|
||||||
|
|
||||||
// 静态文件服务
|
// 静态文件服务
|
||||||
s.registerStaticHandler(router, &s.config.Server)
|
s.registerStaticHandlers(router, &s.config.Server)
|
||||||
|
|
||||||
// 构建中间件链
|
// 构建中间件链
|
||||||
chain, err := s.buildMiddlewareChain(&s.config.Server)
|
chain, err := s.buildMiddlewareChain(&s.config.Server)
|
||||||
@ -410,20 +411,7 @@ func (s *Server) startVHostMode() error {
|
|||||||
s.registerProxyRoutes(router, &s.config.Servers[i])
|
s.registerProxyRoutes(router, &s.config.Servers[i])
|
||||||
|
|
||||||
// 静态文件
|
// 静态文件
|
||||||
staticHandler := handler.NewStaticHandler(
|
s.registerStaticHandlers(router, &s.config.Servers[i])
|
||||||
s.config.Servers[i].Static.Root,
|
|
||||||
s.config.Servers[i].Static.Index,
|
|
||||||
true, // useSendfile
|
|
||||||
)
|
|
||||||
if s.fileCache != nil {
|
|
||||||
staticHandler.SetFileCache(s.fileCache)
|
|
||||||
}
|
|
||||||
// 设置预压缩文件支持
|
|
||||||
if s.config.Servers[i].Compression.GzipStatic {
|
|
||||||
staticHandler.SetGzipStatic(true, s.config.Servers[i].Compression.GzipStaticExtensions)
|
|
||||||
}
|
|
||||||
router.GET("/{filepath:*}", staticHandler.Handle)
|
|
||||||
router.HEAD("/{filepath:*}", staticHandler.Handle)
|
|
||||||
|
|
||||||
// 为每个虚拟主机构建独立的中间件链
|
// 为每个虚拟主机构建独立的中间件链
|
||||||
chain, err := s.buildMiddlewareChain(&s.config.Servers[i])
|
chain, err := s.buildMiddlewareChain(&s.config.Servers[i])
|
||||||
@ -454,19 +442,9 @@ func (s *Server) startVHostMode() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.registerProxyRoutes(router, &s.config.Server)
|
s.registerProxyRoutes(router, &s.config.Server)
|
||||||
staticHandler := handler.NewStaticHandler(
|
|
||||||
s.config.Server.Static.Root,
|
// 静态文件
|
||||||
s.config.Server.Static.Index,
|
s.registerStaticHandlers(router, &s.config.Server)
|
||||||
true, // useSendfile
|
|
||||||
)
|
|
||||||
if s.fileCache != nil {
|
|
||||||
staticHandler.SetFileCache(s.fileCache)
|
|
||||||
}
|
|
||||||
// 设置预压缩文件支持
|
|
||||||
if s.config.Server.Compression.GzipStatic {
|
|
||||||
staticHandler.SetGzipStatic(true, s.config.Server.Compression.GzipStaticExtensions)
|
|
||||||
}
|
|
||||||
router.GET("/{filepath:*}", staticHandler.Handle)
|
|
||||||
|
|
||||||
chain, err := s.buildMiddlewareChain(&s.config.Server)
|
chain, err := s.buildMiddlewareChain(&s.config.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -678,25 +656,39 @@ func (s *Server) getProxyCacheStats() ProxyCacheStats {
|
|||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerStaticHandler 注册静态文件处理器。
|
// registerStaticHandlers 注册静态文件处理器。
|
||||||
//
|
//
|
||||||
// 为路由器注册静态文件服务,支持文件缓存和预压缩文件。
|
// 为路由器注册静态文件服务,支持多个静态目录、文件缓存和预压缩文件。
|
||||||
//
|
//
|
||||||
// 参数:
|
// 参数:
|
||||||
// - router: 路由器实例,用于注册路由规则
|
// - router: 路由器实例,用于注册路由规则
|
||||||
// - cfg: 服务器配置,包含静态文件和压缩设置
|
// - cfg: 服务器配置,包含静态文件和压缩设置
|
||||||
func (s *Server) registerStaticHandler(router *handler.Router, cfg *config.ServerConfig) {
|
func (s *Server) registerStaticHandlers(router *handler.Router, cfg *config.ServerConfig) {
|
||||||
staticHandler := handler.NewStaticHandler(
|
for _, static := range cfg.Static {
|
||||||
cfg.Static.Root,
|
path := static.Path
|
||||||
cfg.Static.Index,
|
if path == "" {
|
||||||
true, // useSendfile
|
path = "/"
|
||||||
)
|
}
|
||||||
if s.fileCache != nil {
|
|
||||||
staticHandler.SetFileCache(s.fileCache)
|
staticHandler := handler.NewStaticHandler(
|
||||||
|
static.Root,
|
||||||
|
path,
|
||||||
|
static.Index,
|
||||||
|
true, // useSendfile
|
||||||
|
)
|
||||||
|
if s.fileCache != nil {
|
||||||
|
staticHandler.SetFileCache(s.fileCache)
|
||||||
|
}
|
||||||
|
if cfg.Compression.GzipStatic {
|
||||||
|
staticHandler.SetGzipStatic(true, cfg.Compression.GzipStaticExtensions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册路由:确保路径以 / 结尾
|
||||||
|
routePath := path
|
||||||
|
if !strings.HasSuffix(routePath, "/") {
|
||||||
|
routePath += "/"
|
||||||
|
}
|
||||||
|
router.GET(routePath+"{filepath:*}", staticHandler.Handle)
|
||||||
|
router.HEAD(routePath+"{filepath:*}", staticHandler.Handle)
|
||||||
}
|
}
|
||||||
if cfg.Compression.GzipStatic {
|
|
||||||
staticHandler.SetGzipStatic(true, cfg.Compression.GzipStaticExtensions)
|
|
||||||
}
|
|
||||||
router.GET("/{filepath:*}", staticHandler.Handle)
|
|
||||||
router.HEAD("/{filepath:*}", staticHandler.Handle)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,10 +27,11 @@ func TestNew(t *testing.T) {
|
|||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Server: config.ServerConfig{
|
Server: config.ServerConfig{
|
||||||
Listen: ":8080",
|
Listen: ":8080",
|
||||||
Static: config.StaticConfig{
|
Static: []config.StaticConfig{{
|
||||||
|
Path: "/",
|
||||||
Root: "./static",
|
Root: "./static",
|
||||||
Index: []string{"index.html"},
|
Index: []string{"index.html"},
|
||||||
},
|
}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user