fix(converter): 支持一个 server 块有多个 listen 指令的转换
nginx 配置中一个 server 块可以有多个 listen 指令(如 listen 80; listen 443 ssl;), 之前转换器只保留最后一个 listen 值,导致多个 server 块最终有相同的 listen 地址, 触发验证冲突。 修改内容: - converter: 添加 listenInfo 结构体和 parseListenInfo 函数 - converter: 重构 convertServerBlock 返回 []ServerConfig,为每个 listen 创建独立配置 - validate: 使用 listen+name 组合作为唯一键,允许相同 listen 但不同 server_name Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
d191e1865d
commit
0e1a826464
@ -75,8 +75,9 @@ func validateMode(mode ServerMode) error {
|
||||
|
||||
// validateListenConflicts 检测 servers 中监听地址冲突。
|
||||
//
|
||||
// 在 multi_server 模式下,每个 server 必须有 listen 配置且不能重复。
|
||||
// 收集所有 servers[i].Listen,检查重复项并输出具体冲突信息。
|
||||
// 在 multi_server 模式下,每个 server 必须有 listen 配置。
|
||||
// 允许相同 listen 地址但不同 server_name 的配置(nginx 虚拟主机风格)。
|
||||
// 只有当 listen 和 server_name 都相同时才报冲突。
|
||||
//
|
||||
// 参数:
|
||||
// - servers: 服务器配置列表
|
||||
@ -89,15 +90,19 @@ func validateListenConflicts(servers []ServerConfig, mode ServerMode) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 使用 listen+name 组合作为唯一标识
|
||||
// 允许相同 listen 但不同 name(虚拟主机)
|
||||
seen := make(map[string]int)
|
||||
for i, s := range servers {
|
||||
if s.Listen == "" {
|
||||
return fmt.Errorf("servers[%d]: multi_server 模式下每个 server 必须配置 listen 地址", i)
|
||||
}
|
||||
if idx, exists := seen[s.Listen]; exists {
|
||||
return fmt.Errorf("监听地址冲突: servers[%d] 和 servers[%d] 都使用 %s", idx, i, s.Listen)
|
||||
// 使用 listen + name 作为唯一键
|
||||
key := s.Listen + "|" + s.Name
|
||||
if idx, exists := seen[key]; exists {
|
||||
return fmt.Errorf("监听地址冲突: servers[%d] 和 servers[%d] 都使用 %s 且 server_name 相同", idx, i, s.Listen)
|
||||
}
|
||||
seen[s.Listen] = i
|
||||
seen[key] = i
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -48,6 +48,13 @@ type locationClassification struct {
|
||||
Directives []Directive // original directives in the location block
|
||||
}
|
||||
|
||||
// listenInfo holds parsed listen directive information.
|
||||
type listenInfo struct {
|
||||
Addr string
|
||||
IsSSL bool
|
||||
IsDefault bool
|
||||
}
|
||||
|
||||
// unsupportedDirectives are known nginx directives that have no lolly equivalent.
|
||||
var unsupportedDirectives = map[string]string{
|
||||
"if": "the 'if' directive is not supported; consider using map or rewrite",
|
||||
@ -120,8 +127,8 @@ func Convert(nginxCfg *NginxConfig) (*ConvertResult, error) {
|
||||
|
||||
// 3. Convert each server block.
|
||||
for i := range serverBlocks {
|
||||
serverCfg := convertServerBlock(&serverBlocks[i], upstreams, result)
|
||||
result.Config.Servers = append(result.Config.Servers, serverCfg)
|
||||
serverCfgs := convertServerBlock(&serverBlocks[i], upstreams, result)
|
||||
result.Config.Servers = append(result.Config.Servers, serverCfgs...)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@ -196,23 +203,38 @@ func convertUpstreamServer(d *Directive) config.ProxyTarget {
|
||||
return target
|
||||
}
|
||||
|
||||
// convertServerBlock converts a server block directive to a ServerConfig.
|
||||
func convertServerBlock(d *Directive, upstreams map[string]*upstreamInfo, result *ConvertResult) config.ServerConfig {
|
||||
server := config.ServerConfig{}
|
||||
var sslDetected bool
|
||||
// convertServerBlock converts a server block directive to one or more ServerConfigs.
|
||||
// If a server block has multiple listen directives (e.g., listen 80; listen 443 ssl;),
|
||||
// it will be split into multiple ServerConfigs, one for each listen address.
|
||||
func convertServerBlock(d *Directive, upstreams map[string]*upstreamInfo, result *ConvertResult) []config.ServerConfig {
|
||||
var listens []listenInfo
|
||||
baseServer := config.ServerConfig{}
|
||||
var serverRoot string
|
||||
var serverIndex []string
|
||||
|
||||
// First pass: collect all listen directives
|
||||
for i := range d.Block {
|
||||
bd := &d.Block[i]
|
||||
if bd.Name == "listen" {
|
||||
info := parseListenInfo(bd)
|
||||
listens = append(listens, info)
|
||||
}
|
||||
}
|
||||
|
||||
// Default if no listen directive
|
||||
if len(listens) == 0 {
|
||||
listens = append(listens, listenInfo{Addr: "0.0.0.0:80"})
|
||||
}
|
||||
|
||||
// Second pass: process other directives into baseServer
|
||||
for i := range d.Block {
|
||||
bd := &d.Block[i]
|
||||
|
||||
switch bd.Name {
|
||||
case "listen":
|
||||
if parseListen(bd, &server) {
|
||||
sslDetected = true
|
||||
}
|
||||
// Already processed above
|
||||
case "server_name":
|
||||
parseServerName(bd, &server)
|
||||
parseServerName(bd, &baseServer)
|
||||
case "root":
|
||||
if len(bd.Args) > 0 {
|
||||
serverRoot = bd.Args[0]
|
||||
@ -221,45 +243,45 @@ func convertServerBlock(d *Directive, upstreams map[string]*upstreamInfo, result
|
||||
serverIndex = append(serverIndex, bd.Args...)
|
||||
case "ssl_certificate":
|
||||
if len(bd.Args) > 0 {
|
||||
server.SSL.Cert = bd.Args[0]
|
||||
baseServer.SSL.Cert = bd.Args[0]
|
||||
}
|
||||
case "ssl_certificate_key":
|
||||
if len(bd.Args) > 0 {
|
||||
server.SSL.Key = bd.Args[0]
|
||||
baseServer.SSL.Key = bd.Args[0]
|
||||
}
|
||||
case gzipType:
|
||||
parseGzip(bd, &server)
|
||||
parseGzip(bd, &baseServer)
|
||||
case "gzip_types":
|
||||
server.Compression.Types = bd.Args
|
||||
baseServer.Compression.Types = bd.Args
|
||||
case "gzip_min_length":
|
||||
if len(bd.Args) > 0 {
|
||||
if v, err := strconv.Atoi(bd.Args[0]); err == nil {
|
||||
server.Compression.MinSize = v
|
||||
baseServer.Compression.MinSize = v
|
||||
}
|
||||
}
|
||||
case "client_max_body_size":
|
||||
if len(bd.Args) > 0 {
|
||||
server.ClientMaxBodySize = bd.Args[0]
|
||||
baseServer.ClientMaxBodySize = bd.Args[0]
|
||||
}
|
||||
case "server_tokens":
|
||||
if len(bd.Args) > 0 {
|
||||
server.ServerTokens = bd.Args[0] != offValue
|
||||
baseServer.ServerTokens = bd.Args[0] != offValue
|
||||
}
|
||||
case "access_log":
|
||||
parseAccessLog(bd, result)
|
||||
case "error_log":
|
||||
parseErrorLog(bd, result)
|
||||
case "return":
|
||||
parseServerReturn(bd, &server, result)
|
||||
parseServerReturn(bd, &baseServer, result)
|
||||
case "rewrite":
|
||||
parseRewrite(bd, &server)
|
||||
parseRewrite(bd, &baseServer)
|
||||
case "location":
|
||||
classification := classifyLocation(bd, serverRoot, result)
|
||||
convertLocation(classification, &server, upstreams, result)
|
||||
convertLocation(classification, &baseServer, upstreams, result)
|
||||
case "error_page":
|
||||
parseErrorPage(bd, &server)
|
||||
parseErrorPage(bd, &baseServer)
|
||||
case "auth_basic":
|
||||
parseAuthBasic(bd, &server)
|
||||
parseAuthBasic(bd, &baseServer)
|
||||
case "auth_basic_user_file":
|
||||
if len(bd.Args) > 0 {
|
||||
result.Warnings = append(result.Warnings, Warning{
|
||||
@ -281,28 +303,22 @@ func convertServerBlock(d *Directive, upstreams map[string]*upstreamInfo, result
|
||||
}
|
||||
}
|
||||
|
||||
// If server-level root is defined but no explicit location / static config exists,
|
||||
// create a default static configuration for "/".
|
||||
// However, if location / is a proxy, don't create static config.
|
||||
// Also, fill empty root in existing static configs with server-level root.
|
||||
// Handle server-level root for static files
|
||||
if serverRoot != "" {
|
||||
hasRootLocation := false
|
||||
for i := range server.Static {
|
||||
// Inherit server-level root if location has no root specified
|
||||
if server.Static[i].Root == "" {
|
||||
server.Static[i].Root = serverRoot
|
||||
for i := range baseServer.Static {
|
||||
if baseServer.Static[i].Root == "" {
|
||||
baseServer.Static[i].Root = serverRoot
|
||||
}
|
||||
// Inherit server-level index if location has no index specified
|
||||
if len(server.Static[i].Index) == 0 && len(serverIndex) > 0 {
|
||||
server.Static[i].Index = serverIndex
|
||||
if len(baseServer.Static[i].Index) == 0 && len(serverIndex) > 0 {
|
||||
baseServer.Static[i].Index = serverIndex
|
||||
}
|
||||
if server.Static[i].Path == "/" {
|
||||
if baseServer.Static[i].Path == "/" {
|
||||
hasRootLocation = true
|
||||
}
|
||||
}
|
||||
// Check if location / is a proxy
|
||||
if !hasRootLocation {
|
||||
for _, p := range server.Proxy {
|
||||
for _, p := range baseServer.Proxy {
|
||||
if p.Path == "/" {
|
||||
hasRootLocation = true
|
||||
break
|
||||
@ -310,7 +326,7 @@ func convertServerBlock(d *Directive, upstreams map[string]*upstreamInfo, result
|
||||
}
|
||||
}
|
||||
if !hasRootLocation {
|
||||
server.Static = append(server.Static, config.StaticConfig{
|
||||
baseServer.Static = append(baseServer.Static, config.StaticConfig{
|
||||
Path: "/",
|
||||
Root: serverRoot,
|
||||
Index: serverIndex,
|
||||
@ -318,62 +334,54 @@ func convertServerBlock(d *Directive, upstreams map[string]*upstreamInfo, result
|
||||
}
|
||||
}
|
||||
|
||||
// Warn if SSL was detected (listen ... ssl) but cert/key are not configured.
|
||||
if sslDetected && (server.SSL.Cert == "" || server.SSL.Key == "") {
|
||||
result.Warnings = append(result.Warnings, Warning{
|
||||
Directive: "listen",
|
||||
Message: "SSL is enabled via listen directive but ssl_certificate and/or ssl_certificate_key are not configured; SSL config will be incomplete",
|
||||
})
|
||||
// Create servers for each listen address
|
||||
var servers []config.ServerConfig
|
||||
for _, li := range listens {
|
||||
server := baseServer // Copy base config
|
||||
server.Listen = li.Addr
|
||||
server.Default = li.IsDefault
|
||||
|
||||
// Warn if SSL listen but no cert/key
|
||||
if li.IsSSL && (server.SSL.Cert == "" || server.SSL.Key == "") {
|
||||
result.Warnings = append(result.Warnings, Warning{
|
||||
Directive: "listen",
|
||||
Message: "SSL is enabled via listen directive but ssl_certificate and/or ssl_certificate_key are not configured; SSL config will be incomplete",
|
||||
})
|
||||
}
|
||||
|
||||
servers = append(servers, server)
|
||||
}
|
||||
|
||||
// Default listen address if no listen directive was specified.
|
||||
if server.Listen == "" {
|
||||
server.Listen = "0.0.0.0:80"
|
||||
}
|
||||
|
||||
return server
|
||||
return servers
|
||||
}
|
||||
|
||||
// parseListen parses a listen directive.
|
||||
func parseListen(d *Directive, server *config.ServerConfig) bool {
|
||||
// parseListenInfo parses a listen directive and returns structured info.
|
||||
func parseListenInfo(d *Directive) listenInfo {
|
||||
info := listenInfo{}
|
||||
if len(d.Args) == 0 {
|
||||
return false
|
||||
return info
|
||||
}
|
||||
|
||||
addr := d.Args[0]
|
||||
isSSL := false
|
||||
isDefault := false
|
||||
|
||||
for _, arg := range d.Args[1:] {
|
||||
if arg == "ssl" {
|
||||
isSSL = true
|
||||
info.IsSSL = true
|
||||
}
|
||||
if arg == "default_server" {
|
||||
isDefault = true
|
||||
info.IsDefault = true
|
||||
}
|
||||
}
|
||||
|
||||
// If addr is just a port number like "80" or "8080", prefix with ":".
|
||||
// Format address: if just a port number, prefix with ":".
|
||||
if port, err := strconv.Atoi(addr); err == nil {
|
||||
server.Listen = fmt.Sprintf(":%d", port)
|
||||
info.Addr = fmt.Sprintf(":%d", port)
|
||||
} else if strings.Contains(addr, ":") {
|
||||
server.Listen = addr
|
||||
info.Addr = addr
|
||||
} else {
|
||||
server.Listen = ":" + addr
|
||||
info.Addr = ":" + addr
|
||||
}
|
||||
|
||||
// Set default_server flag.
|
||||
if isDefault {
|
||||
server.Default = true
|
||||
}
|
||||
|
||||
// Enable SSL if specified.
|
||||
if isSSL {
|
||||
server.SSL.Cert = "" // Marker cleared; cert/key set by ssl_certificate directives.
|
||||
server.SSL.Key = "" // If cert/key remain empty, a warning is added after processing.
|
||||
}
|
||||
|
||||
return isSSL
|
||||
return info
|
||||
}
|
||||
|
||||
// parseServerName parses a server_name directive.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user