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:
xfy 2026-04-27 15:21:17 +08:00
parent d191e1865d
commit 0e1a826464
2 changed files with 91 additions and 78 deletions

View File

@ -75,8 +75,9 @@ func validateMode(mode ServerMode) error {
// validateListenConflicts 检测 servers 中监听地址冲突。 // validateListenConflicts 检测 servers 中监听地址冲突。
// //
// 在 multi_server 模式下,每个 server 必须有 listen 配置且不能重复。 // 在 multi_server 模式下,每个 server 必须有 listen 配置。
// 收集所有 servers[i].Listen检查重复项并输出具体冲突信息。 // 允许相同 listen 地址但不同 server_name 的配置nginx 虚拟主机风格)。
// 只有当 listen 和 server_name 都相同时才报冲突。
// //
// 参数: // 参数:
// - servers: 服务器配置列表 // - servers: 服务器配置列表
@ -89,15 +90,19 @@ func validateListenConflicts(servers []ServerConfig, mode ServerMode) error {
return nil return nil
} }
// 使用 listen+name 组合作为唯一标识
// 允许相同 listen 但不同 name虚拟主机
seen := make(map[string]int) seen := make(map[string]int)
for i, s := range servers { for i, s := range servers {
if s.Listen == "" { if s.Listen == "" {
return fmt.Errorf("servers[%d]: multi_server 模式下每个 server 必须配置 listen 地址", i) return fmt.Errorf("servers[%d]: multi_server 模式下每个 server 必须配置 listen 地址", i)
} }
if idx, exists := seen[s.Listen]; exists { // 使用 listen + name 作为唯一键
return fmt.Errorf("监听地址冲突: servers[%d] 和 servers[%d] 都使用 %s", idx, i, s.Listen) 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 return nil
} }

View File

@ -48,6 +48,13 @@ type locationClassification struct {
Directives []Directive // original directives in the location block 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. // unsupportedDirectives are known nginx directives that have no lolly equivalent.
var unsupportedDirectives = map[string]string{ var unsupportedDirectives = map[string]string{
"if": "the 'if' directive is not supported; consider using map or rewrite", "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. // 3. Convert each server block.
for i := range serverBlocks { for i := range serverBlocks {
serverCfg := convertServerBlock(&serverBlocks[i], upstreams, result) serverCfgs := convertServerBlock(&serverBlocks[i], upstreams, result)
result.Config.Servers = append(result.Config.Servers, serverCfg) result.Config.Servers = append(result.Config.Servers, serverCfgs...)
} }
return result, nil return result, nil
@ -196,23 +203,38 @@ func convertUpstreamServer(d *Directive) config.ProxyTarget {
return target return target
} }
// convertServerBlock converts a server block directive to a ServerConfig. // convertServerBlock converts a server block directive to one or more ServerConfigs.
func convertServerBlock(d *Directive, upstreams map[string]*upstreamInfo, result *ConvertResult) config.ServerConfig { // If a server block has multiple listen directives (e.g., listen 80; listen 443 ssl;),
server := config.ServerConfig{} // it will be split into multiple ServerConfigs, one for each listen address.
var sslDetected bool func convertServerBlock(d *Directive, upstreams map[string]*upstreamInfo, result *ConvertResult) []config.ServerConfig {
var listens []listenInfo
baseServer := config.ServerConfig{}
var serverRoot string var serverRoot string
var serverIndex []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 { for i := range d.Block {
bd := &d.Block[i] bd := &d.Block[i]
switch bd.Name { switch bd.Name {
case "listen": case "listen":
if parseListen(bd, &server) { // Already processed above
sslDetected = true
}
case "server_name": case "server_name":
parseServerName(bd, &server) parseServerName(bd, &baseServer)
case "root": case "root":
if len(bd.Args) > 0 { if len(bd.Args) > 0 {
serverRoot = 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...) serverIndex = append(serverIndex, bd.Args...)
case "ssl_certificate": case "ssl_certificate":
if len(bd.Args) > 0 { if len(bd.Args) > 0 {
server.SSL.Cert = bd.Args[0] baseServer.SSL.Cert = bd.Args[0]
} }
case "ssl_certificate_key": case "ssl_certificate_key":
if len(bd.Args) > 0 { if len(bd.Args) > 0 {
server.SSL.Key = bd.Args[0] baseServer.SSL.Key = bd.Args[0]
} }
case gzipType: case gzipType:
parseGzip(bd, &server) parseGzip(bd, &baseServer)
case "gzip_types": case "gzip_types":
server.Compression.Types = bd.Args baseServer.Compression.Types = bd.Args
case "gzip_min_length": case "gzip_min_length":
if len(bd.Args) > 0 { if len(bd.Args) > 0 {
if v, err := strconv.Atoi(bd.Args[0]); err == nil { if v, err := strconv.Atoi(bd.Args[0]); err == nil {
server.Compression.MinSize = v baseServer.Compression.MinSize = v
} }
} }
case "client_max_body_size": case "client_max_body_size":
if len(bd.Args) > 0 { if len(bd.Args) > 0 {
server.ClientMaxBodySize = bd.Args[0] baseServer.ClientMaxBodySize = bd.Args[0]
} }
case "server_tokens": case "server_tokens":
if len(bd.Args) > 0 { if len(bd.Args) > 0 {
server.ServerTokens = bd.Args[0] != offValue baseServer.ServerTokens = bd.Args[0] != offValue
} }
case "access_log": case "access_log":
parseAccessLog(bd, result) parseAccessLog(bd, result)
case "error_log": case "error_log":
parseErrorLog(bd, result) parseErrorLog(bd, result)
case "return": case "return":
parseServerReturn(bd, &server, result) parseServerReturn(bd, &baseServer, result)
case "rewrite": case "rewrite":
parseRewrite(bd, &server) parseRewrite(bd, &baseServer)
case "location": case "location":
classification := classifyLocation(bd, serverRoot, result) classification := classifyLocation(bd, serverRoot, result)
convertLocation(classification, &server, upstreams, result) convertLocation(classification, &baseServer, upstreams, result)
case "error_page": case "error_page":
parseErrorPage(bd, &server) parseErrorPage(bd, &baseServer)
case "auth_basic": case "auth_basic":
parseAuthBasic(bd, &server) parseAuthBasic(bd, &baseServer)
case "auth_basic_user_file": case "auth_basic_user_file":
if len(bd.Args) > 0 { if len(bd.Args) > 0 {
result.Warnings = append(result.Warnings, Warning{ 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, // Handle server-level root for static files
// 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.
if serverRoot != "" { if serverRoot != "" {
hasRootLocation := false hasRootLocation := false
for i := range server.Static { for i := range baseServer.Static {
// Inherit server-level root if location has no root specified if baseServer.Static[i].Root == "" {
if server.Static[i].Root == "" { baseServer.Static[i].Root = serverRoot
server.Static[i].Root = serverRoot
} }
// Inherit server-level index if location has no index specified if len(baseServer.Static[i].Index) == 0 && len(serverIndex) > 0 {
if len(server.Static[i].Index) == 0 && len(serverIndex) > 0 { baseServer.Static[i].Index = serverIndex
server.Static[i].Index = serverIndex
} }
if server.Static[i].Path == "/" { if baseServer.Static[i].Path == "/" {
hasRootLocation = true hasRootLocation = true
} }
} }
// Check if location / is a proxy
if !hasRootLocation { if !hasRootLocation {
for _, p := range server.Proxy { for _, p := range baseServer.Proxy {
if p.Path == "/" { if p.Path == "/" {
hasRootLocation = true hasRootLocation = true
break break
@ -310,7 +326,7 @@ func convertServerBlock(d *Directive, upstreams map[string]*upstreamInfo, result
} }
} }
if !hasRootLocation { if !hasRootLocation {
server.Static = append(server.Static, config.StaticConfig{ baseServer.Static = append(baseServer.Static, config.StaticConfig{
Path: "/", Path: "/",
Root: serverRoot, Root: serverRoot,
Index: serverIndex, 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. // Create servers for each listen address
if sslDetected && (server.SSL.Cert == "" || server.SSL.Key == "") { var servers []config.ServerConfig
result.Warnings = append(result.Warnings, Warning{ for _, li := range listens {
Directive: "listen", server := baseServer // Copy base config
Message: "SSL is enabled via listen directive but ssl_certificate and/or ssl_certificate_key are not configured; SSL config will be incomplete", 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. return servers
if server.Listen == "" {
server.Listen = "0.0.0.0:80"
}
return server
} }
// parseListen parses a listen directive. // parseListenInfo parses a listen directive and returns structured info.
func parseListen(d *Directive, server *config.ServerConfig) bool { func parseListenInfo(d *Directive) listenInfo {
info := listenInfo{}
if len(d.Args) == 0 { if len(d.Args) == 0 {
return false return info
} }
addr := d.Args[0] addr := d.Args[0]
isSSL := false
isDefault := false
for _, arg := range d.Args[1:] { for _, arg := range d.Args[1:] {
if arg == "ssl" { if arg == "ssl" {
isSSL = true info.IsSSL = true
} }
if arg == "default_server" { 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 { 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, ":") { } else if strings.Contains(addr, ":") {
server.Listen = addr info.Addr = addr
} else { } else {
server.Listen = ":" + addr info.Addr = ":" + addr
} }
// Set default_server flag. return info
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
} }
// parseServerName parses a server_name directive. // parseServerName parses a server_name directive.