From a909ecf24593aea436d1df7d311b47c81df3f0df Mon Sep 17 00:00:00 2001 From: Zhan Ziyang Date: Sat, 22 Aug 2026 15:43:07 +0800 Subject: [PATCH] feat: add Node SSR Nginx upstream switching --- internal/hostnginx/nodessr.go | 134 +++++++++++++++++++++++++++++ internal/hostnginx/nodessr_test.go | 30 +++++++ 2 files changed, 164 insertions(+) create mode 100644 internal/hostnginx/nodessr.go create mode 100644 internal/hostnginx/nodessr_test.go diff --git a/internal/hostnginx/nodessr.go b/internal/hostnginx/nodessr.go new file mode 100644 index 0000000..9fc14b1 --- /dev/null +++ b/internal/hostnginx/nodessr.go @@ -0,0 +1,134 @@ +package hostnginx + +import ( + "errors" + "fmt" + "net" + "strconv" + "strings" +) + +const ( + nodeSsrManagedBegin = "# yms-update managed nodeSsr upstream begin" + nodeSsrManagedEnd = "# yms-update managed nodeSsr upstream end" + nodeSsrPort18910 = 18910 + nodeSsrPort28910 = 28910 +) + +// ActiveNodeSsrPort 从 Node SSR 受控 upstream 区块读取唯一活动槽位端口。 +func ActiveNodeSsrPort(content []byte) (int, error) { + block, err := parseNodeSsrBlock(content) + if err != nil { + return 0, err + } + active := 0 + for _, server := range block.servers { + if !server.commented { + if active != 0 { + return 0, errors.New("managed Nginx upstream contains more than one active Node SSR server") + } + active = server.port + } + } + if active == 0 { + return 0, errors.New("managed Nginx upstream does not contain an active Node SSR server") + } + return active, nil +} + +// RenderNodeSsrPort 只切换 Node SSR 受控 upstream 区块中的活动服务器行。 +func RenderNodeSsrPort(content []byte, activePort int) ([]byte, error) { + if activePort != nodeSsrPort18910 && activePort != nodeSsrPort28910 { + return nil, fmt.Errorf("host Nginx Node SSR port must be 18910 or 28910: %d", activePort) + } + block, err := parseNodeSsrBlock(content) + if err != nil { + return nil, err + } + for _, server := range block.servers { + indentLength := len(block.lines[server.line]) - len(strings.TrimLeft(block.lines[server.line], " \t")) + indent := block.lines[server.line][:indentLength] + serverText := strings.TrimPrefix(strings.TrimSpace(block.lines[server.line]), "# ") + if server.port == activePort { + block.lines[server.line] = indent + serverText + } else { + block.lines[server.line] = indent + "# " + serverText + } + } + rendered := []byte(strings.Join(block.lines, "\n")) + if _, err := ActiveNodeSsrPort(rendered); err != nil { + return nil, fmt.Errorf("validate rendered host Nginx Node SSR upstream: %w", err) + } + return rendered, nil +} + +type nodeSsrBlock struct { + lines []string + servers []nodeSsrServer +} + +type nodeSsrServer struct { + line int + port int + commented bool +} + +func parseNodeSsrBlock(content []byte) (nodeSsrBlock, error) { + if len(content) == 0 { + return nodeSsrBlock{}, errors.New("host Nginx configuration is empty") + } + lines := strings.Split(string(content), "\n") + begin, end := -1, -1 + for index, line := range lines { + switch strings.TrimSpace(line) { + case nodeSsrManagedBegin: + if begin != -1 { + return nodeSsrBlock{}, errors.New("host Nginx configuration contains duplicate Node SSR upstream begin markers") + } + begin = index + case nodeSsrManagedEnd: + if end != -1 { + return nodeSsrBlock{}, errors.New("host Nginx configuration contains duplicate Node SSR upstream end markers") + } + end = index + } + } + if begin == -1 || end <= begin { + return nodeSsrBlock{}, errors.New("host Nginx configuration requires one ordered Node SSR upstream marker pair") + } + servers := make([]nodeSsrServer, 0, 2) + seen := make(map[int]struct{}, 2) + for index := begin + 1; index < end; index++ { + trimmed := strings.TrimSpace(lines[index]) + commented := strings.HasPrefix(trimmed, "# server ") + active := strings.HasPrefix(trimmed, "server ") + if !commented && !active { + if trimmed != "" { + return nodeSsrBlock{}, fmt.Errorf("managed Node SSR upstream contains an unexpected line: %q", trimmed) + } + continue + } + serverText := strings.TrimPrefix(trimmed, "# ") + fields := strings.Fields(serverText) + if len(fields) != 4 || fields[0] != "server" || fields[2] != "max_fails=1" || fields[3] != "fail_timeout=2s;" { + return nodeSsrBlock{}, fmt.Errorf("managed Node SSR upstream server line has an unsupported format: %q", trimmed) + } + _, portText, err := net.SplitHostPort(fields[1]) + if err != nil { + return nodeSsrBlock{}, fmt.Errorf("parse managed Node SSR upstream address %q: %w", fields[1], err) + } + port, err := strconv.Atoi(portText) + if err != nil || (port != nodeSsrPort18910 && port != nodeSsrPort28910) { + return nodeSsrBlock{}, fmt.Errorf("managed Node SSR upstream contains unsupported port %q", portText) + } + if _, exists := seen[port]; exists { + return nodeSsrBlock{}, fmt.Errorf("managed Node SSR upstream contains duplicate port %d", port) + } + seen[port] = struct{}{} + servers = append(servers, nodeSsrServer{line: index, port: port, commented: commented}) + } + if len(servers) != 2 { + return nodeSsrBlock{}, fmt.Errorf("managed Node SSR upstream must contain exactly two servers, got %d", len(servers)) + } + return nodeSsrBlock{lines: lines, servers: servers}, nil +} diff --git a/internal/hostnginx/nodessr_test.go b/internal/hostnginx/nodessr_test.go new file mode 100644 index 0000000..8e77cc6 --- /dev/null +++ b/internal/hostnginx/nodessr_test.go @@ -0,0 +1,30 @@ +package hostnginx + +import ( + "strings" + "testing" +) + +const nodeSsrConfiguration = `events {} +# yms-update managed nodeSsr upstream begin +# server 127.0.0.1:18910 max_fails=1 fail_timeout=2s; +server 127.0.0.1:28910 max_fails=1 fail_timeout=2s; +# yms-update managed nodeSsr upstream end +` + +func TestActiveNodeSsrPort(t *testing.T) { + port, err := ActiveNodeSsrPort([]byte(nodeSsrConfiguration)) + if err != nil || port != nodeSsrPort28910 { + t.Fatalf("unexpected active Node SSR port: port=%d err=%v", port, err) + } +} + +func TestRenderNodeSsrPort(t *testing.T) { + rendered, err := RenderNodeSsrPort([]byte(nodeSsrConfiguration), nodeSsrPort18910) + if err != nil { + t.Fatalf("render Node SSR port: %v", err) + } + if !strings.Contains(string(rendered), "\nserver 127.0.0.1:18910") || !strings.Contains(string(rendered), "\n# server 127.0.0.1:28910") { + t.Fatalf("unexpected Node SSR rendering: %s", rendered) + } +}