2026-09-23 17:46:21 +08:00
|
|
|
package nginx
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2026-10-01 16:54:35 +08:00
|
|
|
"github.com/havline/havline/internal/proxy"
|
2026-09-23 17:46:21 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ParseServerEndpoints 从 Nginx 配置文本里抽取各 server 块的 (server_name, listen 端口) 组合,
|
|
|
|
|
// 返回 proxy.Endpoint(与表单规则的端点同一类型),用于手动(custom)规则的监听占用校验。
|
|
|
|
|
//
|
|
|
|
|
// 采取保守策略:解析不出的写法(include 片段、正则 server_name、listen unix:、变量)一律忽略。
|
|
|
|
|
// 该校验只用于保存时提示,宁可漏报也不能因为解析失败而拦住保存。
|
|
|
|
|
func ParseServerEndpoints(content string) []proxy.Endpoint {
|
|
|
|
|
var out []proxy.Endpoint
|
|
|
|
|
seen := map[string]bool{}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
inServer bool
|
|
|
|
|
depth int
|
|
|
|
|
ports []int
|
|
|
|
|
names []string
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
flush := func() {
|
|
|
|
|
// 未写 listen 的 server 块按 Nginx 默认监听端口 80 处理
|
|
|
|
|
if len(ports) == 0 && len(names) > 0 {
|
|
|
|
|
ports = []int{80}
|
|
|
|
|
}
|
|
|
|
|
for _, port := range ports {
|
|
|
|
|
for _, name := range names {
|
|
|
|
|
key := name + ":" + strconv.Itoa(port)
|
|
|
|
|
if seen[key] {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seen[key] = true
|
|
|
|
|
out = append(out, proxy.Endpoint{Hostname: name, Port: port})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
inServer, depth, ports, names = false, 0, nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, raw := range strings.Split(content, "\n") {
|
|
|
|
|
line := stripInlineComment(raw)
|
|
|
|
|
if line == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
opens := strings.Count(line, "{")
|
|
|
|
|
closes := strings.Count(line, "}")
|
|
|
|
|
if !inServer {
|
|
|
|
|
if opens > 0 && serverBlockOpens(line) {
|
|
|
|
|
inServer, depth = true, opens-closes
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
switch {
|
|
|
|
|
case strings.HasPrefix(line, "listen"):
|
|
|
|
|
if port, ok := listenPort(line); ok {
|
|
|
|
|
ports = append(ports, port)
|
|
|
|
|
}
|
|
|
|
|
case strings.HasPrefix(line, "server_name"):
|
|
|
|
|
names = append(names, serverNames(line)...)
|
|
|
|
|
}
|
|
|
|
|
depth += opens - closes
|
|
|
|
|
if depth <= 0 {
|
|
|
|
|
flush()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func serverBlockOpens(line string) bool {
|
|
|
|
|
fields := strings.Fields(line)
|
|
|
|
|
return len(fields) > 0 && fields[0] == "server" && strings.Contains(line, "{")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// listenPort 从 listen 指令取出端口号;unix socket 等无法判定的写法返回 false
|
|
|
|
|
func listenPort(line string) (int, bool) {
|
|
|
|
|
fields := strings.Fields(line)
|
|
|
|
|
if len(fields) < 2 {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
addr := strings.TrimSuffix(fields[1], ";")
|
|
|
|
|
if strings.Contains(addr, "unix:") {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
if idx := strings.LastIndex(addr, ":"); idx >= 0 {
|
|
|
|
|
addr = addr[idx+1:]
|
|
|
|
|
}
|
|
|
|
|
port, err := strconv.Atoi(addr)
|
|
|
|
|
if err != nil || port <= 0 || port > 65535 {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
return port, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// serverNames 从 server_name 指令取出字面量主机名;正则(~ 开头)与占位符(含 $)跳过
|
|
|
|
|
func serverNames(line string) []string {
|
|
|
|
|
fields := strings.Fields(line)
|
|
|
|
|
if len(fields) < 2 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
var names []string
|
|
|
|
|
for _, field := range fields[1:] {
|
|
|
|
|
name := strings.ToLower(strings.TrimSuffix(field, ";"))
|
|
|
|
|
if name == "" || name == "_" || strings.HasPrefix(name, "~") || strings.Contains(name, "$") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
names = append(names, name)
|
|
|
|
|
}
|
|
|
|
|
return names
|
|
|
|
|
}
|