feat: add support for root block configuration in NgxConfig and implement parsing logic

This commit is contained in:
0xJacky
2026-03-16 03:15:27 +00:00
parent 88a86edcd2
commit 9e41ecf9df
5 changed files with 148 additions and 0 deletions
+1
View File
@@ -3,6 +3,7 @@ import { http } from '@uozi-admin/request'
export interface NgxConfig {
file_name?: string
name: string
root_block?: 'http' | 'stream'
upstreams?: NgxUpstream[]
servers: NgxServer[]
custom?: string
+31
View File
@@ -17,6 +17,32 @@ func buildComments(orig string, indent int) (content string) {
return
}
func wrapRootBlock(name, content string) string {
trimmedContent := strings.TrimSpace(content)
if trimmedContent == "" {
return fmt.Sprintf("%s {\n}\n", name)
}
var builder strings.Builder
builder.WriteString(name)
builder.WriteString(" {\n")
scanner := bufio.NewScanner(strings.NewReader(strings.TrimRight(content, "\n")))
for scanner.Scan() {
line := scanner.Text()
if line == "" {
builder.WriteByte('\n')
continue
}
builder.WriteByte('\t')
builder.WriteString(line)
builder.WriteByte('\n')
}
builder.WriteString("}\n")
return builder.String()
}
func (c *NgxConfig) BuildConfig() (content string, err error) {
// Custom
if c.Custom != "" {
@@ -82,6 +108,11 @@ func (c *NgxConfig) BuildConfig() (content string, err error) {
content += fmt.Sprintf("%sserver {\n%s}\n\n", comments, server)
}
if c.RootBlock != "" {
content = wrapRootBlock(c.RootBlock, content)
}
p := parser.NewStringParser(content, parser.WithSkipValidDirectivesErr())
cfg, err := p.Parse()
if err != nil {
+31
View File
@@ -13,6 +13,8 @@ const (
Server = "server"
Location = "location"
Upstream = "upstream"
Http = "http"
Stream = "stream"
)
func (s *NgxServer) ParseServer(directive config.IDirective) {
@@ -170,11 +172,40 @@ func buildComment(c []string) string {
return strings.ReplaceAll(strings.Join(c, "\n"), "#", "")
}
func shouldUnwrapRootBlock(block config.IBlock) config.IDirective {
if block == nil {
return nil
}
directives := block.GetDirectives()
if len(directives) != 1 {
return nil
}
directive := directives[0]
if directive.GetBlock() == nil {
return nil
}
switch directive.GetName() {
case Http, Stream:
return directive
default:
return nil
}
}
func parse(block config.IBlock, ngxConfig *NgxConfig) (err error) {
if block == nil {
err = ErrBlockIsNil
return
}
if rootBlock := shouldUnwrapRootBlock(block); rootBlock != nil {
ngxConfig.RootBlock = rootBlock.GetName()
return parse(rootBlock.GetBlock(), ngxConfig)
}
for _, v := range block.GetDirectives() {
comments := buildComment(v.GetComment())
switch v.GetName() {
+84
View File
@@ -0,0 +1,84 @@
package nginx
import (
"strings"
"testing"
)
func TestParseNgxConfigByContent_UnwrapsRootStreamBlock(t *testing.T) {
content := `stream {
log_format vless_lb '$remote_addr:$remote_port [$time_local] '
'$protocol $status '
'connect=$upstream_connect_time '
'session=$session_time '
'sent=$bytes_sent recv=$bytes_received '
'upstream=$upstream_addr';
access_log /var/log/nginx/xray_stream_access.log vless_lb;
error_log /var/log/nginx/xray_stream_error.log warn;
upstream xray_vless_449 {
least_conn;
server 1.1.1.1:449 fail_timeout=15s max_fails=3;
server 1.1.1.1:449 fail_timeout=15s max_fails=3;
}
server {
listen 449;
proxy_connect_timeout 5s;
proxy_timeout 15m;
proxy_pass xray_vless_449;
}
include /etc/nginx/streams-enabled/*;
}`
ngxConfig, err := ParseNgxConfigByContent(content)
if err != nil {
t.Fatalf("ParseNgxConfigByContent() error = %v", err)
}
if ngxConfig.RootBlock != Stream {
t.Fatalf("RootBlock = %q, want %q", ngxConfig.RootBlock, Stream)
}
if len(ngxConfig.Upstreams) != 1 {
t.Fatalf("len(Upstreams) = %d, want 1", len(ngxConfig.Upstreams))
}
if len(ngxConfig.Servers) != 1 {
t.Fatalf("len(Servers) = %d, want 1", len(ngxConfig.Servers))
}
if !strings.Contains(ngxConfig.Custom, "log_format vless_lb") {
t.Fatalf("Custom = %q, want log_format directive", ngxConfig.Custom)
}
if !strings.Contains(ngxConfig.Custom, "include /etc/nginx/streams-enabled/*;") {
t.Fatalf("Custom = %q, want include directive", ngxConfig.Custom)
}
builtContent, err := ngxConfig.BuildConfig()
if err != nil {
t.Fatalf("BuildConfig() error = %v", err)
}
if !strings.Contains(builtContent, "stream {") {
t.Fatalf("built content = %q, want stream root block", builtContent)
}
if !strings.Contains(builtContent, "upstream xray_vless_449 {") {
t.Fatalf("built content = %q, want upstream block", builtContent)
}
if !strings.Contains(builtContent, "least_conn;") {
t.Fatalf("built content = %q, want least_conn directive", builtContent)
}
if !strings.Contains(builtContent, "server {") {
t.Fatalf("built content = %q, want server block", builtContent)
}
if !strings.Contains(builtContent, "proxy_pass xray_vless_449;") {
t.Fatalf("built content = %q, want proxy_pass directive", builtContent)
}
}
+1
View File
@@ -10,6 +10,7 @@ import (
type NgxConfig struct {
FileName string `json:"file_name"`
Name string `json:"name"`
RootBlock string `json:"root_block,omitempty"`
Upstreams []*NgxUpstream `json:"upstreams"`
Servers []*NgxServer `json:"servers"`
Custom string `json:"custom"`