├── full-example ├── .gitignore ├── unit-test.conf ├── proxy.conf ├── fastcgi.conf ├── nginx-structure.conf ├── nginx.conf ├── formatting │ ├── raw.conf │ ├── issue17-raw.conf │ ├── issue17-formatted.conf │ └── formatted.conf ├── mime.types └── nginx2.conf ├── testdata ├── include-glob │ ├── http.conf │ ├── events.conf │ ├── conf.d │ │ ├── location-root.conf │ │ └── location-letsencrypt.conf │ ├── sites-enabled │ │ ├── conf.d │ │ │ ├── location-root.conf │ │ │ └── location-letsencrypt.conf │ │ ├── mysite.com.conf │ │ └── example.com.conf │ └── nginx.conf ├── issues │ ├── 37.conf │ ├── 22.conf │ ├── 50.conf │ ├── 20.conf │ ├── 17.conf │ └── 31.conf └── full_conf │ ├── scgi_params │ ├── uwsgi_params │ ├── fastcgi_params │ ├── fastcgi.conf │ ├── koi-win │ ├── nginx.conf │ ├── koi-utf │ ├── win-utf │ └── mime.types ├── gopher.png ├── config ├── doc.go ├── include_test.go ├── block.go ├── directive.go ├── location.go ├── statement.go ├── block_test.go ├── config_test.go ├── include.go ├── server.go ├── config.go ├── lua_block.go ├── http.go ├── upstream.go └── upstream_server.go ├── parser ├── doc.go ├── skip_valid_block.go ├── token │ ├── token.go │ └── token_test.go ├── lexer.go ├── lexer_test.go ├── parser.go └── valid_directives.go ├── dumper ├── doc.go ├── include_test.go ├── dumper_test.go ├── location_test.go ├── lua.go ├── config_test.go ├── server_test.go ├── http_test.go ├── directive_test.go ├── upstream_server_test.go ├── block_test.go ├── upstream_test.go └── dumper.go ├── go.mod ├── .gitignore ├── Makefile ├── examples ├── adding-server │ └── main.go ├── parse-nginx-conf-get-listen-port │ └── main.go ├── update-server-listen-port │ └── main.go ├── add-custom-directive │ └── main.go ├── update-directive │ └── main.go ├── formatting │ └── main.go └── dump-nginx-config │ └── main.go ├── .github └── workflows │ └── check.yml ├── LICENSE ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── AGENTS.md ├── README.md └── GUIDE.md /full-example/.gitignore: -------------------------------------------------------------------------------- 1 | unittest -------------------------------------------------------------------------------- /testdata/include-glob/http.conf: -------------------------------------------------------------------------------- 1 | 2 | include sites-enabled/*.conf; -------------------------------------------------------------------------------- /gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tufanbarisyildirim/gonginx/HEAD/gopher.png -------------------------------------------------------------------------------- /testdata/include-glob/events.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 4096; 3 | } -------------------------------------------------------------------------------- /config/doc.go: -------------------------------------------------------------------------------- 1 | // Package config models nginx directives and blocks. 2 | package config 3 | -------------------------------------------------------------------------------- /testdata/include-glob/conf.d/location-root.conf: -------------------------------------------------------------------------------- 1 | location / { 2 | try_files $uri $uri/ =404; 3 | } -------------------------------------------------------------------------------- /full-example/unit-test.conf: -------------------------------------------------------------------------------- 1 | user nginx nginx; 2 | worker_processes 5; 3 | include /etc/nginx/conf/*.conf; -------------------------------------------------------------------------------- /parser/doc.go: -------------------------------------------------------------------------------- 1 | // Package parser parses nginx configuration files into structured objects. 2 | package parser 3 | -------------------------------------------------------------------------------- /dumper/doc.go: -------------------------------------------------------------------------------- 1 | // Package dumper renders configuration structures back into nginx config text. 2 | package dumper 3 | -------------------------------------------------------------------------------- /testdata/include-glob/sites-enabled/conf.d/location-root.conf: -------------------------------------------------------------------------------- 1 | # if this file is included, panic should be raised 2 | Hello World -------------------------------------------------------------------------------- /testdata/include-glob/nginx.conf: -------------------------------------------------------------------------------- 1 | user www www; 2 | worker_processes 5; 3 | 4 | include events.conf; 5 | include http.conf; -------------------------------------------------------------------------------- /testdata/include-glob/sites-enabled/conf.d/location-letsencrypt.conf: -------------------------------------------------------------------------------- 1 | # if this file is included, panic should be raised 2 | Hello World -------------------------------------------------------------------------------- /testdata/include-glob/conf.d/location-letsencrypt.conf: -------------------------------------------------------------------------------- 1 | location /.well-known/acme-challenge { 2 | default_type "text/plain"; 3 | root /var/www/html; 4 | } -------------------------------------------------------------------------------- /testdata/include-glob/sites-enabled/mysite.com.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name mysite.com; 4 | root /var/www/html/mysite.com; 5 | include conf.d/loc*.conf; 6 | } -------------------------------------------------------------------------------- /testdata/include-glob/sites-enabled/example.com.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name example.com; 4 | root /var/www/html/example.com; 5 | include conf.d/loc*.conf; 6 | } -------------------------------------------------------------------------------- /testdata/issues/37.conf: -------------------------------------------------------------------------------- 1 | http { 2 | include mime.types; 3 | default_type application/octet-stream; 4 | my_custom_directive myParam1 myParam2; 5 | my_custom_directive2 myParam1 myParam2; 6 | my_custom_directive3 myParam1 myParam2; 7 | } -------------------------------------------------------------------------------- /testdata/issues/22.conf: -------------------------------------------------------------------------------- 1 | server { 2 | location = /foo { 3 | rewrite_by_lua_block { 4 | res = ngx.location.capture("/memc", 5 | { args = { cmd = "incr", key = ngx.var.uri } } # comment contained unexpect '{' 6 | # comment contained unexpect '}' 7 | ) 8 | t = { key="foo", val="bar" } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tufanbarisyildirim/gonginx 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/imega/luaformatter v0.0.0-20211025140405-86b0a68d6bef 7 | gotest.tools/v3 v3.5.1 8 | ) 9 | 10 | require ( 11 | github.com/google/go-cmp v0.6.0 // indirect 12 | github.com/timtadh/data-structures v0.5.3 // indirect 13 | github.com/timtadh/lexmachine v0.2.2 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /full-example/proxy.conf: -------------------------------------------------------------------------------- 1 | proxy_redirect off; 2 | proxy_set_header Host $host; 3 | proxy_set_header X-Real-IP $remote_addr; 4 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 5 | client_max_body_size 10m; 6 | client_body_buffer_size 128k; 7 | proxy_connect_timeout 90; 8 | proxy_send_timeout 90; 9 | proxy_read_timeout 90; 10 | proxy_buffers 32 4k; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Editors and IDEs metadata 18 | # VSCode 19 | .vscode/ 20 | 21 | # GoLand 22 | .idea 23 | 24 | # macOS 25 | .DS_Store 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PWD := $(shell pwd) 2 | export GO111MODULE=on 3 | 4 | test: 5 | go test -race -cover ${PWD}/{config,dumper,parser,parser/token} 6 | 7 | test-parser: 8 | go test -race -cover ${PWD}/parser/parser.go 9 | 10 | lint: 11 | golangci-lint run ./... 12 | golint ./... 13 | 14 | check: 15 | staticcheck ./... 16 | 17 | example: 18 | go run ${PWD}/examples/$(example) 19 | 20 | bench: 21 | go test -bench=. -benchmem ${PWD}/parser 22 | 23 | fmt: 24 | find . -name "*.go" | xargs gofmt -w -s 25 | 26 | deps: 27 | go get -v all 28 | 29 | .PHONY: fmt deps test lint 30 | -------------------------------------------------------------------------------- /parser/skip_valid_block.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import "strings" 4 | 5 | var skipValidBlocks = `types 6 | map 7 | ` 8 | 9 | // SkipValidBlocks defines a list of valid blocks to be skipped during initialization. 10 | // This string is split by newline characters, with each trimmed block name added to the SkipValidBlocks mapping. 11 | var SkipValidBlocks map[string]struct{} = map[string]struct{}{} 12 | 13 | func init() { 14 | blocks := strings.Split(skipValidBlocks, "\n") 15 | for _, block := range blocks { 16 | SkipValidBlocks[strings.TrimSpace(block)] = struct{}{} 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /testdata/full_conf/scgi_params: -------------------------------------------------------------------------------- 1 | 2 | scgi_param REQUEST_METHOD $request_method; 3 | scgi_param REQUEST_URI $request_uri; 4 | scgi_param QUERY_STRING $query_string; 5 | scgi_param CONTENT_TYPE $content_type; 6 | 7 | scgi_param DOCUMENT_URI $document_uri; 8 | scgi_param DOCUMENT_ROOT $document_root; 9 | scgi_param SCGI 1; 10 | scgi_param SERVER_PROTOCOL $server_protocol; 11 | scgi_param REQUEST_SCHEME $scheme; 12 | scgi_param HTTPS $https if_not_empty; 13 | 14 | scgi_param REMOTE_ADDR $remote_addr; 15 | scgi_param REMOTE_PORT $remote_port; 16 | scgi_param SERVER_PORT $server_port; 17 | scgi_param SERVER_NAME $server_name; 18 | -------------------------------------------------------------------------------- /testdata/full_conf/uwsgi_params: -------------------------------------------------------------------------------- 1 | 2 | uwsgi_param QUERY_STRING $query_string; 3 | uwsgi_param REQUEST_METHOD $request_method; 4 | uwsgi_param CONTENT_TYPE $content_type; 5 | uwsgi_param CONTENT_LENGTH $content_length; 6 | 7 | uwsgi_param REQUEST_URI $request_uri; 8 | uwsgi_param PATH_INFO $document_uri; 9 | uwsgi_param DOCUMENT_ROOT $document_root; 10 | uwsgi_param SERVER_PROTOCOL $server_protocol; 11 | uwsgi_param REQUEST_SCHEME $scheme; 12 | uwsgi_param HTTPS $https if_not_empty; 13 | 14 | uwsgi_param REMOTE_ADDR $remote_addr; 15 | uwsgi_param REMOTE_PORT $remote_port; 16 | uwsgi_param SERVER_PORT $server_port; 17 | uwsgi_param SERVER_NAME $server_name; 18 | -------------------------------------------------------------------------------- /dumper/include_test.go: -------------------------------------------------------------------------------- 1 | package dumper 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tufanbarisyildirim/gonginx/config" 7 | "gotest.tools/v3/assert" 8 | ) 9 | 10 | func TestConfig_IncludeToString(t *testing.T) { 11 | t.Parallel() 12 | include := &config.Include{ 13 | Directive: &config.Directive{ 14 | Name: "include", 15 | Parameters: []config.Parameter{{Value: "/etc/nginx/conf.d/*.conf"}}, 16 | }, 17 | IncludePath: "/etc/nginx/conf.d/*.conf", 18 | } 19 | assert.Equal(t, "include /etc/nginx/conf.d/*.conf;", DumpDirective(include, NoIndentStyle)) 20 | var i interface{} = include 21 | _, ok := i.(config.IDirective) 22 | //_, ok2 := i.(IncludeDirective)// TODO(tufan):reactivate here after getting include and file things done 23 | assert.Assert(t, ok) 24 | //assert.Assert(t, ok2) 25 | } 26 | -------------------------------------------------------------------------------- /examples/adding-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tufanbarisyildirim/gonginx/config" 7 | "github.com/tufanbarisyildirim/gonginx/dumper" 8 | "github.com/tufanbarisyildirim/gonginx/parser" 9 | ) 10 | 11 | func main() { 12 | p := parser.NewStringParser(`http{ 13 | upstream my_backend{ 14 | server 127.0.0.1:443; 15 | server 127.0.0.2:443 backup; 16 | } 17 | }`) 18 | 19 | conf, err := p.Parse() 20 | if err != nil { 21 | panic(err) 22 | } 23 | upstreams := conf.FindUpstreams() 24 | 25 | upstreams[0].AddServer(&config.UpstreamServer{ 26 | Address: "127.0.0.1:443", 27 | Parameters: map[string]string{ 28 | "weight": "5", 29 | }, 30 | Flags: []string{"down"}, 31 | }) 32 | 33 | fmt.Println(dumper.DumpBlock(conf.Block, dumper.IndentedStyle)) 34 | 35 | } 36 | -------------------------------------------------------------------------------- /testdata/issues/50.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1;#global inlinecomment 2 | events { 3 | worker_connections 1024;#event inlinecomment 4 | } 5 | http { 6 | include mime.types;#http inlinecomment 7 | default_type application/octet-stream; 8 | sendfile on; 9 | keepalive_timeout 65; 10 | server { 11 | listen 80;#server inlinecomment 12 | server_name localhost; 13 | location / { 14 | root /usr/share/nginx/html;#location inlinecomment 15 | index index.html index.htm; 16 | } 17 | error_page 500 502 503 504 /50x.html; 18 | location = /50x.html { 19 | root /usr/share/nginx/html; 20 | } 21 | } 22 | server { 23 | listen 8000; 24 | location / { 25 | root html; 26 | index index.html index.htm; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /dumper/dumper_test.go: -------------------------------------------------------------------------------- 1 | package dumper 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestStyle_Iterate(t *testing.T) { 9 | t.Parallel() 10 | tests := []struct { 11 | name string 12 | style *Style 13 | want *Style 14 | }{ 15 | { 16 | name: "iteration test", 17 | style: NewStyle(), 18 | want: &Style{ 19 | SortDirectives: false, 20 | StartIndent: 4, 21 | Indent: 4, 22 | }, 23 | }, 24 | { 25 | name: "always empty no interation constant", 26 | style: NoIndentStyle, 27 | want: &Style{ 28 | SortDirectives: false, 29 | StartIndent: 0, 30 | Indent: 0, 31 | }, 32 | }, 33 | } 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | if got := tt.style.Iterate(); !reflect.DeepEqual(got, tt.want) { 37 | t.Errorf("Style.Iterate() = %v, want %v", got, tt.want) 38 | } 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /testdata/issues/20.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | server_name _; 5 | location / { 6 | content_by_lua_block { -- comment 7 | local foo = "bar" -- comment } } 8 | location = /random { 9 | set_by_lua_block $file_name { 10 | # comment contained unexpect '{' 11 | local t = ngx.var.uri 12 | local query = string.find(t, "?", 1) 13 | if query ~= nil then 14 | t = string.sub(t, 1, query-1) 15 | end 16 | return t; 17 | } 18 | set_by_lua_block $random { 19 | # comment contained unexpect '{' 20 | return math.random(1, 100) 21 | } 22 | return 403 "Random number: $random"; 23 | } 24 | } 25 | 26 | server { 27 | listen 80; 28 | server_name _; 29 | location = /unexpected-eof { 30 | rewrite ^(.*)$ https://${server_name}$1 permanent; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | 11 | audit: 12 | runs-on: ubuntu-24.04 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: 1.24.0 20 | 21 | - name: Verify dependencies 22 | run: go mod verify 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Run go vet 28 | run: go vet ./... 29 | 30 | - name: Install staticcheck 31 | run: go install honnef.co/go/tools/cmd/staticcheck@latest 32 | 33 | - name: Run staticcheck 34 | run: staticcheck ./... 35 | 36 | - name: Install golint 37 | run: go install golang.org/x/lint/golint@latest 38 | 39 | - name: Run golint 40 | run: golint ./... 41 | 42 | - name: Run tests 43 | run: go test -race -vet=off ./... -------------------------------------------------------------------------------- /full-example/fastcgi.conf: -------------------------------------------------------------------------------- 1 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 2 | fastcgi_param QUERY_STRING $query_string; 3 | fastcgi_param REQUEST_METHOD $request_method; 4 | fastcgi_param CONTENT_TYPE $content_type; 5 | fastcgi_param CONTENT_LENGTH $content_length; 6 | fastcgi_param SCRIPT_NAME $fastcgi_script_name; 7 | fastcgi_param REQUEST_URI $request_uri; 8 | fastcgi_param DOCUMENT_URI $document_uri; 9 | fastcgi_param DOCUMENT_ROOT $document_root; 10 | fastcgi_param SERVER_PROTOCOL $server_protocol; 11 | fastcgi_param GATEWAY_INTERFACE CGI/1.1; 12 | fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; 13 | fastcgi_param REMOTE_ADDR $remote_addr; 14 | fastcgi_param REMOTE_PORT $remote_port; 15 | fastcgi_param SERVER_ADDR $server_addr; 16 | fastcgi_param SERVER_PORT $server_port; 17 | fastcgi_param SERVER_NAME $server_name; 18 | 19 | fastcgi_index index.php; 20 | 21 | fastcgi_param REDIRECT_STATUS 200; -------------------------------------------------------------------------------- /full-example/nginx-structure.conf: -------------------------------------------------------------------------------- 1 | 2 | http{ 3 | server{ 4 | include /var/sites/b.conf; 5 | } 6 | include /var/sites/a.conf; 7 | } 8 | 9 | //vhosts/tenta_com.conf 10 | server{ 11 | listen 90; 12 | server_nme tenta.com; 13 | } 14 | 15 | server { 16 | listen 81; 17 | server_name tenta.com; 18 | } 19 | 20 | 21 | //vhosts/tenta_io.conf 22 | server{ 23 | listen 80; 24 | server_name tenta.io; 25 | } 26 | 27 | 28 | class statement { 29 | filepath 30 | []statements 31 | func virtual saveToFile(){ 32 | 33 | } 34 | } 35 | 36 | config : statement{ 37 | savetofile(){ 38 | 39 | include.getIncludeStatemtn() // include /var/sites/*.conf 40 | include.savetoFile() 41 | 42 | } 43 | } 44 | 45 | server : statement{ 46 | 47 | } 48 | 49 | include : statement{ 50 | 51 | } 52 | 53 | 54 | ben seni duymuyorum 55 | 56 | ben seni duyuyorum :) bi settinglere bakim 57 | 58 | sesin acik mi? 59 | 60 | ben seni net duyuyorum 61 | 62 | neuyse :) gorusuruz -------------------------------------------------------------------------------- /examples/parse-nginx-conf-get-listen-port/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tufanbarisyildirim/gonginx/parser" 7 | ) 8 | 9 | func parseConfigAndGetPorts(filePath string) ([]string, error) { 10 | p, err := parser.NewParser(filePath) 11 | if err != nil { 12 | return nil, fmt.Errorf("failed to create parser: %w", err) 13 | } 14 | conf, err := p.Parse() 15 | if err != nil { 16 | return nil, fmt.Errorf("failed to parse config: %w", err) 17 | } 18 | servers := conf.FindDirectives("server") 19 | ports := make([]string, 0) 20 | for _, server := range servers { 21 | listens := server.GetBlock().FindDirectives("listen") 22 | if len(listens) > 0 { 23 | listenPorts := listens[0].GetParameters() 24 | for _, port := range listenPorts { 25 | ports = append(ports, port.GetValue()) 26 | } 27 | } 28 | } 29 | return ports, nil 30 | } 31 | func main() { 32 | ports, err := parseConfigAndGetPorts("../../testdata/full_conf/nginx.conf") 33 | if err != nil { 34 | panic(err) 35 | } 36 | fmt.Println(ports) 37 | } 38 | -------------------------------------------------------------------------------- /testdata/full_conf/fastcgi_params: -------------------------------------------------------------------------------- 1 | 2 | fastcgi_param QUERY_STRING $query_string; 3 | fastcgi_param REQUEST_METHOD $request_method; 4 | fastcgi_param CONTENT_TYPE $content_type; 5 | fastcgi_param CONTENT_LENGTH $content_length; 6 | 7 | fastcgi_param SCRIPT_NAME $fastcgi_script_name; 8 | fastcgi_param REQUEST_URI $request_uri; 9 | fastcgi_param DOCUMENT_URI $document_uri; 10 | fastcgi_param DOCUMENT_ROOT $document_root; 11 | fastcgi_param SERVER_PROTOCOL $server_protocol; 12 | fastcgi_param REQUEST_SCHEME $scheme; 13 | fastcgi_param HTTPS $https if_not_empty; 14 | 15 | fastcgi_param GATEWAY_INTERFACE CGI/1.1; 16 | fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; 17 | 18 | fastcgi_param REMOTE_ADDR $remote_addr; 19 | fastcgi_param REMOTE_PORT $remote_port; 20 | fastcgi_param SERVER_ADDR $server_addr; 21 | fastcgi_param SERVER_PORT $server_port; 22 | fastcgi_param SERVER_NAME $server_name; 23 | 24 | # PHP only, required if PHP was built with --enable-force-cgi-redirect 25 | fastcgi_param REDIRECT_STATUS 200; 26 | -------------------------------------------------------------------------------- /config/include_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | //TODO(tufan): reactivate here after getting SaveToFile() done 4 | //func TestInclude_SaveToFile(t *testing.T) { 5 | // type fields struct { 6 | // IncludePath string 7 | // Config *Config 8 | // } 9 | // tests := []struct { 10 | // name string 11 | // fields fields 12 | // wantErr bool 13 | // }{ 14 | // { 15 | // name: "test saving file", 16 | // fields: fields{ 17 | // IncludePath: "../full-example/unittest/*.conf", 18 | // Config: &Config{ 19 | // FilePath: "../full-example/unittest/included.conf", 20 | // Block: &Block{ 21 | // Directives: []IDirective{}, 22 | // }, 23 | // }, 24 | // }, 25 | // }, 26 | // } 27 | // for _, tt := range tests { 28 | // t.Run(tt.name, func(t *testing.T) { 29 | // i := &Include{ 30 | // IncludePath: tt.fields.IncludePath, 31 | // Configs: []*Config{ 32 | // tt.fields.Config, 33 | // }, 34 | // } 35 | // if err := i.SaveToFile(NoIndentStyle); (err != nil) != tt.wantErr { 36 | // t.Errorf("Include.SaveToFile() error = %v, wantErr %v", err, tt.wantErr) 37 | // } 38 | // }) 39 | // } 40 | //} 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tufan Barış YILDIRIM 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /testdata/full_conf/fastcgi.conf: -------------------------------------------------------------------------------- 1 | 2 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 3 | fastcgi_param QUERY_STRING $query_string; 4 | fastcgi_param REQUEST_METHOD $request_method; 5 | fastcgi_param CONTENT_TYPE $content_type; 6 | fastcgi_param CONTENT_LENGTH $content_length; 7 | 8 | fastcgi_param SCRIPT_NAME $fastcgi_script_name; 9 | fastcgi_param REQUEST_URI $request_uri; 10 | fastcgi_param DOCUMENT_URI $document_uri; 11 | fastcgi_param DOCUMENT_ROOT $document_root; 12 | fastcgi_param SERVER_PROTOCOL $server_protocol; 13 | fastcgi_param REQUEST_SCHEME $scheme; 14 | fastcgi_param HTTPS $https if_not_empty; 15 | 16 | fastcgi_param GATEWAY_INTERFACE CGI/1.1; 17 | fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; 18 | 19 | fastcgi_param REMOTE_ADDR $remote_addr; 20 | fastcgi_param REMOTE_PORT $remote_port; 21 | fastcgi_param SERVER_ADDR $server_addr; 22 | fastcgi_param SERVER_PORT $server_port; 23 | fastcgi_param SERVER_NAME $server_name; 24 | 25 | # PHP only, required if PHP was built with --enable-force-cgi-redirect 26 | fastcgi_param REDIRECT_STATUS 200; 27 | -------------------------------------------------------------------------------- /dumper/location_test.go: -------------------------------------------------------------------------------- 1 | package dumper 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tufanbarisyildirim/gonginx/config" 7 | ) 8 | 9 | func TestLocation_ToString(t *testing.T) { 10 | t.Parallel() 11 | type fields struct { 12 | Directive *config.Directive 13 | Modifier string 14 | Match string 15 | } 16 | tests := []struct { 17 | name string 18 | fields fields 19 | want string 20 | }{ 21 | { 22 | name: "empty bloc and one match location empty block", 23 | fields: fields{ 24 | Directive: &config.Directive{ 25 | Name: "location", 26 | Parameters: []config.Parameter{{Value: "/admin"}}, 27 | Block: &config.Block{ 28 | Directives: make([]config.IDirective, 0), 29 | }, 30 | }, 31 | Modifier: "", 32 | Match: "/admin", 33 | }, 34 | want: "location /admin {\n\n}", 35 | }, 36 | } 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | l := &config.Location{ 40 | Directive: tt.fields.Directive, 41 | Modifier: tt.fields.Modifier, 42 | Match: tt.fields.Match, 43 | } 44 | if got := DumpDirective(l, NoIndentStyle); got != tt.want { 45 | t.Errorf("Location.ToString() = %v, want %v", got, tt.want) 46 | } 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/update-server-listen-port/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/tufanbarisyildirim/gonginx/config" 8 | "github.com/tufanbarisyildirim/gonginx/dumper" 9 | "github.com/tufanbarisyildirim/gonginx/parser" 10 | ) 11 | 12 | func updateServerListenPort(filePath string, oldPort string, newPort string) (string, error) { 13 | p, err := parser.NewParser(filePath) 14 | if err != nil { 15 | return "", fmt.Errorf("failed to create parser: %w", err) 16 | } 17 | conf, err := p.Parse() 18 | if err != nil { 19 | return "", fmt.Errorf("failed to parse config: %w", err) 20 | } 21 | 22 | servers := conf.FindDirectives("server") 23 | for _, server := range servers { 24 | listens := server.GetBlock().FindDirectives("listen") 25 | for _, listen := range listens { 26 | if listen.GetParameters()[0].GetValue() == oldPort { 27 | listenDirective := listen.(*config.Directive) 28 | listenDirective.Parameters[0].SetValue(newPort) 29 | } 30 | } 31 | } 32 | changedConf := dumper.DumpConfig(conf, dumper.IndentedStyle) 33 | return changedConf, nil 34 | } 35 | func main() { 36 | 37 | filePath := "../../testdata/full_conf/nginx.conf" 38 | oldPort := "80" 39 | newPort := "8080" 40 | if changedConf, err := updateServerListenPort(filePath, oldPort, newPort); err != nil { 41 | log.Fatalf("Error updating server listen port: %v", err) 42 | } else { 43 | fmt.Println(changedConf) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /full-example/nginx.conf: -------------------------------------------------------------------------------- 1 | user www www; 2 | worker_processes 5; 3 | error_log logs/error.log; 4 | pid logs/nginx.pid; 5 | worker_rlimit_nofile 8192; 6 | events { worker_connections 4096; } http { 7 | include mime.types; 8 | include proxy.conf; 9 | include fastcgi.conf; 10 | index index.html index.htm index.php; 11 | default_type application/octet-stream; 12 | log_format main '$remote_addr - $remote_user [$time_local] $status "$request" $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"'; 13 | access_log logs/access.log main; 14 | sendfile on; 15 | tcp_nopush on; 16 | server_names_hash_bucket_size 128; 17 | server { 18 | listen 80; 19 | server_name domain1.com www.domain1.com; 20 | access_log logs/domain1.access.log main; 21 | root html; 22 | location ~ \.php$ { 23 | fastcgi_pass 127.0.0.1:1025; } } server { 24 | listen 80; 25 | server_name domain2.com www.domain2.com; 26 | access_log logs/domain2.access.log main; 27 | location ~ ^/(images|javascript|js|css|flash|media|static)/ { 28 | root /var/www/virtual/big.server.com/htdocs; 29 | expires 30d; 30 | } location / { proxy_pass http://127.0.0.1:8080; } } 31 | upstream big_server_com { 32 | server 127.0.0.3:8000 weight=5; 33 | server 127.0.0.3:8001 weight=5; 34 | server 192.168.0.1:8000; 35 | server 192.168.0.1:8001; 36 | } server { listen 80; 37 | server_name big.server.com; 38 | access_log logs/big.server.access.log main; 39 | location / { proxy_pass http://big_server_com; } } } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for your interest in contributing to Gonginx! 4 | 5 | ## Code of Conduct 6 | 7 | Help us keep Gonginx open and inclusive. Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md). 8 | 9 | ## Getting Started 10 | 11 | * submit a ticket for your issue, assuming one does not already exist 12 | * clearly describe the issue including steps to reproduce when it is a bug 13 | * identify specific versions of the binaries and client libraries 14 | * fork the repository on GitHub 15 | 16 | ## Making Changes 17 | 18 | * create a branch from where you want to base your work 19 | * we typically name branches according to the following format: `fix/` 20 | * make commits of logical units 21 | * make sure your commit messages are in a clear and readable format, example: 22 | 23 | ``` 24 | fixed bug in http context 25 | 26 | * fixed parsing locations 27 | * cleanup variable replacing 28 | * ... 29 | ``` 30 | 31 | * if you're fixing a bug or adding functionality you have to write a test that fails without your fix 32 | * make sure to run `make test` in the root of the repo to ensure that your code is 33 | properly formatted and that tests pass. 34 | * test must prove the feature presents and fail when you revert your changes 35 | 36 | ## Submitting Changes 37 | 38 | * push your changes to your branch in your fork of the repository 39 | * submit a pull request against gongix' repository -------------------------------------------------------------------------------- /config/block.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Block represents a block statement. 4 | type Block struct { 5 | Directives []IDirective 6 | IsLuaBlock bool 7 | LiteralCode string 8 | Parent IDirective 9 | } 10 | 11 | // SetParent sets the parent directive. 12 | func (b *Block) SetParent(parent IDirective) { 13 | b.Parent = parent 14 | } 15 | 16 | // GetParent returns the parent directive. 17 | func (b *Block) GetParent() IDirective { 18 | return b.Parent 19 | } 20 | 21 | // GetDirectives returns all directives in this block. 22 | func (b *Block) GetDirectives() []IDirective { 23 | return b.Directives 24 | } 25 | 26 | // GetCodeBlock returns the literal code block. 27 | func (b *Block) GetCodeBlock() string { 28 | return b.LiteralCode 29 | } 30 | 31 | // FindDirectives finds directives in the block recursively. 32 | func (b *Block) FindDirectives(directiveName string) []IDirective { 33 | directives := make([]IDirective, 0) 34 | for _, directive := range b.GetDirectives() { 35 | if directive.GetName() == directiveName { 36 | directives = append(directives, directive) 37 | } 38 | if include, ok := directive.(*Include); ok { 39 | for _, c := range include.Configs { 40 | directives = append(directives, c.FindDirectives(directiveName)...) 41 | } 42 | } 43 | if directive.GetBlock() != nil { 44 | directives = append(directives, directive.GetBlock().FindDirectives(directiveName)...) 45 | } 46 | } 47 | 48 | return directives 49 | } 50 | -------------------------------------------------------------------------------- /full-example/formatting/raw.conf: -------------------------------------------------------------------------------- 1 | user www www; 2 | worker_processes 5; 3 | error_log logs/error.log; 4 | pid logs/nginx.pid; 5 | worker_rlimit_nofile 8192; 6 | events { worker_connections 4096; } http { 7 | include mime.types; 8 | include proxy.conf; 9 | include fastcgi.conf; 10 | index index.html index.htm index.php; 11 | default_type application/octet-stream; 12 | log_format main '$remote_addr - $remote_user [$time_local] $status ' 13 | '"$request" $body_bytes_sent "$http_referer" ' 14 | ' "$http_user_agent" "$http_x_forwarded_for"'; 15 | access_log logs/access.log main; 16 | sendfile on; 17 | tcp_nopush on; 18 | server_names_hash_bucket_size 128; 19 | server { 20 | listen 80; 21 | server_name domain1.com www.domain1.com; 22 | access_log logs/domain1.access.log main; 23 | root html; 24 | location ~ \.php$ { 25 | fastcgi_pass 127.0.0.1:1025; } } server { 26 | listen 80; 27 | server_name domain2.com www.domain2.com; 28 | access_log logs/domain2.access.log main; 29 | location ~ ^/(images|javascript|js|css|flash|media|static)/ { 30 | root /var/www/virtual/big.server.com/htdocs; 31 | expires 30d; 32 | } location / { proxy_pass http://127.0.0.1:8080; } } 33 | upstream big_server_com { 34 | server 127.0.0.3:8000 weight=5; 35 | server 127.0.0.3:8001 weight=5; 36 | server 192.168.0.1:8000; 37 | server 192.168.0.1:8001; 38 | } server { listen 80; 39 | server_name big.server.com; 40 | access_log logs/big.server.access.log main; 41 | location / { proxy_pass http://big_server_com; } } } -------------------------------------------------------------------------------- /config/directive.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Directive represents any nginx directive. 4 | type Directive struct { 5 | Block IBlock 6 | Name string 7 | Parameters []Parameter //TODO: Save parameters with their type 8 | Comment []string 9 | DefaultInlineComment 10 | Parent IDirective 11 | Line int 12 | } 13 | 14 | // SetLine sets the line number. 15 | func (d *Directive) SetLine(line int) { 16 | d.Line = line 17 | } 18 | 19 | // GetLine returns the line number. 20 | func (d *Directive) GetLine() int { 21 | return d.Line 22 | } 23 | 24 | // SetParent sets the parent directive. 25 | func (d *Directive) SetParent(parent IDirective) { 26 | d.Parent = parent 27 | } 28 | 29 | // GetParent returns the parent directive. 30 | func (d *Directive) GetParent() IDirective { 31 | return d.Parent 32 | } 33 | 34 | // SetComment sets the directive comment. 35 | func (d *Directive) SetComment(comment []string) { 36 | d.Comment = comment 37 | } 38 | 39 | // GetName returns the directive name. 40 | func (d *Directive) GetName() string { 41 | return d.Name 42 | } 43 | 44 | // GetParameters returns all parameters of the directive. 45 | func (d *Directive) GetParameters() []Parameter { 46 | return d.Parameters 47 | } 48 | 49 | // GetBlock returns the directive block if it exists. 50 | func (d *Directive) GetBlock() IBlock { 51 | return d.Block 52 | } 53 | 54 | // GetComment returns the directive comment. 55 | func (d *Directive) GetComment() []string { 56 | return d.Comment 57 | } 58 | -------------------------------------------------------------------------------- /testdata/issues/17.conf: -------------------------------------------------------------------------------- 1 | location / { 2 | set $serve_URL $fullurl${uri}index.html; 3 | try_files $serve_URL $uri $uri/ /index.php$is_args$args; 4 | 5 | } 6 | 7 | location ~* ^/xmlrpc.php$ { return 403; } # deny access to xmlrpc.php - https://kinsta.com/blog/xmlrpc-php/ 8 | 9 | location ~ \.php$ { 10 | 11 | include snippets/fastcgi-php.conf; 12 | fastcgi_param PHP_VALUE "open_basedir=$document_root:/tmp;\nerror_log=/public_html/logs/demo1-php_errors.log;"; 13 | fastcgi_pass unix:/run/php/php7.4-fpm.sock; 14 | 15 | } 16 | 17 | location /wp-content/uploads/ { 18 | 19 | location ~ .(aspx|php|jsp|cgi)$ { return 410; } 20 | #location ~ \.pdf$ { rewrite .* /custom/pdf_auth.php; } 21 | 22 | } 23 | 24 | location ~* \.(css|gif|ico|jpeg|jpg|js|png|woff|woff2|ttf|ttc|otf|eot)$ { 25 | 26 | expires 30d; # https://nginx.org/en/docs/http/ngx_http_headers_module.html 27 | log_not_found off; # https://nginx.org/en/docs/http/ngx_http_core_module.html#log_not_found 28 | 29 | } 30 | 31 | location ~ /\.ht { deny all; } # deny access to .htaccess files 32 | 33 | location ~ ^/(status)$ { 34 | # https://www.tecmint.com/enable-monitor-php-fpm-status-in-nginx/ 35 | allow 127.0.0.1; 36 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 37 | fastcgi_index index.php; 38 | include fastcgi_params; 39 | fastcgi_pass unix:/run/php/php7.4-fpm.sock; 40 | } 41 | 42 | location /.well-known/acme-challenge { 43 | alias /public_html/certbot_temp/.well-known/acme-challenge; 44 | } -------------------------------------------------------------------------------- /full-example/formatting/issue17-raw.conf: -------------------------------------------------------------------------------- 1 | location / { 2 | set $serve_URL $fullurl${uri}index.html; 3 | try_files $serve_URL $uri $uri/ /index.php$is_args$args; 4 | 5 | } 6 | 7 | location ~* ^/xmlrpc.php$ { return 403; } # deny access to xmlrpc.php - https://kinsta.com/blog/xmlrpc-php/ 8 | 9 | location ~ \.php$ { 10 | 11 | include snippets/fastcgi-php.conf; 12 | fastcgi_param PHP_VALUE "open_basedir=$document_root:/tmp;\nerror_log=/public_html/logs/demo1-php_errors.log;"; 13 | fastcgi_pass unix:/run/php/php7.4-fpm.sock; 14 | 15 | } 16 | 17 | location /wp-content/uploads/ { 18 | 19 | location ~ .(aspx|php|jsp|cgi)$ { return 410; } 20 | #location ~ \.pdf$ { rewrite .* /custom/pdf_auth.php; } 21 | 22 | } 23 | 24 | location ~* \.(css|gif|ico|jpeg|jpg|js|png|woff|woff2|ttf|ttc|otf|eot)$ { 25 | 26 | expires 30d; # https://nginx.org/en/docs/http/ngx_http_headers_module.html 27 | log_not_found off; # https://nginx.org/en/docs/http/ngx_http_core_module.html#log_not_found 28 | 29 | } 30 | 31 | location ~ /\.ht { deny all; } # deny access to .htaccess files 32 | 33 | location ~ ^/(status)$ { 34 | # https://www.tecmint.com/enable-monitor-php-fpm-status-in-nginx/ 35 | allow 127.0.0.1; 36 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 37 | fastcgi_index index.php; 38 | include fastcgi_params; 39 | fastcgi_pass unix:/run/php/php7.4-fpm.sock; 40 | } 41 | 42 | location /.well-known/acme-challenge { 43 | alias /public_html/certbot_temp/.well-known/acme-challenge; 44 | } -------------------------------------------------------------------------------- /full-example/formatting/issue17-formatted.conf: -------------------------------------------------------------------------------- 1 | location / { 2 | set $serve_URL $fullurl${uri}index.html; 3 | try_files $serve_URL $uri $uri/ /index.php$is_args$args; 4 | } 5 | # deny access to xmlrpc.php - https://kinsta.com/blog/xmlrpc-php/ 6 | location ~* ^/xmlrpc.php$ { 7 | return 403; 8 | } 9 | location ~ \.php$ { 10 | include snippets/fastcgi-php.conf; 11 | fastcgi_param PHP_VALUE "open_basedir=$document_root:/tmp;\nerror_log=/public_html/logs/demo1-php_errors.log;"; 12 | fastcgi_pass unix:/run/php/php7.4-fpm.sock; 13 | } 14 | location /wp-content/uploads/ { 15 | location ~ .(aspx|php|jsp|cgi)$ { 16 | return 410; 17 | } 18 | } 19 | #location ~ \.pdf$ { rewrite .* /custom/pdf_auth.php; } 20 | location ~* \.(css|gif|ico|jpeg|jpg|js|png|woff|woff2|ttf|ttc|otf|eot)$ { 21 | # https://nginx.org/en/docs/http/ngx_http_headers_module.html 22 | expires 30d; 23 | # https://nginx.org/en/docs/http/ngx_http_core_module.html#log_not_found 24 | log_not_found off; 25 | } 26 | # deny access to .htaccess files 27 | location ~ /\.ht { 28 | deny all; 29 | } 30 | location ~ ^/(status)$ { 31 | # https://www.tecmint.com/enable-monitor-php-fpm-status-in-nginx/ 32 | allow 127.0.0.1; 33 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 34 | fastcgi_index index.php; 35 | include fastcgi_params; 36 | fastcgi_pass unix:/run/php/php7.4-fpm.sock; 37 | } 38 | location /.well-known/acme-challenge { 39 | alias /public_html/certbot_temp/.well-known/acme-challenge; 40 | } 41 | -------------------------------------------------------------------------------- /dumper/lua.go: -------------------------------------------------------------------------------- 1 | package dumper 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "runtime" 7 | "strings" 8 | 9 | "github.com/imega/luaformatter/formatter" 10 | "github.com/tufanbarisyildirim/gonginx/config" 11 | ) 12 | 13 | // DumpLuaBlock convert a lua block to a string 14 | func DumpLuaBlock(b config.IBlock, style *Style) (luaCode string) { 15 | luaCode = b.GetCodeBlock() 16 | 17 | defer func() { 18 | // luaformatter may panic if the lua code is not valid 19 | if r := recover(); r != nil { 20 | buf := make([]byte, 1024) 21 | runtime.Stack(buf, false) 22 | log.Printf("%s\n%s", r, buf) 23 | } 24 | }() 25 | var buf bytes.Buffer 26 | 27 | if luaCode == "" { 28 | return "" 29 | } 30 | 31 | config := formatter.DefaultConfig() 32 | config.IndentSize = uint8(style.Indent / 4) 33 | config.Highlight = false 34 | 35 | // Replace # comments with -- comments temporarily 36 | luaCode = strings.ReplaceAll(luaCode, "#", "--") 37 | 38 | formatter.Format(config, []byte(luaCode), &buf) 39 | 40 | formatted := buf.String() 41 | 42 | // Add indentation to each line 43 | lines := bytes.Split([]byte(formatted), []byte("\n")) 44 | indentation := bytes.Repeat([]byte(" "), style.StartIndent) 45 | 46 | var indentedBuf bytes.Buffer 47 | for i, line := range lines { 48 | if len(line) > 0 { 49 | indentedBuf.Write(indentation) 50 | indentedBuf.Write(line) 51 | } 52 | if i < len(lines)-1 { 53 | indentedBuf.WriteByte('\n') 54 | } 55 | } 56 | 57 | formatted = indentedBuf.String() 58 | 59 | // Restore # comments 60 | formatted = strings.ReplaceAll(formatted, "--", "#") 61 | 62 | return strings.TrimRight(formatted, "\n") 63 | } 64 | -------------------------------------------------------------------------------- /dumper/config_test.go: -------------------------------------------------------------------------------- 1 | package dumper 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tufanbarisyildirim/gonginx/config" 7 | ) 8 | 9 | func TestConfig_ToString(t *testing.T) { 10 | t.Parallel() 11 | type fields struct { 12 | Block *config.Block 13 | FilePath string 14 | } 15 | tests := []struct { 16 | name string 17 | fields fields 18 | want string 19 | }{ 20 | { 21 | name: "block", 22 | fields: fields{ 23 | Block: &config.Block{ 24 | Directives: []config.IDirective{ 25 | &config.Directive{ 26 | Name: "user", 27 | Parameters: []config.Parameter{ 28 | {Value: "nginx"}, 29 | {Value: "nginx"}, 30 | }, 31 | }, 32 | &config.Directive{ 33 | Name: "worker_processes", 34 | Parameters: []config.Parameter{{Value: "5"}}, 35 | }, 36 | &config.Include{ 37 | Directive: &config.Directive{ 38 | Name: "include", 39 | Parameters: []config.Parameter{{Value: "/etc/nginx/conf/*.conf"}}, 40 | }, 41 | IncludePath: "/etc/nginx/conf/*.conf", 42 | }, 43 | }, 44 | }, 45 | }, 46 | want: "user nginx nginx;\nworker_processes 5;\ninclude /etc/nginx/conf/*.conf;", 47 | }, 48 | } 49 | for _, tt := range tests { 50 | t.Run(tt.name, func(t *testing.T) { 51 | c := &config.Config{ 52 | Block: tt.fields.Block, 53 | FilePath: tt.fields.FilePath, 54 | } 55 | //TODO(tufan): create another dumper for a config and include statement (file thingis) 56 | if got := DumpConfig(c, NoIndentStyle); got != tt.want { 57 | t.Errorf("ToString() = %v, want %v", got, tt.want) 58 | } 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /dumper/server_test.go: -------------------------------------------------------------------------------- 1 | package dumper 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tufanbarisyildirim/gonginx/config" 7 | ) 8 | 9 | func TestServer_ToString(t *testing.T) { 10 | t.Parallel() 11 | type fields struct { 12 | Directive *config.Directive 13 | } 14 | tests := []struct { 15 | name string 16 | fields fields 17 | args *Style 18 | want string 19 | }{ 20 | { 21 | name: "empty server block", 22 | fields: fields{ 23 | Directive: &config.Directive{ 24 | Block: &config.Block{ 25 | Directives: make([]config.IDirective, 0), 26 | }, 27 | Name: "server", 28 | }, 29 | }, 30 | args: NoIndentStyle, 31 | want: "server {\n\n}", 32 | }, 33 | { 34 | name: "styled server block with some directives", 35 | fields: fields{ 36 | Directive: &config.Directive{ 37 | Block: &config.Block{ 38 | Directives: []config.IDirective{ 39 | &config.Directive{ 40 | Name: "server_name", 41 | Parameters: []config.Parameter{{Value: "gonginx.dev"}}, 42 | }, 43 | &config.Directive{ 44 | Name: "root", 45 | Parameters: []config.Parameter{{Value: "/var/sites/gonginx"}}, 46 | }, 47 | }, 48 | }, 49 | Name: "server", 50 | }, 51 | }, 52 | args: NewStyle(), 53 | want: `server { 54 | server_name gonginx.dev; 55 | root /var/sites/gonginx; 56 | }`, 57 | }, 58 | } 59 | for _, tt := range tests { 60 | t.Run(tt.name, func(t *testing.T) { 61 | s, err := config.NewServer(tt.fields.Directive) 62 | if err != nil { 63 | t.Error("NewServer(tt.fields.Directive) failed") 64 | } 65 | 66 | if got := DumpDirective(s, tt.args); got != tt.want { 67 | t.Errorf("Server.ToString() = \"%v\", want \"%v\"", got, tt.want) 68 | } 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/add-custom-directive/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/tufanbarisyildirim/gonginx/config" 8 | "github.com/tufanbarisyildirim/gonginx/dumper" 9 | "github.com/tufanbarisyildirim/gonginx/parser" 10 | ) 11 | 12 | func addCustomDirective(fullConf string, blockName string, directiveName string, directiveValue string) (string, error) { 13 | p := parser.NewStringParser(fullConf) 14 | conf, err := p.Parse() 15 | if err != nil { 16 | return "", fmt.Errorf("failed to parse config: %w", err) 17 | } 18 | 19 | blocks := conf.FindDirectives(blockName) 20 | if len(blocks) == 0 { 21 | return "", fmt.Errorf("no such block: %s", blockName) 22 | } 23 | 24 | block := blocks[0].GetBlock() 25 | newDirective := &config.Directive{ 26 | Name: directiveName, 27 | Parameters: []config.Parameter{{Value: directiveValue}}, 28 | } 29 | realBlock := block.(*config.Block) 30 | realBlock.Directives = append(realBlock.Directives, newDirective) 31 | 32 | return dumper.DumpConfig(conf, dumper.IndentedStyle), nil 33 | } 34 | 35 | func main() { 36 | fullConf := `http{ 37 | upstream my_backend{ 38 | server 127.0.0.1:443; 39 | server 127.0.0.2:443 backup; 40 | } 41 | server { 42 | listen 8080; 43 | location / { 44 | root /var/www/html; 45 | index index.html; 46 | } 47 | } 48 | 49 | server { 50 | listen 9090; 51 | location / { 52 | root /var/www/html; 53 | index index.html; 54 | } 55 | } 56 | }` 57 | 58 | blockName := "server" 59 | directiveName := "access_log" 60 | directiveValue := "/var/log/nginx/access.log" 61 | newFullConf, err := addCustomDirective(fullConf, blockName, directiveName, directiveValue) 62 | if err != nil { 63 | log.Fatalf("Error adding custom directive: %v", err) 64 | } 65 | fmt.Println("New Full Config:", newFullConf) 66 | } 67 | -------------------------------------------------------------------------------- /full-example/formatting/formatted.conf: -------------------------------------------------------------------------------- 1 | error_log logs/error.log; 2 | 3 | events { 4 | worker_connections 4096; 5 | } 6 | 7 | http { 8 | access_log logs/access.log main; 9 | default_type application/octet-stream; 10 | include mime.types; 11 | include proxy.conf; 12 | include fastcgi.conf; 13 | index index.html index.htm index.php; 14 | log_format main '$remote_addr - $remote_user [$time_local] $status ' '"$request" $body_bytes_sent "$http_referer" ' ' "$http_user_agent" "$http_x_forwarded_for"'; 15 | sendfile on; 16 | 17 | server { 18 | access_log logs/domain1.access.log main; 19 | listen 80; 20 | 21 | location ~ \.php$ { 22 | fastcgi_pass 127.0.0.1:1025; 23 | } 24 | root html; 25 | server_name domain1.com www.domain1.com; 26 | } 27 | 28 | server { 29 | access_log logs/domain2.access.log main; 30 | listen 80; 31 | 32 | location ~ ^/(images|javascript|js|css|flash|media|static)/ { 33 | expires 30d; 34 | root /var/www/virtual/big.server.com/htdocs; 35 | } 36 | 37 | location / { 38 | proxy_pass http://127.0.0.1:8080; 39 | } 40 | server_name domain2.com www.domain2.com; 41 | } 42 | 43 | server { 44 | access_log logs/big.server.access.log main; 45 | listen 80; 46 | 47 | location / { 48 | proxy_pass http://big_server_com; 49 | } 50 | server_name big.server.com; 51 | } 52 | server_names_hash_bucket_size 128; 53 | tcp_nopush on; 54 | 55 | upstream big_server_com { 56 | server 127.0.0.3:8000 weight=5; 57 | server 127.0.0.3:8001 weight=5; 58 | server 192.168.0.1:8000; 59 | server 192.168.0.1:8001; 60 | } 61 | } 62 | pid logs/nginx.pid; 63 | user www www; 64 | worker_processes 5; 65 | worker_rlimit_nofile 8192; -------------------------------------------------------------------------------- /dumper/http_test.go: -------------------------------------------------------------------------------- 1 | package dumper 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/tufanbarisyildirim/gonginx/config" 8 | ) 9 | 10 | func TestHttp_ToString(t *testing.T) { 11 | t.Parallel() 12 | type fields struct { 13 | Directive *config.Directive 14 | } 15 | tests := []struct { 16 | name string 17 | fields fields 18 | args *Style 19 | want string 20 | }{ 21 | { 22 | name: "empty http block", 23 | fields: fields{ 24 | Directive: &config.Directive{ 25 | Block: &config.Block{ 26 | Directives: make([]config.IDirective, 0), 27 | }, 28 | Name: "http", 29 | }, 30 | }, 31 | args: NoIndentStyle, 32 | want: "http {\n\n}", 33 | }, 34 | { 35 | name: "styled http block with some directives", 36 | fields: fields{ 37 | Directive: &config.Directive{ 38 | Block: &config.Block{ 39 | Directives: []config.IDirective{ 40 | &config.Directive{ 41 | Name: "access_log", 42 | Parameters: []config.Parameter{{Value: "logs/access.log"}, {Value: "main"}}, 43 | }, 44 | &config.Directive{ 45 | Name: "default_type", 46 | Parameters: []config.Parameter{{Value: "application/octet-stream"}}, 47 | }, 48 | }, 49 | }, 50 | Name: "http", 51 | }, 52 | }, 53 | args: NewStyle(), 54 | want: `http { 55 | access_log logs/access.log main; 56 | default_type application/octet-stream; 57 | }`, 58 | }, 59 | } 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | s, err := config.NewHTTP(tt.fields.Directive) 63 | if err != nil { 64 | t.Error("NewHTTP(tt.fields.Directive) failed") 65 | } 66 | if got := DumpDirective(s, tt.args); got != tt.want { 67 | t.Errorf("HTTP.ToString() = \"%v\", want \"%v\"", strings.ReplaceAll(got, " ", "."), strings.ReplaceAll(tt.want, " ", ".")) 68 | } 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. 6 | 7 | Examples of unacceptable behavior by participants include: 8 | 9 | * The use of sexualized language or imagery 10 | * Personal attacks 11 | * Trolling or insulting/derogatory comments 12 | * Public or private harassment 13 | * Publishing other's private information, such as physical or electronic addresses, without explicit permission 14 | * Other unethical or unprofessional conduct 15 | 16 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. 17 | 18 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 19 | 20 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 21 | 22 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.2.0, available at https://www.contributor-covenant.org/version/1/2/0/code-of-conduct.html -------------------------------------------------------------------------------- /config/location.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "errors" 4 | 5 | // Location represents a location block in an nginx configuration. 6 | type Location struct { 7 | *Directive 8 | Modifier string 9 | Match string 10 | Parent IDirective 11 | Line int 12 | } 13 | 14 | // SetLine sets the line number. 15 | func (l *Location) SetLine(line int) { 16 | l.Line = line 17 | } 18 | 19 | // GetLine returns the line number. 20 | func (l *Location) GetLine() int { 21 | return l.Line 22 | } 23 | 24 | // SetParent sets the parent directive. 25 | func (l *Location) SetParent(parent IDirective) { 26 | l.Parent = parent 27 | } 28 | 29 | // GetParent returns the parent directive. 30 | func (l *Location) GetParent() IDirective { 31 | return l.Parent 32 | } 33 | 34 | // NewLocation initializes a Location from a directive. 35 | func NewLocation(directive IDirective) (*Location, error) { 36 | dir, ok := directive.(*Directive) 37 | if !ok { 38 | return nil, errors.New("no ") 39 | } 40 | 41 | if len(dir.Parameters) == 0 { 42 | return nil, errors.New("no enough parameter for location") 43 | } 44 | location := &Location{ 45 | Modifier: "", 46 | Match: "", 47 | Directive: dir, 48 | } 49 | if len(dir.Parameters) == 1 { 50 | location.Match = dir.Parameters[0].GetValue() 51 | return location, nil 52 | } else if len(dir.Parameters) == 2 { 53 | location.Modifier = dir.Parameters[0].GetValue() 54 | location.Match = dir.Parameters[1].GetValue() 55 | return location, nil 56 | } 57 | return nil, errors.New("too many arguments for location directive") 58 | } 59 | 60 | // FindDirectives finds directives by name. 61 | func (l *Location) FindDirectives(directiveName string) []IDirective { 62 | block := l.GetBlock() 63 | if block == nil { 64 | return []IDirective{} 65 | } 66 | return block.FindDirectives(directiveName) 67 | } 68 | 69 | // GetDirectives returns all directives in the location block. 70 | func (l *Location) GetDirectives() []IDirective { 71 | block := l.GetBlock() 72 | if block == nil { 73 | return []IDirective{} 74 | } 75 | return block.GetDirectives() 76 | } 77 | -------------------------------------------------------------------------------- /examples/update-directive/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tufanbarisyildirim/gonginx/dumper" 7 | "github.com/tufanbarisyildirim/gonginx/parser" 8 | ) 9 | 10 | func main() { 11 | p := parser.NewStringParser(` 12 | user nginx; 13 | worker_processes auto; 14 | error_log /var/log/nginx/error.log; 15 | pid /run/nginx.pid; 16 | 17 | # Load dynamic modules. See /usr/share/doc/nginx/README.dynamic. 18 | include /usr/share/nginx/modules/*.conf; 19 | 20 | events { 21 | worker_connections 1024; 22 | } 23 | 24 | http { 25 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 26 | '$status $body_bytes_sent "$http_referer" ' 27 | '"$http_user_agent" "$http_x_forwarded_for"'; 28 | 29 | access_log /var/log/nginx/access.log main; 30 | 31 | sendfile on; 32 | tcp_nopush on; 33 | tcp_nodelay on; 34 | keepalive_timeout 65; 35 | types_hash_max_size 2048; 36 | 37 | include /etc/nginx/mime.types; 38 | default_type application/octet-stream; 39 | 40 | server { 41 | listen 80 default_server; 42 | listen [::]:80 default_server; 43 | server_name _; 44 | root /usr/share/nginx/html; 45 | 46 | # Load configuration files for the default server block. 47 | include /etc/nginx/default.d/*.conf; 48 | 49 | location / { 50 | proxy_pass http://www.google.com/; 51 | } 52 | 53 | error_page 404 /404.html; 54 | location = /40x.html { 55 | } 56 | 57 | error_page 500 502 503 504 /50x.html; 58 | location = /50x.html { 59 | } 60 | } 61 | 62 | }`) 63 | 64 | c, err := p.Parse() 65 | if err != nil { 66 | panic(err) 67 | } 68 | directives := c.FindDirectives("proxy_pass") 69 | for _, directive := range directives { 70 | fmt.Println("found a proxy_pass : ", directive.GetName(), directive.GetParameters()) 71 | if directive.GetParameters()[0].GetValue() == "http://www.google.com/" { 72 | directive.GetParameters()[0].SetValue("http://www.duckduckgo.com/") 73 | } 74 | } 75 | 76 | fmt.Println(dumper.DumpBlock(c.Block, dumper.IndentedStyle)) 77 | 78 | } 79 | -------------------------------------------------------------------------------- /examples/formatting/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tufanbarisyildirim/gonginx/dumper" 7 | "github.com/tufanbarisyildirim/gonginx/parser" 8 | ) 9 | 10 | func main() { 11 | p := parser.NewStringParser(`user www www; 12 | worker_processes 5; 13 | error_log logs/error.log; 14 | pid logs/nginx.pid; 15 | worker_rlimit_nofile 8192; 16 | events { worker_connections 4096; } 17 | # http comment 18 | http { 19 | # include comment 20 | include mime.types; 21 | include proxy.conf; 22 | include fastcgi.conf; 23 | index index.html index.htm index.php; 24 | default_type application/octet-stream; 25 | log_format main '$remote_addr - $remote_user [$time_local] $status ' 26 | '"$request" $body_bytes_sent "$http_referer" ' 27 | ' "$http_user_agent" "$http_x_forwarded_for"'; 28 | access_log logs/access.log main; 29 | sendfile on; 30 | tcp_nopush on; 31 | server_names_hash_bucket_size 128; 32 | server { 33 | listen 80; 34 | server_name domain1.com www.domain1.com; 35 | access_log logs/domain1.access.log main; 36 | root html; 37 | # comment: location 38 | location ~ \.php$ { 39 | fastcgi_pass 127.0.0.1:1025; } } 40 | map $http_upgrade $connection_upgrade { 41 | default upgrade; 42 | '' close; 43 | test ''; 44 | } 45 | # comment: server 46 | server { 47 | listen 80; 48 | server_name domain2.com www.domain2.com; 49 | access_log logs/domain2.access.log main; 50 | location ~ ^/(images|javascript|js|css|flash|media|static)/ { 51 | root /var/www/virtual/big.server.com/htdocs; 52 | expires 30d; # inline comment: expires 30d 53 | } location / { proxy_pass http://127.0.0.1:8080; } } 54 | # comment: big_server_com 55 | # comment: upstream big_server_com 56 | upstream big_server_com { 57 | server 127.0.0.3:8000 weight=5; # inline comment: server 127.0.0.3:8000 weight=5 58 | server 127.0.0.3:8001 weight=5; # inline comment: server 127.0.0.3:8000 weight=5 59 | server 192.168.0.1:8000; 60 | server 192.168.0.1:8001; 61 | } 62 | # comment: server 63 | server { # comment: listen 64 | listen 80; # inline comment: listen 80 65 | server_name big.server.com; 66 | # comment: access_log 67 | access_log logs/big.server.access.log main; 68 | location / { proxy_pass http://big_server_com; } } }`) 69 | 70 | c, err := p.Parse() 71 | if err != nil { 72 | panic(err) 73 | } 74 | fmt.Println(dumper.DumpConfig(c, dumper.IndentedStyle)) 75 | 76 | } 77 | -------------------------------------------------------------------------------- /full-example/mime.types: -------------------------------------------------------------------------------- 1 | types { 2 | text/html html htm shtml; 3 | text/css css; 4 | text/xml xml rss; 5 | image/gif gif; 6 | image/jpeg jpeg jpg; 7 | application/x-javascript js; 8 | text/plain txt; 9 | text/x-component htc; 10 | text/mathml mml; 11 | image/png png; 12 | image/x-icon ico; 13 | image/x-jng jng; 14 | image/vnd.wap.wbmp wbmp; 15 | application/java-archive jar war ear; 16 | application/mac-binhex40 hqx; 17 | application/pdf pdf; 18 | application/x-cocoa cco; 19 | application/x-java-archive-diff jardiff; 20 | application/x-java-jnlp-file jnlp; 21 | application/x-makeself run; 22 | application/x-perl pl pm; 23 | application/x-pilot prc pdb; 24 | application/x-rar-compressed rar; 25 | application/x-redhat-package-manager rpm; 26 | application/x-sea sea; 27 | application/x-shockwave-flash swf; 28 | application/x-stuffit sit; 29 | application/x-tcl tcl tk; 30 | application/x-x509-ca-cert der pem crt; 31 | application/x-xpinstall xpi; 32 | application/zip zip; 33 | application/octet-stream deb; 34 | application/octet-stream bin exe dll; 35 | application/octet-stream dmg; 36 | application/octet-stream eot; 37 | application/octet-stream iso img; 38 | application/octet-stream msi msp msm; 39 | audio/mpeg mp3; 40 | audio/x-realaudio ra; 41 | video/mpeg mpeg mpg; 42 | video/quicktime mov; 43 | video/x-flv flv; 44 | video/x-msvideo avi; 45 | video/x-ms-wmv wmv; 46 | video/x-ms-asf asx asf; 47 | video/x-mng mng; 48 | } -------------------------------------------------------------------------------- /dumper/directive_test.go: -------------------------------------------------------------------------------- 1 | package dumper 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tufanbarisyildirim/gonginx/config" 7 | ) 8 | 9 | func TestDirective_ToString(t *testing.T) { 10 | t.Parallel() 11 | type fields struct { 12 | Name string 13 | Parameters []config.Parameter 14 | } 15 | tests := []struct { 16 | name string 17 | fields fields 18 | want string 19 | }{ 20 | { 21 | name: "server_name direction", 22 | fields: fields{ 23 | Name: "server_name", 24 | Parameters: []config.Parameter{ 25 | {Value: "gonginx.dev"}, 26 | {Value: "gonginx.local"}, 27 | {Value: "microspector.com"}, 28 | }, 29 | }, 30 | want: "server_name gonginx.dev gonginx.local microspector.com;", 31 | }, 32 | { 33 | name: "proxy_pass direction", 34 | fields: fields{ 35 | Name: "proxy_pass", 36 | Parameters: []config.Parameter{ 37 | {Value: "http://127.0.0.1/"}, 38 | }, 39 | }, 40 | want: "proxy_pass http://127.0.0.1/;", 41 | }, 42 | { 43 | name: "proxy_set_header direction", 44 | fields: fields{ 45 | Name: "proxy_set_header", 46 | Parameters: []config.Parameter{ 47 | {Value: "Host"}, 48 | {Value: "$host"}, 49 | }, 50 | }, 51 | want: "proxy_set_header Host $host;", 52 | }, 53 | { 54 | name: "proxy_buffers direction", 55 | fields: fields{ 56 | Name: "proxy_buffers", 57 | Parameters: []config.Parameter{ 58 | {Value: "4"}, 59 | {Value: "32k"}, 60 | }, 61 | }, 62 | want: "proxy_buffers 4 32k;", 63 | }, 64 | { 65 | name: "charset direction", 66 | fields: fields{ 67 | Name: "charset", 68 | Parameters: []config.Parameter{ 69 | {Value: "koi8-r"}, 70 | }, 71 | }, 72 | want: "charset koi8-r;", 73 | }, 74 | { 75 | name: "'' close", 76 | fields: fields{ 77 | Name: "''", 78 | Parameters: []config.Parameter{ 79 | {Value: "close"}, 80 | }, 81 | }, 82 | want: "'' close;", 83 | }, 84 | } 85 | for _, tt := range tests { 86 | tt2 := tt 87 | t.Run(tt.name, func(t *testing.T) { 88 | t.Parallel() 89 | d := &config.Directive{ 90 | Name: tt2.fields.Name, 91 | Parameters: tt2.fields.Parameters, 92 | } 93 | if got := DumpDirective(d, NoIndentStyle); got != tt2.want { 94 | t.Errorf("Directive.ToString() = %v, want %v", got, tt2.want) 95 | } 96 | }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # Coding Agent Guidelines for Gonginx 2 | 3 | ## Explanation 4 | 5 | Gonginx is a Go library for parsing, editing, and regenerating nginx 6 | configuration files. The main packages are: 7 | 8 | - **parser** – turns nginx config files into structured objects. 9 | - **config** – models directives and blocks. 10 | - **dumper** – renders configuration objects back into text. 11 | 12 | Examples demonstrating typical usage can be found in the `examples/` 13 | folder and in `GUIDE.md`. 14 | 15 | ## Examples 16 | 17 | Parse a configuration file and print the listen ports of all servers: 18 | 19 | ```go 20 | p, err := parser.NewParser("nginx.conf") 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | conf, err := p.Parse() 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | servers := conf.FindDirectives("server") 29 | for _, srv := range servers { 30 | for _, listen := range srv.GetBlock().FindDirectives("listen") { 31 | fmt.Println(listen.GetParameters()[0].GetValue()) 32 | } 33 | } 34 | ``` 35 | 36 | Add a server to an upstream block and dump the result: 37 | 38 | ```go 39 | p := parser.NewStringParser(`http{ upstream backend{} }`) 40 | conf, _ := p.Parse() 41 | up := conf.FindUpstreams()[0] 42 | up.AddServer(&config.UpstreamServer{Address: "127.0.0.1:443"}) 43 | fmt.Println(dumper.DumpConfig(conf, dumper.IndentedStyle)) 44 | ``` 45 | 46 | More detailed examples, including writing configs to disk, are in 47 | `GUIDE.md`. 48 | 49 | ## Contribution Guide 50 | 51 | 1. Follow the [Code of Conduct](CODE_OF_CONDUCT.md). 52 | 2. Before sending a pull request, run `make test` from the repository 53 | root to format the code and execute all tests. 54 | 3. Commit messages should be clear and contain logical units, e.g. 55 | 56 | ``` 57 | fix parser include handling 58 | 59 | * handle recursive includes 60 | * add regression test 61 | ``` 62 | 63 | 4. If you add new functionality, include tests that fail without your 64 | change and pass with it. 65 | 66 | ## Using the Library 67 | 68 | Install dependencies via Go modules and import packages directly from 69 | `github.com/tufanbarisyildirim/gonginx`: 70 | 71 | ```go 72 | import ( 73 | "github.com/tufanbarisyildirim/gonginx/config" 74 | "github.com/tufanbarisyildirim/gonginx/dumper" 75 | "github.com/tufanbarisyildirim/gonginx/parser" 76 | ) 77 | ``` 78 | 79 | Create a parser with `parser.NewParser` (for files) or 80 | `parser.NewStringParser` (for strings). After modifying the returned 81 | `config.Config` object, convert it back to text with 82 | `dumper.DumpConfig` or persist it using `dumper.WriteConfig`. 83 | 84 | -------------------------------------------------------------------------------- /config/statement.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // IBlock represents any directive block 4 | type IBlock interface { 5 | GetDirectives() []IDirective 6 | FindDirectives(directiveName string) []IDirective 7 | GetCodeBlock() string 8 | SetParent(IDirective) 9 | GetParent() IDirective 10 | } 11 | 12 | // IDirective represents any directive 13 | type IDirective interface { 14 | GetName() string //the directive name. 15 | GetParameters() []Parameter 16 | GetBlock() IBlock 17 | GetComment() []string 18 | SetComment(comment []string) 19 | SetParent(IDirective) 20 | GetParent() IDirective 21 | GetLine() int 22 | SetLine(int) 23 | InlineCommenter 24 | } 25 | 26 | // InlineCommenter represents the inline comment holder 27 | type InlineCommenter interface { 28 | GetInlineComment() []InlineComment 29 | SetInlineComment(comment InlineComment) 30 | } 31 | 32 | // DefaultInlineComment represents the default inline comment holder 33 | type DefaultInlineComment struct { 34 | InlineComment []InlineComment 35 | } 36 | 37 | // GetInlineComment returns the inline comment 38 | func (d *DefaultInlineComment) GetInlineComment() []InlineComment { 39 | return d.InlineComment 40 | } 41 | 42 | // SetInlineComment sets the inline comment 43 | func (d *DefaultInlineComment) SetInlineComment(comment InlineComment) { 44 | d.InlineComment = append(d.InlineComment, comment) 45 | } 46 | 47 | // FileDirective a statement that saves its own file 48 | type FileDirective interface { 49 | isFileDirective() 50 | } 51 | 52 | // IncludeDirective represents include statement in nginx 53 | type IncludeDirective interface { 54 | FileDirective 55 | } 56 | 57 | // Parameter represents a parameter in a directive 58 | type Parameter struct { 59 | Value string 60 | RelativeLineIndex int // relative line index to the directive 61 | } 62 | 63 | // String returns the value of the parameter 64 | func (p *Parameter) String() string { 65 | return p.Value 66 | } 67 | 68 | // SetValue sets the value of the parameter 69 | func (p *Parameter) SetValue(v string) { 70 | p.Value = v 71 | } 72 | 73 | // GetValue returns the value of the parameter 74 | func (p *Parameter) GetValue() string { 75 | return p.Value 76 | } 77 | 78 | // SetRelativeLineIndex sets the relative line index of the parameter 79 | func (p *Parameter) SetRelativeLineIndex(i int) { 80 | p.RelativeLineIndex = i 81 | } 82 | 83 | // GetRelativeLineIndex returns the relative line index of the parameter 84 | func (p *Parameter) GetRelativeLineIndex() int { 85 | return p.RelativeLineIndex 86 | } 87 | 88 | // InlineComment represents an inline comment 89 | type InlineComment Parameter 90 | -------------------------------------------------------------------------------- /dumper/upstream_server_test.go: -------------------------------------------------------------------------------- 1 | package dumper 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/tufanbarisyildirim/gonginx/config" 8 | "gotest.tools/v3/assert" 9 | ) 10 | 11 | func TestNewUpstreamServer(t *testing.T) { 12 | t.Parallel() 13 | type args struct { 14 | directive *config.Directive 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | want *config.UpstreamServer 20 | wantString string 21 | }{ 22 | { 23 | name: "new upstream server", 24 | args: args{ 25 | directive: &config.Directive{ 26 | Name: "server", 27 | Parameters: []config.Parameter{{Value: "127.0.0.1:8080"}}, 28 | }, 29 | }, 30 | want: &config.UpstreamServer{ 31 | Address: "127.0.0.1:8080", 32 | Flags: make([]string, 0), 33 | Parameters: make(map[string]string, 0), 34 | }, 35 | wantString: "server 127.0.0.1:8080;", 36 | }, 37 | { 38 | name: "new upstream server with weight", 39 | args: args{ 40 | directive: &config.Directive{ 41 | Name: "server", 42 | Parameters: []config.Parameter{{Value: "127.0.0.1:8080"}, {Value: "weight=5"}}, 43 | }, 44 | }, 45 | want: &config.UpstreamServer{ 46 | Address: "127.0.0.1:8080", 47 | Flags: make([]string, 0), 48 | Parameters: map[string]string{ 49 | "weight": "5", 50 | }, 51 | }, 52 | wantString: "server 127.0.0.1:8080 weight=5;", 53 | }, 54 | { 55 | name: "new upstream server with weight and a flag", 56 | args: args{ 57 | directive: &config.Directive{ 58 | Name: "server", 59 | Parameters: []config.Parameter{{Value: "127.0.0.1:8080"}, {Value: "weight=5"}, {Value: "down"}}, 60 | }, 61 | }, 62 | want: &config.UpstreamServer{ 63 | Address: "127.0.0.1:8080", 64 | Flags: []string{"down"}, 65 | Parameters: map[string]string{ 66 | "weight": "5", 67 | }, 68 | }, 69 | wantString: "server 127.0.0.1:8080 weight=5 down;", 70 | }, 71 | } 72 | for _, tt := range tests { 73 | t.Run(tt.name, func(t *testing.T) { 74 | got, err := config.NewUpstreamServer(tt.args.directive) 75 | assert.NilError(t, err, "no error expected here") 76 | if !reflect.DeepEqual(got, tt.want) { 77 | t.Errorf("NewUpstreamServer() = %v, want %v", got, tt.want) 78 | } 79 | 80 | if got.GetBlock() != nil { 81 | t.Error("Upstream server returns a block") 82 | } 83 | 84 | gotString := DumpDirective(got, NoIndentStyle) 85 | if gotString != tt.wantString { 86 | t.Errorf("NewUpstreamServer().ToString = %v, want %v", gotString, tt.wantString) 87 | } 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /config/block_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func NewServerOrNill(directive IDirective) *Server { 9 | s, _ := NewServer(directive) 10 | return s 11 | } 12 | 13 | func TestBlock_FindDirectives(t *testing.T) { 14 | t.Parallel() 15 | type args struct { 16 | directiveName string 17 | } 18 | tests := []struct { 19 | name string 20 | block *Block 21 | args args 22 | want []IDirective 23 | }{ 24 | { 25 | name: "find all servers", 26 | block: &Block{ 27 | Directives: []IDirective{ 28 | &Server{ 29 | Block: &Block{ 30 | Directives: []IDirective{ 31 | &Directive{ 32 | Name: "server_name", 33 | Parameters: []Parameter{{Value: "gonginx.dev"}}, 34 | }, 35 | }, 36 | }, 37 | }, 38 | &Server{ 39 | Block: &Block{ 40 | Directives: []IDirective{ 41 | &Directive{ 42 | Name: "server_name", 43 | Parameters: []Parameter{{Value: "gonginx2.dev"}}, 44 | }, 45 | }, 46 | }, 47 | }, 48 | &HTTP{ 49 | Servers: []*Server{ 50 | { 51 | Block: &Block{ 52 | Directives: []IDirective{ 53 | &Directive{ 54 | Name: "server_name", 55 | Parameters: []Parameter{{Value: "gonginx3.dev"}}, 56 | }, 57 | }, 58 | }, 59 | }, 60 | }, 61 | }, 62 | }, 63 | }, 64 | want: []IDirective{ 65 | &Server{ 66 | Block: &Block{ 67 | Directives: []IDirective{ 68 | &Directive{ 69 | Name: "server_name", 70 | Parameters: []Parameter{{Value: "gonginx.dev"}}, 71 | }, 72 | }, 73 | }, 74 | }, 75 | &Server{ 76 | Block: &Block{ 77 | Directives: []IDirective{ 78 | &Directive{ 79 | Name: "server_name", 80 | Parameters: []Parameter{{Value: "gonginx2.dev"}}, 81 | }, 82 | }, 83 | }, 84 | }, 85 | &Server{ 86 | Block: &Block{ 87 | Directives: []IDirective{ 88 | &Directive{ 89 | Name: "server_name", 90 | Parameters: []Parameter{{Value: "gonginx3.dev"}}, 91 | }, 92 | }, 93 | }, 94 | }, 95 | }, 96 | args: args{ 97 | directiveName: "server", 98 | }, 99 | }, 100 | } 101 | for _, tt := range tests { 102 | t.Run(tt.name, func(t *testing.T) { 103 | if got := tt.block.FindDirectives(tt.args.directiveName); !reflect.DeepEqual(got, tt.want) { 104 | t.Errorf("Block.FindDirectives() = %v want %v", got, tt.want) 105 | } 106 | }) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | //TODO(tufan): reactive after getting SaveToFile() done 4 | //func TestConfig_SaveToFile(t *testing.T) { 5 | // type fields struct { 6 | // Block *Block 7 | // FilePath string 8 | // } 9 | // tests := []struct { 10 | // name string 11 | // fields fields 12 | // wantErr bool 13 | // }{ 14 | // { 15 | // name: "block", 16 | // fields: fields{ 17 | // FilePath: "../full-example/unit-test.conf", 18 | // Block: &Block{ 19 | // Directives: []IDirective{ 20 | // &Directive{ 21 | // Name: "user", 22 | // Parameters: []string{"nginx", "nginx"}, 23 | // }, 24 | // &Directive{ 25 | // Name: "worker_processes", 26 | // Parameters: []string{"5"}, 27 | // }, 28 | // }, 29 | // }, 30 | // }, 31 | // wantErr: false, 32 | // }, 33 | // { 34 | // name: "block", 35 | // fields: fields{ 36 | // FilePath: "../full-example/unit-test.conf", 37 | // Block: &Block{ 38 | // Directives: []IDirective{ 39 | // &Directive{ 40 | // Name: "user", 41 | // Parameters: []string{"nginx", "nginx"}, 42 | // }, 43 | // &Directive{ 44 | // Name: "worker_processes", 45 | // Parameters: []string{"5"}, 46 | // }, 47 | // &Include{ 48 | // Directive: &Directive{ 49 | // Name: "include", 50 | // Parameters: []string{"/etc/nginx/conf/*.conf"}, 51 | // }, 52 | // IncludePath: "/etc/nginx/conf/*.conf", 53 | // }, 54 | // }, 55 | // }, 56 | // }, 57 | // wantErr: true, 58 | // }, 59 | // { 60 | // name: "block", 61 | // fields: fields{ 62 | // FilePath: "../full-example/unittest/file.conf", 63 | // Block: &Block{ 64 | // Directives: []IDirective{ 65 | // &Directive{ 66 | // Name: "user", 67 | // Parameters: []string{"nginx", "nginx"}, 68 | // }, 69 | // &Directive{ 70 | // Name: "worker_processes", 71 | // Parameters: []string{"5"}, 72 | // }, 73 | // &Include{ 74 | // Directive: &Directive{ 75 | // Name: "include", 76 | // Parameters: []string{"/etc/nginx/conf/*.conf"}, 77 | // }, 78 | // IncludePath: "/etc/nginx/conf/*.conf", 79 | // }, 80 | // }, 81 | // }, 82 | // }, 83 | // wantErr: true, 84 | // }, 85 | // } 86 | // os.RemoveAll("../full-example/makedir") 87 | // for _, tt := range tests { 88 | // t.Run(tt.name, func(t *testing.T) { 89 | // c := &Config{ 90 | // Block: tt.fields.Block, 91 | // FilePath: tt.fields.FilePath, 92 | // } 93 | // if err := c.SaveToFile(NoIndentStyle); (err != nil) != tt.wantErr { 94 | // t.Errorf("SaveToFile() error = %v, wantErr %v", err, tt.wantErr) 95 | // } 96 | // }) 97 | // } 98 | //} 99 | -------------------------------------------------------------------------------- /testdata/full_conf/koi-win: -------------------------------------------------------------------------------- 1 | 2 | charset_map koi8-r windows-1251 { 3 | 4 | 80 88 ; # euro 5 | 6 | 95 95 ; # bullet 7 | 8 | 9A A0 ; #   9 | 10 | 9E B7 ; # · 11 | 12 | A3 B8 ; # small yo 13 | A4 BA ; # small Ukrainian ye 14 | 15 | A6 B3 ; # small Ukrainian i 16 | A7 BF ; # small Ukrainian yi 17 | 18 | AD B4 ; # small Ukrainian soft g 19 | AE A2 ; # small Byelorussian short u 20 | 21 | B0 B0 ; # ° 22 | 23 | B3 A8 ; # capital YO 24 | B4 AA ; # capital Ukrainian YE 25 | 26 | B6 B2 ; # capital Ukrainian I 27 | B7 AF ; # capital Ukrainian YI 28 | 29 | B9 B9 ; # numero sign 30 | 31 | BD A5 ; # capital Ukrainian soft G 32 | BE A1 ; # capital Byelorussian short U 33 | 34 | BF A9 ; # (C) 35 | 36 | C0 FE ; # small yu 37 | C1 E0 ; # small a 38 | C2 E1 ; # small b 39 | C3 F6 ; # small ts 40 | C4 E4 ; # small d 41 | C5 E5 ; # small ye 42 | C6 F4 ; # small f 43 | C7 E3 ; # small g 44 | C8 F5 ; # small kh 45 | C9 E8 ; # small i 46 | CA E9 ; # small j 47 | CB EA ; # small k 48 | CC EB ; # small l 49 | CD EC ; # small m 50 | CE ED ; # small n 51 | CF EE ; # small o 52 | 53 | D0 EF ; # small p 54 | D1 FF ; # small ya 55 | D2 F0 ; # small r 56 | D3 F1 ; # small s 57 | D4 F2 ; # small t 58 | D5 F3 ; # small u 59 | D6 E6 ; # small zh 60 | D7 E2 ; # small v 61 | D8 FC ; # small soft sign 62 | D9 FB ; # small y 63 | DA E7 ; # small z 64 | DB F8 ; # small sh 65 | DC FD ; # small e 66 | DD F9 ; # small shch 67 | DE F7 ; # small ch 68 | DF FA ; # small hard sign 69 | 70 | E0 DE ; # capital YU 71 | E1 C0 ; # capital A 72 | E2 C1 ; # capital B 73 | E3 D6 ; # capital TS 74 | E4 C4 ; # capital D 75 | E5 C5 ; # capital YE 76 | E6 D4 ; # capital F 77 | E7 C3 ; # capital G 78 | E8 D5 ; # capital KH 79 | E9 C8 ; # capital I 80 | EA C9 ; # capital J 81 | EB CA ; # capital K 82 | EC CB ; # capital L 83 | ED CC ; # capital M 84 | EE CD ; # capital N 85 | EF CE ; # capital O 86 | 87 | F0 CF ; # capital P 88 | F1 DF ; # capital YA 89 | F2 D0 ; # capital R 90 | F3 D1 ; # capital S 91 | F4 D2 ; # capital T 92 | F5 D3 ; # capital U 93 | F6 C6 ; # capital ZH 94 | F7 C2 ; # capital V 95 | F8 DC ; # capital soft sign 96 | F9 DB ; # capital Y 97 | FA C7 ; # capital Z 98 | FB D8 ; # capital SH 99 | FC DD ; # capital E 100 | FD D9 ; # capital SHCH 101 | FE D7 ; # capital CH 102 | FF DA ; # capital hard sign 103 | } 104 | -------------------------------------------------------------------------------- /examples/dump-nginx-config/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/tufanbarisyildirim/gonginx/dumper" 8 | "github.com/tufanbarisyildirim/gonginx/parser" 9 | ) 10 | 11 | func dumpConfigToFile(fullConf string, filePath string) error { 12 | p := parser.NewStringParser(fullConf) 13 | conf, err := p.Parse() 14 | if err != nil { 15 | return fmt.Errorf("failed to parse config: %w", err) 16 | } 17 | 18 | dumpString := dumper.DumpConfig(conf, dumper.IndentedStyle) 19 | if err := os.WriteFile(filePath, []byte(dumpString), 0644); err != nil { 20 | return fmt.Errorf("failed to write config file: %w", err) 21 | } 22 | return nil 23 | } 24 | 25 | func dumpAndWriteConfigFile(fullConf string, filePath string) error { 26 | p := parser.NewStringParser(fullConf) 27 | conf, err := p.Parse() 28 | if err != nil { 29 | return fmt.Errorf("failed to parse config: %w", err) 30 | } 31 | // set config file path 32 | conf.FilePath = filePath 33 | err = dumper.WriteConfig(conf, dumper.IndentedStyle, false) 34 | if err != nil { 35 | panic(err) 36 | } 37 | return nil 38 | } 39 | 40 | func main() { 41 | fullConf := `user www www; 42 | worker_processes 5; 43 | error_log logs/error.log; 44 | pid logs/nginx.pid; 45 | worker_rlimit_nofile 8192; 46 | events { worker_connections 4096; } http { 47 | include mime.types; 48 | include proxy.conf; 49 | include fastcgi.conf; 50 | index index.html index.htm index.php; 51 | default_type application/octet-stream; 52 | log_format main '$remote_addr - $remote_user [$time_local] $status ' 53 | '"$request" $body_bytes_sent "$http_referer" ' 54 | ' "$http_user_agent" "$http_x_forwarded_for"'; 55 | access_log logs/access.log main; 56 | sendfile on; 57 | tcp_nopush on; 58 | server_names_hash_bucket_size 128; 59 | server { 60 | listen 80; 61 | server_name domain1.com www.domain1.com; 62 | access_log logs/domain1.access.log main; 63 | root html; 64 | location ~ \.php$ { 65 | fastcgi_pass 127.0.0.1:1025; } } server { 66 | listen 80; 67 | server_name domain2.com www.domain2.com; 68 | access_log logs/domain2.access.log main; 69 | location ~ ^/(images|javascript|js|css|flash|media|static)/ { 70 | root /var/www/virtual/big.server.com/htdocs; 71 | expires 30d; 72 | } location / { proxy_pass http://127.0.0.1:8080; } } 73 | upstream big_server_com { 74 | server 127.0.0.3:8000 weight=5; 75 | server 127.0.0.3:8001 weight=5; 76 | server 192.168.0.1:8000; 77 | server 192.168.0.1:8001; 78 | } server { listen 80; 79 | server_name big.server.com; 80 | access_log logs/big.server.access.log main; 81 | location / { proxy_pass http://big_server_com; } } }` 82 | 83 | // dump config with indented style 84 | dumpConfigToFile(fullConf, "nginx-temp.conf") 85 | 86 | // dump config to file whit indented style 87 | dumpAndWriteConfigFile(fullConf, "./nginx-temp2.conf") 88 | } 89 | -------------------------------------------------------------------------------- /config/include.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // Include represents an include directive. 8 | type Include struct { 9 | *Directive 10 | IncludePath string 11 | Configs []*Config 12 | Parent IDirective 13 | } 14 | 15 | //TODO(tufan): move that part into dumper package 16 | //SaveToFile saves include to its own file 17 | //func (i *Include) SaveToFile(style *dumper.Style) error { 18 | // if len(i.Configs) == 0 { 19 | // return fmt.Errorf("included empty file %s", i.IncludePath) 20 | // } 21 | // for _, c := range i.Configs { 22 | // err := c.SaveToFile(style) 23 | // if err != nil { 24 | // return err 25 | // } 26 | // } 27 | // return nil 28 | //} 29 | 30 | // SetLine sets the line number. 31 | func (c *Include) SetLine(line int) { 32 | c.Line = line 33 | } 34 | 35 | // GetLine returns the line number. 36 | func (c *Include) GetLine() int { 37 | return c.Line 38 | } 39 | 40 | // GetParent returns the parent directive. 41 | func (c *Include) GetParent() IDirective { 42 | return c.Parent 43 | } 44 | 45 | // SetParent sets the parent directive. 46 | func (c *Include) SetParent(parent IDirective) { 47 | c.Parent = parent 48 | } 49 | 50 | // GetDirectives returns all directives inside the included file. 51 | func (c *Include) GetDirectives() []IDirective { 52 | directives := make([]IDirective, 0) 53 | for _, config := range c.Configs { 54 | directives = append(directives, config.GetDirectives()...) 55 | } 56 | 57 | return directives 58 | } 59 | 60 | // FindDirectives finds a specific directive in the included file. 61 | func (c *Include) FindDirectives(directiveName string) []IDirective { 62 | directives := make([]IDirective, 0) 63 | for _, config := range c.Configs { 64 | directives = append(directives, config.FindDirectives(directiveName)...) 65 | } 66 | 67 | return directives 68 | } 69 | 70 | // GetName returns the directive name. 71 | func (c *Include) GetName() string { 72 | return c.Directive.Name 73 | } 74 | 75 | // SetComment sets the comment of the include directive. 76 | func (c *Include) SetComment(comment []string) { 77 | c.Comment = comment 78 | } 79 | 80 | // NewInclude initializes an Include from a directive. 81 | func NewInclude(dir IDirective) (*Include, error) { 82 | directive, ok := dir.(*Directive) 83 | if !ok { 84 | return nil, errors.New("type error") 85 | } 86 | include := &Include{ 87 | Directive: directive, 88 | IncludePath: directive.Parameters[0].GetValue(), 89 | } 90 | 91 | if len(directive.Parameters) > 1 { 92 | panic("include directive can not have multiple parameters") 93 | } 94 | 95 | if directive.Block != nil { 96 | panic("include can not have a block, or missing semicolon at the end of include statement") 97 | } 98 | return include, nil 99 | } 100 | -------------------------------------------------------------------------------- /config/server.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // Server represents a server block. 8 | type Server struct { 9 | Block IBlock 10 | Comment []string 11 | DefaultInlineComment 12 | Parent IDirective 13 | Line int 14 | } 15 | 16 | // SetLine sets the line number. 17 | func (s *Server) SetLine(line int) { 18 | s.Line = line 19 | } 20 | 21 | // GetLine returns the line number. 22 | func (s *Server) GetLine() int { 23 | return s.Line 24 | } 25 | 26 | // SetParent sets the parent directive. 27 | func (s *Server) SetParent(parent IDirective) { 28 | s.Parent = parent 29 | } 30 | 31 | // GetParent returns the parent directive. 32 | func (s *Server) GetParent() IDirective { 33 | return s.Parent 34 | } 35 | 36 | // SetComment sets the comment of the server directive. 37 | func (s *Server) SetComment(comment []string) { 38 | s.Comment = comment 39 | } 40 | 41 | // GetComment returns the comment of the server directive. 42 | func (s *Server) GetComment() []string { 43 | return s.Comment 44 | } 45 | 46 | // NewServer creates a server block from a directive with a block. 47 | func NewServer(directive IDirective) (*Server, error) { 48 | if block := directive.GetBlock(); block != nil { 49 | return &Server{ 50 | Block: block, 51 | Comment: directive.GetComment(), 52 | DefaultInlineComment: DefaultInlineComment{InlineComment: directive.GetInlineComment()}, 53 | }, nil 54 | } 55 | return nil, errors.New("server directive must have a block") 56 | } 57 | 58 | // GetName returns the directive name to construct the statement string. 59 | func (s *Server) GetName() string { //the directive name. 60 | return "server" 61 | } 62 | 63 | // GetParameters returns directive parameters if any. 64 | func (s *Server) GetParameters() []Parameter { 65 | return []Parameter{} 66 | } 67 | 68 | // GetBlock returns the block if any. 69 | func (s *Server) GetBlock() IBlock { 70 | return s.Block 71 | } 72 | 73 | // FindDirectives finds directives within the server block. 74 | func (s *Server) FindDirectives(directiveName string) []IDirective { 75 | directives := make([]IDirective, 0) 76 | for _, directive := range s.GetDirectives() { 77 | if directive.GetName() == directiveName { 78 | directives = append(directives, directive) 79 | } 80 | if include, ok := directive.(*Include); ok { 81 | for _, c := range include.Configs { 82 | directives = append(directives, c.FindDirectives(directiveName)...) 83 | } 84 | } 85 | if directive.GetBlock() != nil { 86 | directives = append(directives, directive.GetBlock().FindDirectives(directiveName)...) 87 | } 88 | } 89 | 90 | return directives 91 | } 92 | 93 | // GetDirectives returns all directives in the server. 94 | func (s *Server) GetDirectives() []IDirective { 95 | block := s.GetBlock() 96 | if block == nil { 97 | return []IDirective{} 98 | } 99 | return block.GetDirectives() 100 | } 101 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Config represents a complete nginx configuration file. 4 | type Config struct { 5 | *Block 6 | FilePath string 7 | } 8 | 9 | // Global wrappers provide extension points for custom directive handling. 10 | var ( 11 | BlockWrappers = map[string]func(*Directive) (IDirective, error){} 12 | DirectiveWrappers = map[string]func(*Directive) (IDirective, error){} 13 | IncludeWrappers = map[string]func(*Directive) (IDirective, error){} 14 | ) 15 | 16 | //TODO(tufan): move that part inti dumper package 17 | //SaveToFile save config to a file 18 | //TODO: add custom file / folder path support 19 | //func (c *Config) SaveToFile(style *dumper.Style) error { 20 | // //wrilte file 21 | // dirPath := filepath.Dir(c.FilePath) 22 | // if _, err := os.Stat(dirPath); err != nil { 23 | // err := os.MkdirAll(dirPath, os.ModePerm) 24 | // if err != nil { 25 | // return err //TODO: do we reallt need to find a way to test dir creating error? 26 | // } 27 | // } 28 | // 29 | // //write main file 30 | // err := ioutil.WriteFile(c.FilePath, c.ToByteArray(style), 0644) 31 | // if err != nil { 32 | // return err //TODO: do we need to find a way to test writing error? 33 | // } 34 | // 35 | // //write sub files (inlude /file/path) 36 | // for _, directive := range c.Block.Directives { 37 | // if fs, ok := (interface{}(directive)).(FileDirective); ok { 38 | // err := fs.SaveToFile(style) 39 | // if err != nil { 40 | // return err 41 | // } 42 | // } 43 | // } 44 | // 45 | // return nil 46 | //} 47 | 48 | // FindDirectives find directives from whole config block 49 | func (c *Config) FindDirectives(directiveName string) []IDirective { 50 | return c.Block.FindDirectives(directiveName) 51 | } 52 | 53 | // FindUpstreams find directives from whole config block 54 | func (c *Config) FindUpstreams() []*Upstream { 55 | var upstreams []*Upstream 56 | directives := c.Block.FindDirectives("upstream") 57 | for _, directive := range directives { 58 | // up, _ := NewUpstream(directive) 59 | upstreams = append(upstreams, directive.(*Upstream)) 60 | } 61 | return upstreams 62 | } 63 | 64 | func init() { 65 | BlockWrappers["http"] = func(directive *Directive) (IDirective, error) { 66 | return NewHTTP(directive) 67 | } 68 | BlockWrappers["location"] = func(directive *Directive) (IDirective, error) { 69 | return NewLocation(directive) 70 | } 71 | BlockWrappers["_by_lua_block"] = func(directive *Directive) (IDirective, error) { 72 | return NewLuaBlock(directive) 73 | } 74 | BlockWrappers["server"] = func(directive *Directive) (IDirective, error) { 75 | return NewServer(directive) 76 | } 77 | BlockWrappers["upstream"] = func(directive *Directive) (IDirective, error) { 78 | return NewUpstream(directive) 79 | } 80 | 81 | DirectiveWrappers["server"] = func(directive *Directive) (IDirective, error) { 82 | return NewUpstreamServer(directive) 83 | } 84 | 85 | IncludeWrappers["include"] = func(directive *Directive) (IDirective, error) { 86 | return NewInclude(directive) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /parser/token/token.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Type Token.Type 8 | type Type int 9 | 10 | const ( 11 | //EOF end of file 12 | EOF Type = iota 13 | //Eol end of line 14 | Eol 15 | //Keyword any keyword 16 | Keyword 17 | //QuotedString Quoted String 18 | QuotedString 19 | //Variable any $variabl 20 | Variable 21 | //BlockStart { 22 | BlockStart 23 | //BlockEnd } 24 | BlockEnd 25 | //Semicolon ; 26 | Semicolon 27 | //Comment #comment 28 | Comment 29 | //EndOfLine \n or \r 30 | EndOfLine 31 | //Illegal a token that should never happen 32 | Illegal 33 | //Regex any reg expression 34 | Regex 35 | // LuaCode lua block 36 | LuaCode 37 | ) 38 | 39 | var ( 40 | tokenName = map[Type]string{ 41 | QuotedString: "QuotedString", 42 | EOF: "Eof", 43 | Keyword: "Keyword", 44 | Variable: "Variable", 45 | BlockStart: "BlockStart", 46 | BlockEnd: "BlockEnd", 47 | Semicolon: "Semicolon", 48 | Comment: "Comment", 49 | EndOfLine: "EndOfLine", 50 | Illegal: "Illegal", 51 | Regex: "Regex", 52 | } 53 | ) 54 | 55 | // String convert a token to string as it should be written 56 | func (tt Type) String() string { 57 | return tokenName[tt] 58 | } 59 | 60 | // Token represents a config token 61 | type Token struct { 62 | Type Type 63 | Literal string 64 | Line int 65 | Column int 66 | } 67 | 68 | func (t Token) String() string { 69 | return fmt.Sprintf("{Type:%s,Literal:\"%s\",Line:%d,Column:%d}", t.Type, t.Literal, t.Line, t.Column) 70 | } 71 | 72 | // Lit set literal string 73 | func (t Token) Lit(literal string) Token { 74 | t.Literal = literal 75 | return t 76 | } 77 | 78 | // EqualTo checks equality 79 | func (t Token) EqualTo(t2 Token) bool { 80 | return t.Type == t2.Type && t.Literal == t2.Literal 81 | } 82 | 83 | // Tokens list of token 84 | type Tokens []Token 85 | 86 | // EqualTo check Tokens equality of token list 87 | func (ts Tokens) EqualTo(tokens Tokens) bool { 88 | if len(ts) != len(tokens) { 89 | return false 90 | } 91 | for i, t := range ts { 92 | if !t.EqualTo(tokens[i]) { 93 | return false 94 | } 95 | } 96 | return true 97 | } 98 | 99 | // Diff find what is difference between two Token collection 100 | func (ts Tokens) Diff(tokens Tokens) error { 101 | if len(ts) != len(tokens) { 102 | return fmt.Errorf("different token count %d vs %d", len(ts), len(tokens)) 103 | } 104 | for i, t := range ts { 105 | if t.Type != tokens[i].Type { 106 | return fmt.Errorf("i=%d, Type[%s]!=Type[%s]", i, t.Type.String(), tokens[i].Type.String()) 107 | } 108 | 109 | if !t.EqualTo(tokens[i]) { 110 | return fmt.Errorf("tokens are not equal: i=%d, %v!=%v", i, t, tokens[i]) 111 | } 112 | } 113 | return nil 114 | } 115 | 116 | // Is check type of a token 117 | func (t Token) Is(typ Type) bool { 118 | return t.Type == typ 119 | } 120 | 121 | // IsParameterEligible checks if token is directive parameter eligible 122 | func (t Token) IsParameterEligible() bool { 123 | return t.Is(Keyword) || t.Is(QuotedString) || t.Is(Variable) || t.Is(Regex) 124 | } 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

3 | Report Card 4 | Actions Status 5 |

6 | 7 | # Gonginx 8 | TBH, I would like to rewrite the parser next time I need it again :)) but it still does its job. 9 | 10 | Gonginx is an Nginx configuration parser helps you to parse, edit, regenerate your nginx config files in your go applications. It makes managing your balancer configurations easier. 11 | 12 | ## Basic grammar of an nginx config file 13 | ```yacc 14 | 15 | %token Keyword Variable BlockStart BlockEnd Semicolon Regex 16 | 17 | %% 18 | 19 | config : /* empty */ 20 | | config directives 21 | ; 22 | block : BlockStart directives BlockEnd 23 | ; 24 | directives : directives directive 25 | ; 26 | directive : Keyword [parameters] (semicolon|block) 27 | ; 28 | parameters : parameters keyword 29 | ; 30 | keyword : Keyword 31 | | Variable 32 | | Regex 33 | ; 34 | ``` 35 | 36 | ## API 37 | 38 | ## Core Components 39 | - ### [Parser](/parser/parser.go) 40 | Parser is the main package that analyzes and turns nginx structred files into objects. It basically has 3 libraries, `lexer` explodes it into `token`s and `parser` turns tokens into config objects which are in their own package, 41 | - ### [Config](/config/config.go) 42 | Config package is representation of any context, directive or their parameters in golang. So basically they are models and also AST 43 | - ### [Dumper](/dumper/dumper.go) 44 | Dumper is the package that holds styling configuration only. 45 | 46 | ## Examples 47 | - [Formatting](/examples/formatting/main.go) 48 | - [Adding a Server to upstream block](/examples/adding-server/main.go) 49 | - [add-custom-directive](/examples/add-custom-directive/main.go) 50 | - [dump-nginx-config](/examples/dump-nginx-config/main.go) 51 | - [update-directive](/examples/update-directive/main.go) 52 | - [update-server-listen-port](/examples/update-server-listen-port/main.go) 53 | 54 | ### [Examples and Library Reference](/GUIDE.md) 55 | #### TODO 56 | - [x] associate comments with config objects to print them on config generation and make it configurable with `dumper.Style` 57 | - [x] move any context wrapper into their own file (remove from parser) 58 | - [x] Parse included files recusively, keep relative path on load, save all in a related structure and make that optional in dumper.Style 59 | - [ ] Implement specific searches, like finding servers by server_name (domain) or any upstream by target etc. 60 | - [ ] add more examples 61 | - [x] link the parent directive to any directive for easier manipulation 62 | 63 | ## Limitations 64 | There is no known limitations yet. PRs are more than welcome if you want to implement a specific directive / block, please read [Contributing](CONTRIBUTING.md) before your first PR. 65 | 66 | ## License 67 | [MIT License](LICENSE) 68 | -------------------------------------------------------------------------------- /testdata/issues/31.conf: -------------------------------------------------------------------------------- 1 | #user http; 2 | worker_processes 1; 3 | 4 | #error_log logs/error.log; 5 | #error_log logs/error.log notice; 6 | #error_log logs/error.log info; 7 | 8 | #pid logs/nginx.pid; 9 | 10 | 11 | events { 12 | worker_connections 1024; 13 | } 14 | 15 | 16 | http { 17 | include mime.types; 18 | default_type application/octet-stream; 19 | 20 | #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 21 | # '$status $body_bytes_sent "$http_referer" ' 22 | # '"$http_user_agent" "$http_x_forwarded_for"'; 23 | 24 | #access_log logs/access.log main; 25 | 26 | sendfile on; 27 | #tcp_nopush on; 28 | 29 | #keepalive_timeout 0; 30 | keepalive_timeout 65; 31 | 32 | #gzip on; 33 | 34 | server { 35 | listen 80; 36 | server_name localhost; 37 | 38 | #charset koi8-r; 39 | 40 | #access_log logs/host.access.log main; 41 | 42 | location / { 43 | root /usr/share/nginx/html; 44 | index index.html index.htm; 45 | } 46 | 47 | #error_page 404 /404.html; 48 | 49 | # redirect server error pages to the static page /50x.html 50 | # 51 | error_page 500 502 503 504 /50x.html; 52 | location = /50x.html { 53 | root /usr/share/nginx/html; 54 | } 55 | 56 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 57 | # 58 | #location ~ \.php$ { 59 | # proxy_pass http://127.0.0.1; 60 | #} 61 | 62 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 63 | # 64 | #location ~ \.php$ { 65 | # root html; 66 | # fastcgi_pass 127.0.0.1:9000; 67 | # fastcgi_index index.php; 68 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 69 | # include fastcgi_params; 70 | #} 71 | 72 | # deny access to .htaccess files, if Apache's document root 73 | # concurs with nginx's one 74 | # 75 | #location ~ /\.ht { 76 | # deny all; 77 | #} 78 | } 79 | 80 | 81 | # another virtual host using mix of IP-, name-, and port-based configuration 82 | # 83 | server { 84 | listen 8000; 85 | # listen somename:8080; 86 | # server_name somename alias another.alias; 87 | 88 | location / { 89 | root html; 90 | index index.html index.htm; 91 | } 92 | 93 | 94 | # HTTPS server 95 | # 96 | #server { 97 | # listen 443 ssl; 98 | # server_name localhost; 99 | 100 | # ssl_certificate cert.pem; 101 | # ssl_certificate_key cert.key; 102 | 103 | # ssl_session_cache shared:SSL:1m; 104 | # ssl_session_timeout 5m; 105 | 106 | # ssl_ciphers HIGH:!aNULL:!MD5; 107 | # ssl_prefer_server_ciphers on; 108 | 109 | # location / { 110 | # root html; 111 | # index index.html index.htm; 112 | # } 113 | #} 114 | 115 | } -------------------------------------------------------------------------------- /testdata/full_conf/nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | #user nobody; 3 | worker_processes 1; 4 | 5 | #error_log logs/error.log; 6 | #error_log logs/error.log notice; 7 | #error_log logs/error.log info; 8 | 9 | #pid logs/nginx.pid; 10 | 11 | 12 | events { 13 | worker_connections 1024; 14 | } 15 | 16 | 17 | http { 18 | include mime.types; 19 | default_type application/octet-stream; 20 | 21 | #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 22 | # '$status $body_bytes_sent "$http_referer" ' 23 | # '"$http_user_agent" "$http_x_forwarded_for"'; 24 | 25 | #access_log logs/access.log main; 26 | 27 | sendfile on; 28 | #tcp_nopush on; 29 | 30 | #keepalive_timeout 0; 31 | keepalive_timeout 65; 32 | 33 | #gzip on; 34 | 35 | server { 36 | listen 80; 37 | server_name localhost; 38 | 39 | #charset koi8-r; 40 | 41 | #access_log logs/host.access.log main; 42 | 43 | location / { 44 | root html; 45 | index index.html index.htm; 46 | } 47 | 48 | #error_page 404 /404.html; 49 | 50 | # redirect server error pages to the static page /50x.html 51 | # 52 | error_page 500 502 503 504 /50x.html; 53 | location = /50x.html { 54 | root html; 55 | } 56 | 57 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 58 | # 59 | #location ~ \.php$ { 60 | # proxy_pass http://127.0.0.1; 61 | #} 62 | 63 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 64 | # 65 | #location ~ \.php$ { 66 | # root html; 67 | # fastcgi_pass 127.0.0.1:9000; 68 | # fastcgi_index index.php; 69 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 70 | # include fastcgi_params; 71 | #} 72 | 73 | # deny access to .htaccess files, if Apache's document root 74 | # concurs with nginx's one 75 | # 76 | #location ~ /\.ht { 77 | # deny all; 78 | #} 79 | } 80 | 81 | 82 | # another virtual host using mix of IP-, name-, and port-based configuration 83 | # 84 | #server { 85 | # listen 8000; 86 | # listen somename:8080; 87 | # server_name somename alias another.alias; 88 | 89 | # location / { 90 | # root html; 91 | # index index.html index.htm; 92 | # } 93 | #} 94 | 95 | 96 | # HTTPS server 97 | # 98 | #server { 99 | # listen 443 ssl; 100 | # server_name localhost; 101 | 102 | # ssl_certificate cert.pem; 103 | # ssl_certificate_key cert.key; 104 | 105 | # ssl_session_cache shared:SSL:1m; 106 | # ssl_session_timeout 5m; 107 | 108 | # ssl_ciphers HIGH:!aNULL:!MD5; 109 | # ssl_prefer_server_ciphers on; 110 | 111 | # location / { 112 | # root html; 113 | # index index.html index.htm; 114 | # } 115 | #} 116 | 117 | } 118 | -------------------------------------------------------------------------------- /config/lua_block.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // LuaBlock represents *_by_lua_block 8 | type LuaBlock struct { 9 | Directives []IDirective 10 | Name string 11 | Comment []string 12 | DefaultInlineComment 13 | LuaCode string 14 | Parent IDirective 15 | Line int 16 | Parameters []Parameter 17 | } 18 | 19 | // NewLuaBlock creates a lua block 20 | func NewLuaBlock(directive IDirective) (*LuaBlock, error) { 21 | if block := directive.GetBlock(); block != nil { 22 | lb := &LuaBlock{ 23 | Directives: []IDirective{}, 24 | Name: directive.GetName(), 25 | LuaCode: block.GetCodeBlock(), 26 | Parameters: directive.GetParameters(), 27 | } 28 | 29 | lb.Directives = append(lb.Directives, block.GetDirectives()...) 30 | lb.Comment = directive.GetComment() 31 | lb.InlineComment = directive.GetInlineComment() 32 | 33 | return lb, nil 34 | } 35 | return nil, fmt.Errorf("%s must have a block", directive.GetName()) 36 | } 37 | 38 | // SetLine Set line number 39 | func (lb *LuaBlock) SetLine(line int) { 40 | lb.Line = line 41 | } 42 | 43 | // GetLine Get the line number 44 | func (lb *LuaBlock) GetLine() int { 45 | return lb.Line 46 | } 47 | 48 | // SetParent change the parent block 49 | func (lb *LuaBlock) SetParent(parent IDirective) { 50 | lb.Parent = parent 51 | } 52 | 53 | // GetParent the parent block 54 | func (lb *LuaBlock) GetParent() IDirective { 55 | return lb.Parent 56 | } 57 | 58 | // GetName get directive name to construct the statment string 59 | func (lb *LuaBlock) GetName() string { //the directive name. 60 | return lb.Name 61 | } 62 | 63 | // GetParameters get directive parameters if any 64 | func (lb *LuaBlock) GetParameters() []Parameter { 65 | return lb.Parameters 66 | } 67 | 68 | // GetDirectives get all directives in lua block 69 | // this should return 1 code block and that should be it 70 | func (lb *LuaBlock) GetDirectives() []IDirective { 71 | directives := make([]IDirective, 0) 72 | directives = append(directives, lb.Directives...) 73 | return directives 74 | } 75 | 76 | // FindDirectives find directives 77 | func (lb *LuaBlock) FindDirectives(directiveName string) []IDirective { 78 | directives := make([]IDirective, 0) 79 | for _, directive := range lb.GetDirectives() { 80 | if directive.GetName() == directiveName { 81 | directives = append(directives, directive) 82 | } 83 | if include, ok := directive.(*Include); ok { 84 | for _, c := range include.Configs { 85 | directives = append(directives, c.FindDirectives(directiveName)...) 86 | } 87 | } 88 | if directive.GetBlock() != nil { 89 | directives = append(directives, directive.GetBlock().FindDirectives(directiveName)...) 90 | } 91 | } 92 | 93 | return directives 94 | } 95 | 96 | // GetCodeBlock returns the literal code block 97 | func (lb *LuaBlock) GetCodeBlock() string { 98 | return lb.LuaCode 99 | } 100 | 101 | // GetBlock get block if any 102 | func (lb *LuaBlock) GetBlock() IBlock { 103 | return lb 104 | } 105 | 106 | // GetComment get directive comment 107 | func (lb *LuaBlock) GetComment() []string { 108 | return lb.Comment 109 | } 110 | 111 | // SetComment sets comment tied to this directive 112 | func (lb *LuaBlock) SetComment(comment []string) { 113 | lb.Comment = comment 114 | } 115 | -------------------------------------------------------------------------------- /testdata/full_conf/koi-utf: -------------------------------------------------------------------------------- 1 | 2 | # This map is not a full koi8-r <> utf8 map: it does not contain 3 | # box-drawing and some other characters. Besides this map contains 4 | # several koi8-u and Byelorussian letters which are not in koi8-r. 5 | # If you need a full and standard map, use contrib/unicode2nginx/koi-utf 6 | # map instead. 7 | 8 | charset_map koi8-r utf-8 { 9 | 10 | 80 E282AC ; # euro 11 | 12 | 95 E280A2 ; # bullet 13 | 14 | 9A C2A0 ; #   15 | 16 | 9E C2B7 ; # · 17 | 18 | A3 D191 ; # small yo 19 | A4 D194 ; # small Ukrainian ye 20 | 21 | A6 D196 ; # small Ukrainian i 22 | A7 D197 ; # small Ukrainian yi 23 | 24 | AD D291 ; # small Ukrainian soft g 25 | AE D19E ; # small Byelorussian short u 26 | 27 | B0 C2B0 ; # ° 28 | 29 | B3 D081 ; # capital YO 30 | B4 D084 ; # capital Ukrainian YE 31 | 32 | B6 D086 ; # capital Ukrainian I 33 | B7 D087 ; # capital Ukrainian YI 34 | 35 | B9 E28496 ; # numero sign 36 | 37 | BD D290 ; # capital Ukrainian soft G 38 | BE D18E ; # capital Byelorussian short U 39 | 40 | BF C2A9 ; # (C) 41 | 42 | C0 D18E ; # small yu 43 | C1 D0B0 ; # small a 44 | C2 D0B1 ; # small b 45 | C3 D186 ; # small ts 46 | C4 D0B4 ; # small d 47 | C5 D0B5 ; # small ye 48 | C6 D184 ; # small f 49 | C7 D0B3 ; # small g 50 | C8 D185 ; # small kh 51 | C9 D0B8 ; # small i 52 | CA D0B9 ; # small j 53 | CB D0BA ; # small k 54 | CC D0BB ; # small l 55 | CD D0BC ; # small m 56 | CE D0BD ; # small n 57 | CF D0BE ; # small o 58 | 59 | D0 D0BF ; # small p 60 | D1 D18F ; # small ya 61 | D2 D180 ; # small r 62 | D3 D181 ; # small s 63 | D4 D182 ; # small t 64 | D5 D183 ; # small u 65 | D6 D0B6 ; # small zh 66 | D7 D0B2 ; # small v 67 | D8 D18C ; # small soft sign 68 | D9 D18B ; # small y 69 | DA D0B7 ; # small z 70 | DB D188 ; # small sh 71 | DC D18D ; # small e 72 | DD D189 ; # small shch 73 | DE D187 ; # small ch 74 | DF D18A ; # small hard sign 75 | 76 | E0 D0AE ; # capital YU 77 | E1 D090 ; # capital A 78 | E2 D091 ; # capital B 79 | E3 D0A6 ; # capital TS 80 | E4 D094 ; # capital D 81 | E5 D095 ; # capital YE 82 | E6 D0A4 ; # capital F 83 | E7 D093 ; # capital G 84 | E8 D0A5 ; # capital KH 85 | E9 D098 ; # capital I 86 | EA D099 ; # capital J 87 | EB D09A ; # capital K 88 | EC D09B ; # capital L 89 | ED D09C ; # capital M 90 | EE D09D ; # capital N 91 | EF D09E ; # capital O 92 | 93 | F0 D09F ; # capital P 94 | F1 D0AF ; # capital YA 95 | F2 D0A0 ; # capital R 96 | F3 D0A1 ; # capital S 97 | F4 D0A2 ; # capital T 98 | F5 D0A3 ; # capital U 99 | F6 D096 ; # capital ZH 100 | F7 D092 ; # capital V 101 | F8 D0AC ; # capital soft sign 102 | F9 D0AB ; # capital Y 103 | FA D097 ; # capital Z 104 | FB D0A8 ; # capital SH 105 | FC D0AD ; # capital E 106 | FD D0A9 ; # capital SHCH 107 | FE D0A7 ; # capital CH 108 | FF D0AA ; # capital hard sign 109 | } 110 | -------------------------------------------------------------------------------- /config/http.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // HTTP represents an http block. 8 | type HTTP struct { 9 | Servers []*Server 10 | Directives []IDirective 11 | Comment []string 12 | DefaultInlineComment 13 | Parent IDirective 14 | Line int 15 | } 16 | 17 | // SetLine sets the line number. 18 | func (h *HTTP) SetLine(line int) { 19 | h.Line = line 20 | } 21 | 22 | // GetLine returns the line number. 23 | func (h *HTTP) GetLine() int { 24 | return h.Line 25 | } 26 | 27 | // SetParent sets the parent directive. 28 | func (h *HTTP) SetParent(parent IDirective) { 29 | h.Parent = parent 30 | } 31 | 32 | // GetParent returns the parent directive. 33 | func (h *HTTP) GetParent() IDirective { 34 | return h.Parent 35 | } 36 | 37 | // GetComment returns the comment of the HTTP directive. 38 | func (h *HTTP) GetComment() []string { 39 | return h.Comment 40 | } 41 | 42 | // SetComment sets the comment of the HTTP directive. 43 | func (h *HTTP) SetComment(comment []string) { 44 | h.Comment = comment 45 | } 46 | 47 | // NewHTTP creates an HTTP block from a directive that has a block. 48 | func NewHTTP(directive IDirective) (*HTTP, error) { 49 | if block := directive.GetBlock(); block != nil { 50 | http := &HTTP{ 51 | Servers: []*Server{}, 52 | Directives: []IDirective{}, 53 | } 54 | for _, directive := range block.GetDirectives() { 55 | if server, ok := directive.(*Server); ok { 56 | server.Parent = http 57 | http.Servers = append(http.Servers, server) 58 | continue 59 | } 60 | http.Directives = append(http.Directives, directive) 61 | } 62 | http.Comment = directive.GetComment() 63 | http.InlineComment = directive.GetInlineComment() 64 | 65 | return http, nil 66 | } 67 | return nil, errors.New("http directive must have a block") 68 | } 69 | 70 | // GetName returns the directive name to construct the statement string. 71 | func (h *HTTP) GetName() string { //the directive name. 72 | return "http" 73 | } 74 | 75 | // GetParameters returns directive parameters, if any. 76 | func (h *HTTP) GetParameters() []Parameter { 77 | return []Parameter{} 78 | } 79 | 80 | // GetDirectives returns all directives in the http block. 81 | func (h *HTTP) GetDirectives() []IDirective { 82 | directives := make([]IDirective, 0) 83 | directives = append(directives, h.Directives...) 84 | for _, directive := range h.Servers { 85 | directives = append(directives, directive) 86 | } 87 | return directives 88 | } 89 | 90 | // FindDirectives finds directives in the http block. 91 | func (h *HTTP) FindDirectives(directiveName string) []IDirective { 92 | directives := make([]IDirective, 0) 93 | for _, directive := range h.GetDirectives() { 94 | if directive.GetName() == directiveName { 95 | directives = append(directives, directive) 96 | } 97 | if include, ok := directive.(*Include); ok { 98 | for _, c := range include.Configs { 99 | directives = append(directives, c.FindDirectives(directiveName)...) 100 | } 101 | } 102 | if directive.GetBlock() != nil { 103 | directives = append(directives, directive.GetBlock().FindDirectives(directiveName)...) 104 | } 105 | } 106 | 107 | return directives 108 | } 109 | 110 | // GetBlock returns the block itself. 111 | func (h *HTTP) GetBlock() IBlock { 112 | return h 113 | } 114 | 115 | // GetCodeBlock returns the literal code block. 116 | func (h *HTTP) GetCodeBlock() string { 117 | return "" 118 | } 119 | -------------------------------------------------------------------------------- /full-example/nginx2.conf: -------------------------------------------------------------------------------- 1 | worker_processes 2; 2 | pid /var/run/nginx.pid; 3 | 4 | # [ debug | info | notice | warn | error | crit ] 5 | error_log /var/log/nginx.error_log info; 6 | 7 | events { 8 | worker_connections 2000; 9 | # use [ kqueue | rtsig | epoll | /dev/poll | select | poll ] ; 10 | use kqueue; 11 | } 12 | 13 | http { 14 | include conf/mime.types; 15 | include conf/vhosts/*.conf; 16 | 17 | default_type application/octet-stream; 18 | 19 | log_format main '$remote_addr - $remote_user [$time_local] ' 20 | '"$request" $status $bytes_sent ' 21 | '"$http_referer" "$http_user_agent" ' 22 | '"$gzip_ratio"'; 23 | 24 | log_format download '$remote_addr - $remote_user [$time_local] ' 25 | '"$request" $status $bytes_sent ' 26 | '"$http_referer" "$http_user_agent" ' 27 | '"$http_range" "$sent_http_content_range"'; 28 | 29 | client_header_timeout 3m; 30 | client_body_timeout 3m; 31 | send_timeout 3m; 32 | 33 | client_header_buffer_size 1k; 34 | large_client_header_buffers 4 4k; 35 | 36 | include conf/options/gzip_settings.conf; 37 | gzip on; 38 | gzip_min_length 1100; 39 | gzip_buffers 4 8k; 40 | gzip_types text/plain; 41 | 42 | output_buffers 1 32k; 43 | postpone_output 1460; 44 | 45 | sendfile on; 46 | tcp_nopush on; 47 | 48 | tcp_nodelay on; 49 | send_lowat 12000; 50 | 51 | keepalive_timeout 75 20; 52 | 53 | # lingering_time 30; 54 | # lingering_timeout 10; 55 | # reset_timedout_connection on; 56 | 57 | 58 | server { 59 | listen one.example.com; 60 | server_name one.example.com www.one.example.com; 61 | 62 | access_log /var/log/nginx.access_log main; 63 | 64 | location / { 65 | proxy_pass http://127.0.0.1/; 66 | proxy_redirect off; 67 | 68 | proxy_set_header Host $host; 69 | proxy_set_header X-Real-IP $remote_addr; 70 | # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 71 | 72 | client_max_body_size 10m; 73 | client_body_buffer_size 128k; 74 | 75 | client_body_temp_path /var/nginx/client_body_temp; 76 | 77 | proxy_connect_timeout 90; 78 | proxy_send_timeout 90; 79 | proxy_read_timeout 90; 80 | proxy_send_lowat 12000; 81 | 82 | proxy_buffer_size 4k; 83 | proxy_buffers 4 32k; 84 | proxy_busy_buffers_size 64k; 85 | proxy_temp_file_write_size 64k; 86 | 87 | proxy_temp_path /var/nginx/proxy_temp; 88 | 89 | charset koi8-r; 90 | } 91 | 92 | error_page 404 /404.html; 93 | 94 | location /404.html { 95 | root /spool/www; 96 | 97 | charset on; 98 | source_charset koi8-r; 99 | } 100 | 101 | location /old_stuff/ { 102 | rewrite ^/old_stuff/(.*)$ /new_stuff/$1 permanent; 103 | } 104 | 105 | location /download/ { 106 | valid_referers none blocked server_names *.example.com; 107 | 108 | if ($invalid_referer) { 109 | #rewrite ^/ http://www.example.com/; 110 | return 403; 111 | } 112 | 113 | # rewrite_log on; 114 | # rewrite /download/*/mp3/*.any_ext to /download/*/mp3/*.mp3 115 | rewrite ^/(download/.*)/mp3/(.*)\..*$ /$1/mp3/$2.mp3 break; 116 | 117 | root /spool/www; 118 | # autoindex on; 119 | access_log /var/log/nginx-download.access_log download; 120 | } 121 | 122 | # asdsadds 123 | 124 | 125 | 126 | location ~* ^.+\.(jpg|jpeg|gif)$ { 127 | root /spool/www; 128 | access_log off; 129 | expires 30d; 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /config/upstream.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // Upstream represents an `upstream{}` block. 8 | type Upstream struct { 9 | UpstreamName string 10 | UpstreamServers []*UpstreamServer 11 | //Directives Other directives in upstream (ip_hash; etc) 12 | Directives []IDirective 13 | Comment []string 14 | DefaultInlineComment 15 | Parent IDirective 16 | Line int 17 | } 18 | 19 | // SetLine sets the line number. 20 | func (us *Upstream) SetLine(line int) { 21 | us.Line = line 22 | } 23 | 24 | // GetLine returns the line number. 25 | func (us *Upstream) GetLine() int { 26 | return us.Line 27 | } 28 | 29 | // SetParent sets the parent directive. 30 | func (us *Upstream) SetParent(parent IDirective) { 31 | us.Parent = parent 32 | } 33 | 34 | // GetParent returns the parent directive. 35 | func (us *Upstream) GetParent() IDirective { 36 | return us.Parent 37 | } 38 | 39 | // SetComment sets the directive comment. 40 | func (us *Upstream) SetComment(comment []string) { 41 | us.Comment = comment 42 | } 43 | 44 | // GetName implements the Statement interface. 45 | func (us *Upstream) GetName() string { 46 | return "upstream" 47 | } 48 | 49 | // GetParameters returns the upstream parameters. 50 | func (us *Upstream) GetParameters() []Parameter { 51 | return []Parameter{{Value: us.UpstreamName}} //the only parameter for an upstream is its name 52 | } 53 | 54 | // GetBlock returns the upstream itself, which implements IBlock. 55 | func (us *Upstream) GetBlock() IBlock { 56 | return us 57 | } 58 | 59 | // GetComment returns the directive comment. 60 | func (us *Upstream) GetComment() []string { 61 | return us.Comment 62 | } 63 | 64 | // GetDirectives returns sub directives of the upstream. 65 | func (us *Upstream) GetDirectives() []IDirective { 66 | directives := make([]IDirective, 0) 67 | directives = append(directives, us.Directives...) 68 | for _, uss := range us.UpstreamServers { 69 | directives = append(directives, uss) 70 | } 71 | 72 | return directives 73 | } 74 | 75 | // NewUpstream creates a new Upstream from a directive. 76 | func NewUpstream(directive IDirective) (*Upstream, error) { 77 | parameters := directive.GetParameters() 78 | us := &Upstream{ 79 | UpstreamName: parameters[0].GetValue(), //first parameter of the directive is the upstream name 80 | } 81 | 82 | if directive.GetBlock() == nil { 83 | return nil, errors.New("missing upstream block") 84 | } 85 | 86 | if len(directive.GetBlock().GetDirectives()) > 0 { 87 | for _, d := range directive.GetBlock().GetDirectives() { 88 | if d.GetName() == "server" { 89 | uss, err := NewUpstreamServer(d) 90 | if err != nil { 91 | return nil, err 92 | } 93 | uss.SetParent(us) 94 | uss.SetLine(d.GetLine()) 95 | us.UpstreamServers = append(us.UpstreamServers, uss) 96 | } else { 97 | us.Directives = append(us.Directives, d) 98 | } 99 | } 100 | } 101 | 102 | us.Comment = directive.GetComment() 103 | us.InlineComment = directive.GetInlineComment() 104 | 105 | return us, nil 106 | } 107 | 108 | // AddServer adds a server to the upstream. 109 | func (us *Upstream) AddServer(server *UpstreamServer) { 110 | us.UpstreamServers = append(us.UpstreamServers, server) 111 | } 112 | 113 | // GetCodeBlock returns the literal code block. 114 | func (us *Upstream) GetCodeBlock() string { 115 | return "" 116 | } 117 | 118 | // FindDirectives finds directives in the block recursively. 119 | func (us *Upstream) FindDirectives(directiveName string) []IDirective { 120 | directives := make([]IDirective, 0) 121 | for _, directive := range us.Directives { 122 | if directive.GetName() == directiveName { 123 | directives = append(directives, directive) 124 | } 125 | if directive.GetBlock() != nil { 126 | directives = append(directives, directive.GetBlock().FindDirectives(directiveName)...) 127 | } 128 | } 129 | 130 | return directives 131 | } 132 | -------------------------------------------------------------------------------- /config/upstream_server.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | ) 8 | 9 | // UpstreamServer represents a `server` directive in an `upstream{}` block. 10 | type UpstreamServer struct { 11 | Address string 12 | Flags []string 13 | Parameters map[string]string 14 | Comment []string 15 | DefaultInlineComment 16 | Parent IDirective 17 | Line int 18 | } 19 | 20 | // SetLine sets the line number. 21 | func (uss *UpstreamServer) SetLine(line int) { 22 | uss.Line = line 23 | } 24 | 25 | // GetLine returns the line number. 26 | func (uss *UpstreamServer) GetLine() int { 27 | return uss.Line 28 | } 29 | 30 | // SetParent sets the parent directive. 31 | func (uss *UpstreamServer) SetParent(parent IDirective) { 32 | uss.Parent = parent 33 | } 34 | 35 | // GetParent returns the parent directive. 36 | func (uss *UpstreamServer) GetParent() IDirective { 37 | return uss.Parent 38 | } 39 | 40 | // SetComment sets the directive comment. 41 | func (uss *UpstreamServer) SetComment(comment []string) { 42 | uss.Comment = comment 43 | } 44 | 45 | // GetComment returns the directive comment. 46 | func (uss *UpstreamServer) GetComment() []string { 47 | return uss.Comment 48 | } 49 | 50 | // GetName implements the Statement interface. 51 | func (uss *UpstreamServer) GetName() string { 52 | return "server" 53 | } 54 | 55 | // GetBlock returns nil because upstream servers do not have blocks. 56 | func (uss *UpstreamServer) GetBlock() IBlock { 57 | return nil 58 | } 59 | 60 | // GetParameters returns parameters for the upstream server. 61 | func (uss *UpstreamServer) GetParameters() []Parameter { 62 | return uss.GetDirective().Parameters 63 | } 64 | 65 | // GetDirective returns the directive representation of the upstream server. 66 | func (uss *UpstreamServer) GetDirective() *Directive { 67 | //First, generate a new directive from upstream server 68 | directive := &Directive{ 69 | Name: "server", 70 | Parameters: make([]Parameter, 0), 71 | Block: nil, 72 | } 73 | 74 | //address it the first parameter of an upstream directive 75 | directive.Parameters = append(directive.Parameters, Parameter{Value: uss.Address}) 76 | 77 | //Iterations are random in golang maps https://blog.golang.org/maps#TOC_7. 78 | //we sort keys in different slice and print them sorted. 79 | //we always expect key=values parameters to be sorted by key 80 | paramNames := make([]string, 0) 81 | for k := range uss.Parameters { 82 | paramNames = append(paramNames, k) 83 | } 84 | sort.Strings(paramNames) 85 | 86 | //append named parameters first 87 | for _, k := range paramNames { 88 | directive.Parameters = append(directive.Parameters, Parameter{Value: fmt.Sprintf("%s=%s", k, uss.Parameters[k])}) 89 | } 90 | 91 | //append flags to the end of the directive. 92 | for _, flag := range uss.Flags { 93 | directive.Parameters = append(directive.Parameters, Parameter{Value: flag}) 94 | } 95 | 96 | directive.Comment = uss.GetComment() 97 | 98 | return directive 99 | } 100 | 101 | // NewUpstreamServer creates an UpstreamServer from a directive. 102 | func NewUpstreamServer(directive IDirective) (*UpstreamServer, error) { 103 | uss := &UpstreamServer{ 104 | Flags: make([]string, 0), 105 | Parameters: make(map[string]string, 0), 106 | Comment: make([]string, 0), 107 | } 108 | 109 | for i, parameter := range directive.GetParameters() { 110 | if i == 0 { // alright, we asuume that firstone should be a server address 111 | uss.Address = parameter.GetValue() 112 | continue 113 | } 114 | if strings.Contains(parameter.GetValue(), "=") { //a parameter like weight=5 115 | s := strings.SplitN(parameter.GetValue(), "=", 2) 116 | uss.Parameters[s[0]] = s[1] 117 | } else { 118 | uss.Flags = append(uss.Flags, parameter.GetValue()) 119 | } 120 | } 121 | 122 | uss.Comment = directive.GetComment() 123 | uss.InlineComment = directive.GetInlineComment() 124 | 125 | return uss, nil 126 | } 127 | -------------------------------------------------------------------------------- /testdata/full_conf/win-utf: -------------------------------------------------------------------------------- 1 | 2 | # This map is not a full windows-1251 <> utf8 map: it does not 3 | # contain Serbian and Macedonian letters. If you need a full map, 4 | # use contrib/unicode2nginx/win-utf map instead. 5 | 6 | charset_map windows-1251 utf-8 { 7 | 8 | 82 E2809A ; # single low-9 quotation mark 9 | 10 | 84 E2809E ; # double low-9 quotation mark 11 | 85 E280A6 ; # ellipsis 12 | 86 E280A0 ; # dagger 13 | 87 E280A1 ; # double dagger 14 | 88 E282AC ; # euro 15 | 89 E280B0 ; # per mille 16 | 17 | 91 E28098 ; # left single quotation mark 18 | 92 E28099 ; # right single quotation mark 19 | 93 E2809C ; # left double quotation mark 20 | 94 E2809D ; # right double quotation mark 21 | 95 E280A2 ; # bullet 22 | 96 E28093 ; # en dash 23 | 97 E28094 ; # em dash 24 | 25 | 99 E284A2 ; # trade mark sign 26 | 27 | A0 C2A0 ; #   28 | A1 D18E ; # capital Byelorussian short U 29 | A2 D19E ; # small Byelorussian short u 30 | 31 | A4 C2A4 ; # currency sign 32 | A5 D290 ; # capital Ukrainian soft G 33 | A6 C2A6 ; # borken bar 34 | A7 C2A7 ; # section sign 35 | A8 D081 ; # capital YO 36 | A9 C2A9 ; # (C) 37 | AA D084 ; # capital Ukrainian YE 38 | AB C2AB ; # left-pointing double angle quotation mark 39 | AC C2AC ; # not sign 40 | AD C2AD ; # soft hypen 41 | AE C2AE ; # (R) 42 | AF D087 ; # capital Ukrainian YI 43 | 44 | B0 C2B0 ; # ° 45 | B1 C2B1 ; # plus-minus sign 46 | B2 D086 ; # capital Ukrainian I 47 | B3 D196 ; # small Ukrainian i 48 | B4 D291 ; # small Ukrainian soft g 49 | B5 C2B5 ; # micro sign 50 | B6 C2B6 ; # pilcrow sign 51 | B7 C2B7 ; # · 52 | B8 D191 ; # small yo 53 | B9 E28496 ; # numero sign 54 | BA D194 ; # small Ukrainian ye 55 | BB C2BB ; # right-pointing double angle quotation mark 56 | 57 | BF D197 ; # small Ukrainian yi 58 | 59 | C0 D090 ; # capital A 60 | C1 D091 ; # capital B 61 | C2 D092 ; # capital V 62 | C3 D093 ; # capital G 63 | C4 D094 ; # capital D 64 | C5 D095 ; # capital YE 65 | C6 D096 ; # capital ZH 66 | C7 D097 ; # capital Z 67 | C8 D098 ; # capital I 68 | C9 D099 ; # capital J 69 | CA D09A ; # capital K 70 | CB D09B ; # capital L 71 | CC D09C ; # capital M 72 | CD D09D ; # capital N 73 | CE D09E ; # capital O 74 | CF D09F ; # capital P 75 | 76 | D0 D0A0 ; # capital R 77 | D1 D0A1 ; # capital S 78 | D2 D0A2 ; # capital T 79 | D3 D0A3 ; # capital U 80 | D4 D0A4 ; # capital F 81 | D5 D0A5 ; # capital KH 82 | D6 D0A6 ; # capital TS 83 | D7 D0A7 ; # capital CH 84 | D8 D0A8 ; # capital SH 85 | D9 D0A9 ; # capital SHCH 86 | DA D0AA ; # capital hard sign 87 | DB D0AB ; # capital Y 88 | DC D0AC ; # capital soft sign 89 | DD D0AD ; # capital E 90 | DE D0AE ; # capital YU 91 | DF D0AF ; # capital YA 92 | 93 | E0 D0B0 ; # small a 94 | E1 D0B1 ; # small b 95 | E2 D0B2 ; # small v 96 | E3 D0B3 ; # small g 97 | E4 D0B4 ; # small d 98 | E5 D0B5 ; # small ye 99 | E6 D0B6 ; # small zh 100 | E7 D0B7 ; # small z 101 | E8 D0B8 ; # small i 102 | E9 D0B9 ; # small j 103 | EA D0BA ; # small k 104 | EB D0BB ; # small l 105 | EC D0BC ; # small m 106 | ED D0BD ; # small n 107 | EE D0BE ; # small o 108 | EF D0BF ; # small p 109 | 110 | F0 D180 ; # small r 111 | F1 D181 ; # small s 112 | F2 D182 ; # small t 113 | F3 D183 ; # small u 114 | F4 D184 ; # small f 115 | F5 D185 ; # small kh 116 | F6 D186 ; # small ts 117 | F7 D187 ; # small ch 118 | F8 D188 ; # small sh 119 | F9 D189 ; # small shch 120 | FA D18A ; # small hard sign 121 | FB D18B ; # small y 122 | FC D18C ; # small soft sign 123 | FD D18D ; # small e 124 | FE D18E ; # small yu 125 | FF D18F ; # small ya 126 | } 127 | -------------------------------------------------------------------------------- /dumper/block_test.go: -------------------------------------------------------------------------------- 1 | package dumper 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tufanbarisyildirim/gonginx/config" 7 | ) 8 | 9 | func NewServerOrNill(directive config.IDirective) *config.Server { 10 | s, _ := config.NewServer(directive) 11 | return s 12 | } 13 | func TestBlock_ToString(t *testing.T) { 14 | t.Parallel() 15 | type fields struct { 16 | Directives []config.IDirective 17 | } 18 | tests := []struct { 19 | name string 20 | fields fields 21 | want string 22 | wantSorted string 23 | wantSortedSpaceBeforeBlocks string 24 | }{ 25 | { 26 | name: "empty block", 27 | fields: fields{ 28 | Directives: make([]config.IDirective, 0), 29 | }, 30 | want: "", 31 | }, 32 | { 33 | name: "statement list", 34 | fields: fields{ 35 | Directives: []config.IDirective{ 36 | &config.Directive{ 37 | Name: "user", 38 | Parameters: []config.Parameter{ 39 | {Value: "nginx"}, 40 | {Value: "nginx"}, 41 | }, 42 | }, 43 | &config.Directive{ 44 | Name: "worker_processes", 45 | Parameters: []config.Parameter{{Value: "5"}}, 46 | }, 47 | }, 48 | }, 49 | want: "user nginx nginx;\nworker_processes 5;", 50 | wantSorted: "user nginx nginx;\nworker_processes 5;", 51 | wantSortedSpaceBeforeBlocks: "user nginx nginx;\nworker_processes 5;", 52 | }, 53 | { 54 | name: "statement list with wrapped directives", 55 | fields: fields{ 56 | Directives: []config.IDirective{ 57 | &config.Directive{ 58 | Name: "user", 59 | Parameters: []config.Parameter{ 60 | {Value: "nginx"}, 61 | {Value: "nginx"}}, 62 | }, 63 | &config.Directive{ 64 | Name: "worker_processes", 65 | Parameters: []config.Parameter{{Value: "5"}}, 66 | }, 67 | &config.Include{ 68 | Directive: &config.Directive{ 69 | Name: "include", 70 | Parameters: []config.Parameter{{Value: "/etc/nginx/conf/*.conf"}}, 71 | }, 72 | IncludePath: "/etc/nginx/conf/*.conf", 73 | }, 74 | NewServerOrNill(&config.Directive{ 75 | Block: &config.Block{ 76 | Directives: []config.IDirective{ 77 | &config.Directive{ 78 | Name: "user", 79 | Parameters: []config.Parameter{ 80 | {Value: "nginx"}, 81 | {Value: "nginx"}, 82 | }, 83 | }, 84 | &config.Directive{ 85 | Name: "worker_processes", 86 | Parameters: []config.Parameter{{Value: "5"}}, 87 | }, 88 | &config.Include{ 89 | Directive: &config.Directive{ 90 | Name: "include", 91 | Parameters: []config.Parameter{{Value: "/etc/nginx/conf/*.conf"}}, 92 | }, 93 | IncludePath: "/etc/nginx/conf/*.conf", 94 | }, 95 | }, 96 | }, 97 | Name: "server", 98 | }), 99 | }, 100 | }, 101 | want: "user nginx nginx;\nworker_processes 5;\ninclude /etc/nginx/conf/*.conf;\nserver {\nuser nginx nginx;\nworker_processes 5;\ninclude /etc/nginx/conf/*.conf;\n}", 102 | wantSorted: "include /etc/nginx/conf/*.conf;\nserver {\ninclude /etc/nginx/conf/*.conf;\nuser nginx nginx;\nworker_processes 5;\n}\nuser nginx nginx;\nworker_processes 5;", 103 | wantSortedSpaceBeforeBlocks: "include /etc/nginx/conf/*.conf;\n\nserver {\ninclude /etc/nginx/conf/*.conf;\nuser nginx nginx;\nworker_processes 5;\n}\nuser nginx nginx;\nworker_processes 5;", 104 | }, 105 | } 106 | for _, tt := range tests { 107 | t.Run(tt.name, func(t *testing.T) { 108 | b := &config.Block{ 109 | Directives: tt.fields.Directives, 110 | } 111 | if got := DumpBlock(b, NoIndentStyle); got != tt.want { 112 | t.Errorf("Block.ToString(NoIndentStyle) = \"%v\", want \"%v\"", got, tt.want) 113 | } 114 | if got := DumpBlock(b, NoIndentSortedStyle); got != tt.wantSorted { 115 | t.Errorf("Block.ToString(NoIndentSortedStyle) = \"%v\", want \"%v\"", got, tt.wantSorted) 116 | } 117 | if got := DumpBlock(b, NoIndentSortedSpaceStyle); got != tt.wantSortedSpaceBeforeBlocks { 118 | t.Errorf("Block.ToString(NoIndentSortedSpaceStyle) = \"%v\", want \"%v\"", got, tt.wantSortedSpaceBeforeBlocks) 119 | } 120 | }) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /dumper/upstream_test.go: -------------------------------------------------------------------------------- 1 | package dumper 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/tufanbarisyildirim/gonginx/config" 8 | ) 9 | 10 | func TestUpstream_ToString(t *testing.T) { 11 | t.Parallel() 12 | type fields struct { 13 | Directive *config.Directive 14 | Name string 15 | } 16 | tests := []struct { 17 | name string 18 | fields fields 19 | want string 20 | }{ 21 | { 22 | name: "empty upstream block with name", 23 | fields: fields{ 24 | Directive: &config.Directive{ 25 | Name: "upstream", 26 | Parameters: []config.Parameter{{Value: "gonginx_upstream"}}, 27 | Block: &config.Block{ 28 | Directives: make([]config.IDirective, 0), 29 | }, 30 | }, 31 | }, 32 | want: "upstream gonginx_upstream {\n\n}", 33 | }, 34 | { 35 | name: "empty upstream block with name and upstream server", 36 | fields: fields{ 37 | Directive: &config.Directive{ 38 | Name: "upstream", 39 | Parameters: []config.Parameter{{Value: "gonginx_upstream"}}, 40 | Block: &config.Block{ 41 | Directives: []config.IDirective{ 42 | NewUpstreamServerIgnoreErr(&config.Directive{ 43 | Name: "server", 44 | Parameters: []config.Parameter{{Value: "127.0.0.1:9005"}}, 45 | }), 46 | }, 47 | }, 48 | }, 49 | }, 50 | want: "upstream gonginx_upstream {\nserver 127.0.0.1:9005;\n}", 51 | }, 52 | { 53 | name: "empty upstream block with name and multi upstream server", 54 | fields: fields{ 55 | Directive: &config.Directive{ 56 | Name: "upstream", 57 | Parameters: []config.Parameter{{Value: "gonginx_upstream"}}, 58 | Block: &config.Block{ 59 | Directives: []config.IDirective{ 60 | NewUpstreamServerIgnoreErr(&config.Directive{ 61 | Name: "server", 62 | Parameters: []config.Parameter{{Value: "127.0.0.1:9005"}}, 63 | }), 64 | NewUpstreamServerIgnoreErr(&config.Directive{ 65 | Name: "server", 66 | Parameters: []config.Parameter{{Value: "127.0.0.2:9005"}}, 67 | }), 68 | }, 69 | }, 70 | }, 71 | }, 72 | want: "upstream gonginx_upstream {\nserver 127.0.0.1:9005;\nserver 127.0.0.2:9005;\n}", 73 | }, 74 | { 75 | name: "empty upstream block with name and multi upstream server and some flags, params", 76 | fields: fields{ 77 | Directive: &config.Directive{ 78 | Name: "upstream", 79 | Parameters: []config.Parameter{{Value: "gonginx_upstream"}}, 80 | Block: &config.Block{ 81 | Directives: []config.IDirective{ 82 | NewUpstreamServerIgnoreErr(&config.Directive{ 83 | Name: "server", 84 | Parameters: []config.Parameter{{Value: "127.0.0.1:9005"}, {Value: "weight=5"}}, 85 | }), 86 | NewUpstreamServerIgnoreErr(&config.Directive{ 87 | Name: "server", 88 | Parameters: []config.Parameter{{Value: "127.0.0.2:9005"}, {Value: "weight=4"}, {Value: "down"}}, 89 | }), 90 | }, 91 | }, 92 | }, 93 | }, 94 | want: "upstream gonginx_upstream {\nserver 127.0.0.1:9005 weight=5;\nserver 127.0.0.2:9005 weight=4 down;\n}", 95 | }, 96 | } 97 | for _, tt := range tests { 98 | t.Run(tt.name, func(t *testing.T) { 99 | us, err := config.NewUpstream(tt.fields.Directive) 100 | if err != nil { 101 | t.Error("Failed to create NewUpstream(*tt.fields.Directive)") 102 | } 103 | if got := DumpDirective(us, NoIndentStyle); got != tt.want { 104 | t.Errorf("Upstream.ToString() = %v, want %v", got, tt.want) 105 | } 106 | }) 107 | } 108 | } 109 | 110 | func NewUpstreamServerIgnoreErr(directive config.IDirective) *config.UpstreamServer { 111 | server, _ := config.NewUpstreamServer(directive) 112 | return server 113 | } 114 | 115 | func TestUpstream_AddServer(t *testing.T) { 116 | t.Parallel() 117 | type fields struct { 118 | UpstreamName string 119 | UpstreamServers []*config.UpstreamServer 120 | Directives []config.IDirective 121 | } 122 | type args struct { 123 | server *config.UpstreamServer 124 | } 125 | tests := []struct { 126 | name string 127 | fields fields 128 | args args 129 | toString string 130 | }{ 131 | { 132 | name: "add simple server", 133 | fields: fields{ 134 | UpstreamName: "my_backend", 135 | UpstreamServers: []*config.UpstreamServer{ 136 | { 137 | Address: "127.0.0.1:8080", 138 | Flags: []string{"backup"}, 139 | Parameters: map[string]string{ 140 | "weight": "1", 141 | }, 142 | }, 143 | }, 144 | }, 145 | args: args{ 146 | server: &config.UpstreamServer{ 147 | Address: "backend2.gonginx.org:8090", 148 | Flags: []string{"resolve"}, 149 | Parameters: map[string]string{ 150 | "fail_timeout": "5s", 151 | "slow_start": "30s", 152 | }, 153 | }, 154 | }, 155 | toString: `upstream my_backend { 156 | server 127.0.0.1:8080 weight=1 backup; 157 | server backend2.gonginx.org:8090 fail_timeout=5s slow_start=30s resolve; 158 | }`, 159 | }, 160 | } 161 | for _, tt := range tests { 162 | t.Run(tt.name, func(t *testing.T) { 163 | us := &config.Upstream{ 164 | UpstreamName: tt.fields.UpstreamName, 165 | UpstreamServers: tt.fields.UpstreamServers, 166 | Directives: tt.fields.Directives, 167 | } 168 | us.AddServer(tt.args.server) 169 | if got := DumpDirective(us, NoIndentStyle); got != tt.toString { 170 | t.Errorf("us.ToString() = `%v`, want `%v`", strings.ReplaceAll(got, " ", "."), strings.ReplaceAll(tt.toString, " ", ".")) 171 | } 172 | }) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /testdata/full_conf/mime.types: -------------------------------------------------------------------------------- 1 | 2 | types { 3 | text/html html htm shtml; 4 | text/css css; 5 | text/xml xml; 6 | image/gif gif; 7 | image/jpeg jpeg jpg; 8 | application/javascript js; 9 | application/atom+xml atom; 10 | application/rss+xml rss; 11 | 12 | text/mathml mml; 13 | text/plain txt; 14 | text/vnd.sun.j2me.app-descriptor jad; 15 | text/vnd.wap.wml wml; 16 | text/x-component htc; 17 | 18 | image/avif avif; 19 | image/png png; 20 | image/svg+xml svg svgz; 21 | image/tiff tif tiff; 22 | image/vnd.wap.wbmp wbmp; 23 | image/webp webp; 24 | image/x-icon ico; 25 | image/x-jng jng; 26 | image/x-ms-bmp bmp; 27 | 28 | font/woff woff; 29 | font/woff2 woff2; 30 | 31 | application/java-archive jar war ear; 32 | application/json json; 33 | application/mac-binhex40 hqx; 34 | application/msword doc; 35 | application/pdf pdf; 36 | application/postscript ps eps ai; 37 | application/rtf rtf; 38 | application/vnd.apple.mpegurl m3u8; 39 | application/vnd.google-earth.kml+xml kml; 40 | application/vnd.google-earth.kmz kmz; 41 | application/vnd.ms-excel xls; 42 | application/vnd.ms-fontobject eot; 43 | application/vnd.ms-powerpoint ppt; 44 | application/vnd.oasis.opendocument.graphics odg; 45 | application/vnd.oasis.opendocument.presentation odp; 46 | application/vnd.oasis.opendocument.spreadsheet ods; 47 | application/vnd.oasis.opendocument.text odt; 48 | application/vnd.openxmlformats-officedocument.presentationml.presentation 49 | pptx; 50 | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 51 | xlsx; 52 | application/vnd.openxmlformats-officedocument.wordprocessingml.document 53 | docx; 54 | application/vnd.wap.wmlc wmlc; 55 | application/wasm wasm; 56 | application/x-7z-compressed 7z; 57 | application/x-cocoa cco; 58 | application/x-java-archive-diff jardiff; 59 | application/x-java-jnlp-file jnlp; 60 | application/x-makeself run; 61 | application/x-perl pl pm; 62 | application/x-pilot prc pdb; 63 | application/x-rar-compressed rar; 64 | application/x-redhat-package-manager rpm; 65 | application/x-sea sea; 66 | application/x-shockwave-flash swf; 67 | application/x-stuffit sit; 68 | application/x-tcl tcl tk; 69 | application/x-x509-ca-cert der pem crt; 70 | application/x-xpinstall xpi; 71 | application/xhtml+xml xhtml; 72 | application/xspf+xml xspf; 73 | application/zip zip; 74 | 75 | application/octet-stream bin exe dll; 76 | application/octet-stream deb; 77 | application/octet-stream dmg; 78 | application/octet-stream iso img; 79 | application/octet-stream msi msp msm; 80 | 81 | audio/midi mid midi kar; 82 | audio/mpeg mp3; 83 | audio/ogg ogg; 84 | audio/x-m4a m4a; 85 | audio/x-realaudio ra; 86 | 87 | video/3gpp 3gpp 3gp; 88 | video/mp2t ts; 89 | video/mp4 mp4; 90 | video/mpeg mpeg mpg; 91 | video/quicktime mov; 92 | video/webm webm; 93 | video/x-flv flv; 94 | video/x-m4v m4v; 95 | video/x-mng mng; 96 | video/x-ms-asf asx asf; 97 | video/x-ms-wmv wmv; 98 | video/x-msvideo avi; 99 | } 100 | -------------------------------------------------------------------------------- /dumper/dumper.go: -------------------------------------------------------------------------------- 1 | package dumper 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "sort" 9 | "strings" 10 | 11 | "github.com/tufanbarisyildirim/gonginx/config" 12 | ) 13 | 14 | var ( 15 | //NoIndentStyle default style 16 | NoIndentStyle = &Style{ 17 | SortDirectives: false, 18 | StartIndent: 0, 19 | Indent: 0, 20 | Debug: false, 21 | } 22 | 23 | //IndentedStyle default style 24 | IndentedStyle = &Style{ 25 | SortDirectives: false, 26 | StartIndent: 0, 27 | Indent: 4, 28 | Debug: false, 29 | } 30 | 31 | //NoIndentSortedStyle default style 32 | NoIndentSortedStyle = &Style{ 33 | SortDirectives: true, 34 | StartIndent: 0, 35 | Indent: 0, 36 | Debug: false, 37 | } 38 | 39 | //NoIndentSortedSpaceStyle default style 40 | NoIndentSortedSpaceStyle = &Style{ 41 | SortDirectives: true, 42 | SpaceBeforeBlocks: true, 43 | StartIndent: 0, 44 | Indent: 0, 45 | Debug: false, 46 | } 47 | ) 48 | 49 | // Style dumping style 50 | type Style struct { 51 | SortDirectives bool 52 | SpaceBeforeBlocks bool 53 | StartIndent int 54 | Indent int 55 | Debug bool 56 | } 57 | 58 | // NewStyle create new style 59 | func NewStyle() *Style { 60 | style := &Style{ 61 | SortDirectives: false, 62 | StartIndent: 0, 63 | Indent: 4, 64 | Debug: false, 65 | } 66 | return style 67 | } 68 | 69 | // Iterate interate the indentation for sub blocks 70 | func (s *Style) Iterate() *Style { 71 | newStyle := &Style{ 72 | SortDirectives: s.SortDirectives, 73 | SpaceBeforeBlocks: s.SpaceBeforeBlocks, 74 | StartIndent: s.StartIndent + s.Indent, 75 | Indent: s.Indent, 76 | } 77 | return newStyle 78 | } 79 | 80 | // DumpDirective convert a directive to a string 81 | func DumpDirective(d config.IDirective, style *Style) string { 82 | if d == nil { 83 | return "" 84 | } 85 | 86 | var buf bytes.Buffer 87 | 88 | if style.SpaceBeforeBlocks && d.GetBlock() != nil { 89 | buf.WriteString("\n") 90 | } 91 | // outline comment 92 | if len(d.GetComment()) > 0 { 93 | for _, comment := range d.GetComment() { 94 | buf.WriteString(fmt.Sprintf("%s%s\n", strings.Repeat(" ", style.StartIndent), comment)) 95 | } 96 | } 97 | buf.WriteString(fmt.Sprintf("%s%s", strings.Repeat(" ", style.StartIndent), d.GetName())) 98 | 99 | inlineComments := make(map[int]config.InlineComment) 100 | for _, comment := range d.GetInlineComment() { 101 | inlineComments[comment.RelativeLineIndex] = comment 102 | } 103 | 104 | // Use relative line index to handle different line number arrangements of instruction parameters 105 | relativeLineIndex := 0 106 | for _, parameter := range d.GetParameters() { 107 | // If the parameter line index is not the same as the previous one, add a new line 108 | if parameter.GetRelativeLineIndex() != relativeLineIndex { 109 | // write param comment 110 | if comment, ok := inlineComments[relativeLineIndex]; ok { 111 | buf.WriteString(fmt.Sprintf(" %s\n", comment.Value)) 112 | } else { 113 | buf.WriteString("\n") 114 | } 115 | buf.WriteString(fmt.Sprintf("%s%s", strings.Repeat(" ", style.StartIndent+style.Indent), parameter.GetValue())) 116 | relativeLineIndex = parameter.GetRelativeLineIndex() 117 | } else { 118 | buf.WriteString(fmt.Sprintf(" %s", parameter.GetValue())) 119 | } 120 | } 121 | 122 | if d.GetBlock() == nil { 123 | if d.GetName() != "" { 124 | buf.WriteRune(';') 125 | } 126 | // the last inline comment 127 | if comment, ok := inlineComments[relativeLineIndex]; ok { 128 | buf.WriteString(comment.Value) 129 | } 130 | } else { 131 | buf.WriteString(" {\n") 132 | buf.WriteString(DumpBlock(d.GetBlock(), style.Iterate())) 133 | buf.WriteString(fmt.Sprintf("\n%s}", strings.Repeat(" ", style.StartIndent))) 134 | } 135 | return buf.String() 136 | } 137 | 138 | // DumpBlock convert a directive to a string 139 | func DumpBlock(b config.IBlock, style *Style) string { 140 | if b.GetCodeBlock() != "" { 141 | return DumpLuaBlock(b, style) 142 | } 143 | 144 | var buf bytes.Buffer 145 | directives := b.GetDirectives() 146 | if style.SortDirectives { 147 | sort.SliceStable(directives, func(i, j int) bool { 148 | return directives[i].GetName() < directives[j].GetName() 149 | }) 150 | } 151 | 152 | for i, directive := range directives { 153 | if style.Debug { 154 | buf.WriteString("#") 155 | buf.WriteString(directive.GetName()) 156 | buf.WriteString(fmt.Sprintf("%t", directive.GetBlock())) 157 | buf.WriteString("\n") 158 | } 159 | buf.WriteString(DumpDirective(directive, style)) 160 | if i != len(directives)-1 { 161 | buf.WriteString("\n") 162 | } 163 | } 164 | 165 | return buf.String() 166 | } 167 | 168 | // DumpConfig dump whole config 169 | func DumpConfig(c *config.Config, style *Style) string { 170 | return DumpBlock(c.Block, style) 171 | } 172 | 173 | // DumpInclude dump(stringify) the included AST 174 | func DumpInclude(i *config.Include, style *Style) map[string]string { 175 | mp := make(map[string]string) 176 | for _, cfg := range i.Configs { 177 | mp[cfg.FilePath] = DumpConfig(cfg, style) 178 | } 179 | return mp 180 | } 181 | 182 | // WriteConfig writes config 183 | func WriteConfig(c *config.Config, style *Style, writeInclude bool) error { 184 | if writeInclude { 185 | includes := c.FindDirectives("include") 186 | for _, include := range includes { 187 | i, ok := include.(*config.Include) 188 | if !ok { 189 | panic("bug in FindDirective") 190 | } 191 | 192 | // no config parsed 193 | if len(i.Configs) == 0 { 194 | continue 195 | } 196 | 197 | mp := DumpInclude(i, style) 198 | for path, config := range mp { 199 | // create parent directories, if not exit 200 | dir, _ := filepath.Split(path) 201 | err := os.MkdirAll(dir, 0755) 202 | if err != nil { 203 | return err 204 | } 205 | err = os.WriteFile(path, []byte(config), 0644) 206 | if err != nil { 207 | return err 208 | } 209 | } 210 | } 211 | } 212 | // create parent directories, if not exit 213 | dir, _ := filepath.Split(c.FilePath) 214 | err := os.MkdirAll(dir, 0755) 215 | if err != nil { 216 | return err 217 | } 218 | return os.WriteFile(c.FilePath, []byte(DumpConfig(c, style)), 0644) 219 | } 220 | -------------------------------------------------------------------------------- /parser/lexer.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "strings" 8 | 9 | "github.com/tufanbarisyildirim/gonginx/parser/token" 10 | ) 11 | 12 | // lexer is the main tokenizer 13 | type lexer struct { 14 | reader *bufio.Reader 15 | file string 16 | line int 17 | column int 18 | inLuaBlock bool 19 | Latest token.Token 20 | } 21 | 22 | // lex initializes a lexer from string conetnt 23 | func lex(content string) *lexer { 24 | return newLexer(bytes.NewBuffer([]byte(content))) 25 | } 26 | 27 | // newLexer initilizes a lexer from a reader 28 | func newLexer(r io.Reader) *lexer { 29 | return &lexer{ 30 | line: 1, 31 | reader: bufio.NewReader(r), 32 | } 33 | } 34 | 35 | // Scan gives you next token 36 | func (s *lexer) scan() token.Token { 37 | s.Latest = s.getNextToken() 38 | return s.Latest 39 | } 40 | 41 | // All scans all token and returns them as a slice 42 | func (s *lexer) all() token.Tokens { 43 | tokens := make([]token.Token, 0) 44 | for { 45 | v := s.scan() 46 | if v.Type == token.EOF || v.Type == -1 { 47 | break 48 | } 49 | tokens = append(tokens, v) 50 | } 51 | return tokens 52 | } 53 | 54 | func (s *lexer) getNextToken() token.Token { 55 | if s.inLuaBlock { 56 | s.inLuaBlock = false 57 | return s.scanLuaCode() 58 | } 59 | reToken: 60 | ch := s.peek() 61 | switch { 62 | case isSpace(ch): 63 | s.skipWhitespace() 64 | goto reToken 65 | case isEOF(ch): 66 | return s.NewToken(token.EOF).Lit(string(s.read())) 67 | case ch == ';': 68 | return s.NewToken(token.Semicolon).Lit(string(s.read())) 69 | case ch == '{': 70 | if isLuaBlock(s.Latest) { 71 | s.inLuaBlock = true 72 | } 73 | return s.NewToken(token.BlockStart).Lit(string(s.read())) 74 | case ch == '}': 75 | return s.NewToken(token.BlockEnd).Lit(string(s.read())) 76 | case ch == '#': 77 | return s.scanComment() 78 | case isEndOfLine(ch): 79 | return s.NewToken(token.EndOfLine).Lit(string(s.read())) 80 | case isQuote(ch): 81 | return s.scanQuotedString(ch) 82 | default: 83 | return s.scanKeyword() 84 | } 85 | } 86 | 87 | // Peek returns nexr rune without consuming it 88 | func (s *lexer) peek() rune { 89 | r, _, _ := s.reader.ReadRune() 90 | _ = s.reader.UnreadRune() 91 | return r 92 | } 93 | 94 | type runeCheck func(rune) bool 95 | 96 | func (s *lexer) readUntil(until runeCheck) string { 97 | var buf bytes.Buffer 98 | buf.WriteRune(s.read()) 99 | 100 | for { 101 | ch := s.peek() 102 | if isEOF(ch) { 103 | break 104 | } else if until(ch) { 105 | // Check if this is a $ followed by a variable like ${var} 106 | // If so, don't break - this is part of the token 107 | if ch == '}' && s.maybeLookingAtVariableClose() { 108 | buf.WriteRune(s.read()) 109 | continue 110 | } 111 | break 112 | } else { 113 | buf.WriteRune(s.read()) 114 | } 115 | } 116 | 117 | return buf.String() 118 | } 119 | 120 | // maybeLookingAtVariableClose checks if we're possibly inside a variable reference 121 | // This is a heuristic to determine if a closing brace is part of a variable reference 122 | func (s *lexer) maybeLookingAtVariableClose() bool { 123 | // Try to peek ahead to see if this looks like a variable pattern 124 | 125 | // Read and immediately unread to preserve our position 126 | s.reader.ReadRune() 127 | ch := s.peek() 128 | s.reader.UnreadRune() 129 | 130 | // If the next character after } is $, 1-9, or a letter, we might be in a variable context 131 | return ch == '$' || (ch >= '0' && ch <= '9') || 132 | (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') 133 | } 134 | 135 | // NewToken creates a new Token with its line and column 136 | func (s *lexer) NewToken(tokenType token.Type) token.Token { 137 | return token.Token{ 138 | Type: tokenType, 139 | Line: s.line, 140 | Column: s.column, 141 | } 142 | } 143 | 144 | func (s *lexer) readWhile(while runeCheck) string { 145 | var buf bytes.Buffer 146 | buf.WriteRune(s.read()) 147 | 148 | for { 149 | if ch := s.peek(); while(ch) { 150 | buf.WriteRune(s.read()) 151 | } else { 152 | break 153 | } 154 | } 155 | // unread the latest char we consume. 156 | return buf.String() 157 | } 158 | 159 | func (s *lexer) skipWhitespace() { 160 | s.readWhile(isSpace) 161 | } 162 | 163 | func (s *lexer) scanComment() token.Token { 164 | return s.NewToken(token.Comment).Lit(s.readUntil(isEndOfLine)) 165 | } 166 | 167 | // TODO: support unpaired bracket in string 168 | func (s *lexer) scanLuaCode() token.Token { 169 | // used to save the real line and column 170 | ret := s.NewToken(token.LuaCode) 171 | stack := make([]rune, 0, 50) 172 | code := strings.Builder{} 173 | 174 | for { 175 | ch := s.read() 176 | if ch == rune(token.EOF) { 177 | panic("unexpected end of file while scanning a string, maybe an unclosed lua code?") 178 | } 179 | if ch == '#' { 180 | code.WriteRune(ch) 181 | code.WriteString(s.readUntil(isEndOfLine)) 182 | continue 183 | } else if ch == '}' { 184 | if len(stack) == 0 { 185 | // the end of block 186 | _ = s.reader.UnreadRune() 187 | return ret.Lit(code.String()) 188 | } 189 | // maybe it's lua table end, pop stack 190 | if stack[len(stack)-1] == '{' { 191 | stack = stack[0 : len(stack)-1] 192 | } 193 | } else if ch == '{' { 194 | // maybe it's lua table start, push stack 195 | stack = append(stack, ch) 196 | } 197 | code.WriteRune(ch) 198 | } 199 | } 200 | 201 | /* 202 | * 203 | \” – To escape " within double quoted string. 204 | \\ – To escape the backslash. 205 | \n – To add line breaks between string. 206 | \t – To add tab space. 207 | \r – For carriage return. 208 | */ 209 | func (s *lexer) scanQuotedString(delimiter rune) token.Token { 210 | var buf bytes.Buffer 211 | tok := s.NewToken(token.QuotedString) 212 | _, _ = buf.WriteRune(s.read()) //consume delimiter 213 | for { 214 | ch := s.read() 215 | 216 | if ch == rune(token.EOF) { 217 | panic("unexpected end of file while scanning a string, maybe an unclosed quote?") 218 | } 219 | 220 | if ch == '\\' && (s.peek() == delimiter) { 221 | buf.WriteRune(ch) // the backslash 222 | buf.WriteRune(s.read()) // the char needed escaping 223 | continue 224 | } 225 | 226 | _, _ = buf.WriteRune(ch) 227 | if ch == delimiter { 228 | break 229 | } 230 | } 231 | 232 | return tok.Lit(buf.String()) 233 | } 234 | 235 | func (s *lexer) scanKeyword() token.Token { 236 | var buf bytes.Buffer 237 | tok := s.NewToken(token.Keyword) 238 | prev := s.read() 239 | buf.WriteRune(prev) 240 | inVarRef := false 241 | 242 | for { 243 | ch := s.peek() 244 | 245 | // Space, semicolon, and file ending definitely end the keyword 246 | if isSpace(ch) || isEOF(ch) || ch == ';' || isEndOfLine(ch) { 247 | break 248 | } 249 | 250 | // Block start character ends the keyword unless we're in a variable reference 251 | if ch == '{' { 252 | if prev == '$' { 253 | // Starting a ${var} variable reference 254 | inVarRef = true 255 | buf.WriteRune(s.read()) // consume '{' 256 | } else if !inVarRef { 257 | // This is a real block start, end the keyword 258 | break 259 | } else { 260 | // Otherwise, just consume it as part of the keyword 261 | buf.WriteRune(s.read()) 262 | } 263 | } else if ch == '}' { 264 | if inVarRef { 265 | // End of the variable reference 266 | inVarRef = false 267 | buf.WriteRune(s.read()) // consume '}' 268 | } else { 269 | // This is a real block end, end the keyword 270 | break 271 | } 272 | } else { 273 | // Any other character is part of the keyword 274 | prev = s.read() 275 | buf.WriteRune(prev) 276 | } 277 | } 278 | 279 | return tok.Lit(buf.String()) 280 | } 281 | 282 | func (s *lexer) read() rune { 283 | ch, _, err := s.reader.ReadRune() 284 | if err != nil { 285 | return rune(token.EOF) 286 | } 287 | 288 | if ch == '\n' { 289 | s.column = 1 290 | s.line++ 291 | } else { 292 | s.column++ 293 | } 294 | return ch 295 | } 296 | 297 | func isQuote(ch rune) bool { 298 | return ch == '"' || ch == '\'' || ch == '`' 299 | } 300 | 301 | func isSpace(ch rune) bool { 302 | return ch == ' ' || ch == '\t' 303 | } 304 | 305 | func isEOF(ch rune) bool { 306 | return ch == rune(token.EOF) 307 | } 308 | 309 | func isEndOfLine(ch rune) bool { 310 | return ch == '\r' || ch == '\n' 311 | } 312 | 313 | func isLuaBlock(t token.Token) bool { 314 | return t.Type == token.Keyword && strings.HasSuffix(t.Literal, "_by_lua_block") 315 | } 316 | -------------------------------------------------------------------------------- /parser/lexer_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "gotest.tools/v3/assert" 8 | 9 | "github.com/tufanbarisyildirim/gonginx/parser/token" 10 | ) 11 | 12 | func TestScanner_Lex(t *testing.T) { 13 | t.Parallel() 14 | conf := ` 15 | server { # simple reverse-proxy 16 | listen 80; 17 | server_name gonginx.com www.gonginx.com; 18 | access_log logs/gonginx.access.log main; 19 | 20 | # serve static files 21 | location ~ ^/(images|javascript|js|css|flash|media|static)/ { 22 | root /var/www/virtual/gonginx/; 23 | fastcgi_param SERVER_SOFTWARE nginx/$nginx_version/$server_name; 24 | expires 30d; 25 | } 26 | 27 | # pass requests for dynamic content 28 | location / { 29 | proxy_pass http://127.0.0.1:8080; 30 | proxy_set_header X-Real-IP $remote_addr; 31 | } 32 | } 33 | include /etc/nginx/conf.d/*.conf; 34 | directive "with a quoted string\t \r\n \\ with some escaped thing s\" good."; 35 | #also cmment right before eof` 36 | actual := lex(conf).all() 37 | 38 | var expect = token.Tokens{ 39 | {Type: token.EndOfLine, Literal: "\n", Line: 1, Column: 0}, 40 | {Type: token.Keyword, Literal: "server", Line: 2, Column: 1}, 41 | {Type: token.BlockStart, Literal: "{", Line: 2, Column: 8}, 42 | {Type: token.Comment, Literal: "# simple reverse-proxy", Line: 2, Column: 10}, 43 | {Type: token.EndOfLine, Literal: "\n", Line: 2, Column: 32}, 44 | {Type: token.Keyword, Literal: "listen", Line: 3, Column: 5}, 45 | {Type: token.Keyword, Literal: "80", Line: 3, Column: 18}, 46 | {Type: token.Semicolon, Literal: ";", Line: 3, Column: 20}, 47 | {Type: token.EndOfLine, Literal: "\n", Line: 3, Column: 21}, 48 | {Type: token.Keyword, Literal: "server_name", Line: 4, Column: 5}, 49 | {Type: token.Keyword, Literal: "gonginx.com", Line: 4, Column: 18}, 50 | {Type: token.Keyword, Literal: "www.gonginx.com", Line: 4, Column: 30}, 51 | {Type: token.Semicolon, Literal: ";", Line: 4, Column: 45}, 52 | {Type: token.EndOfLine, Literal: "\n", Line: 4, Column: 46}, 53 | {Type: token.Keyword, Literal: "access_log", Line: 5, Column: 5}, 54 | {Type: token.Keyword, Literal: "logs/gonginx.access.log", Line: 5, Column: 18}, 55 | {Type: token.Keyword, Literal: "main", Line: 5, Column: 43}, 56 | {Type: token.Semicolon, Literal: ";", Line: 5, Column: 47}, 57 | {Type: token.EndOfLine, Literal: "\n", Line: 5, Column: 48}, 58 | {Type: token.EndOfLine, Literal: "\n", Line: 6, Column: 1}, 59 | {Type: token.Comment, Literal: "# serve static files", Line: 7, Column: 5}, 60 | {Type: token.EndOfLine, Literal: "\n", Line: 7, Column: 25}, 61 | {Type: token.Keyword, Literal: "location", Line: 8, Column: 5}, 62 | {Type: token.Keyword, Literal: "~", Line: 8, Column: 14}, 63 | {Type: token.Keyword, Literal: "^/(images|javascript|js|css|flash|media|static)/", Line: 8, Column: 16}, 64 | {Type: token.BlockStart, Literal: "{", Line: 8, Column: 66}, 65 | {Type: token.EndOfLine, Literal: "\n", Line: 8, Column: 67}, 66 | {Type: token.Keyword, Literal: "root", Line: 9, Column: 4}, 67 | {Type: token.Keyword, Literal: "/var/www/virtual/gonginx/", Line: 9, Column: 12}, 68 | {Type: token.Semicolon, Literal: ";", Line: 9, Column: 37}, 69 | {Type: token.EndOfLine, Literal: "\n", Line: 9, Column: 38}, 70 | {Type: token.Keyword, Literal: "fastcgi_param", Line: 10, Column: 4}, 71 | {Type: token.Keyword, Literal: "SERVER_SOFTWARE", Line: 10, Column: 19}, 72 | {Type: token.Keyword, Literal: "nginx/$nginx_version/$server_name", Line: 10, Column: 38}, 73 | {Type: token.Semicolon, Literal: ";", Line: 10, Column: 71}, 74 | {Type: token.EndOfLine, Literal: "\n", Line: 10, Column: 72}, 75 | {Type: token.Keyword, Literal: "expires", Line: 11, Column: 7}, 76 | {Type: token.Keyword, Literal: "30d", Line: 11, Column: 15}, 77 | {Type: token.Semicolon, Literal: ";", Line: 11, Column: 18}, 78 | {Type: token.EndOfLine, Literal: "\n", Line: 11, Column: 19}, 79 | {Type: token.BlockEnd, Literal: "}", Line: 12, Column: 5}, 80 | {Type: token.EndOfLine, Literal: "\n", Line: 12, Column: 6}, 81 | {Type: token.EndOfLine, Literal: "\n", Line: 13, Column: 1}, 82 | {Type: token.Comment, Literal: "# pass requests for dynamic content", Line: 14, Column: 5}, 83 | {Type: token.EndOfLine, Literal: "\n", Line: 14, Column: 40}, 84 | {Type: token.Keyword, Literal: "location", Line: 15, Column: 5}, 85 | {Type: token.Keyword, Literal: "/", Line: 15, Column: 14}, 86 | {Type: token.BlockStart, Literal: "{", Line: 15, Column: 16}, 87 | {Type: token.EndOfLine, Literal: "\n", Line: 15, Column: 17}, 88 | {Type: token.Keyword, Literal: "proxy_pass", Line: 16, Column: 7}, 89 | {Type: token.Keyword, Literal: "http://127.0.0.1:8080", Line: 16, Column: 23}, 90 | {Type: token.Semicolon, Literal: ";", Line: 16, Column: 44}, 91 | {Type: token.EndOfLine, Literal: "\n", Line: 16, Column: 45}, 92 | {Type: token.Keyword, Literal: "proxy_set_header", Line: 17, Column: 7}, 93 | {Type: token.Keyword, Literal: "X-Real-IP", Line: 17, Column: 26}, 94 | {Type: token.Keyword, Literal: "$remote_addr", Line: 17, Column: 43}, 95 | {Type: token.Semicolon, Literal: ";", Line: 17, Column: 55}, 96 | {Type: token.EndOfLine, Literal: "\n", Line: 17, Column: 56}, 97 | {Type: token.BlockEnd, Literal: "}", Line: 18, Column: 5}, 98 | {Type: token.EndOfLine, Literal: "\n", Line: 18, Column: 6}, 99 | {Type: token.BlockEnd, Literal: "}", Line: 19, Column: 3}, 100 | {Type: token.EndOfLine, Literal: "\n", Line: 19, Column: 4}, 101 | {Type: token.Keyword, Literal: "include", Line: 20, Column: 1}, 102 | {Type: token.Keyword, Literal: "/etc/nginx/conf.d/*.conf", Line: 20, Column: 9}, 103 | {Type: token.Semicolon, Literal: ";", Line: 20, Column: 33}, 104 | {Type: token.EndOfLine, Literal: "\n", Line: 20, Column: 34}, 105 | {Type: token.Keyword, Literal: "directive", Line: 21, Column: 1}, 106 | {Type: token.QuotedString, Literal: "\"with a quoted string\\t \\r\\n \\\\ with some escaped thing s\\\" good.\"", Line: 21, Column: 11}, 107 | {Type: token.Semicolon, Literal: ";", Line: 21, Column: 77}, 108 | {Type: token.EndOfLine, Literal: "\n", Line: 21, Column: 78}, 109 | {Type: token.Comment, Literal: "#also cmment right before eof", Line: 22, Column: 1}, 110 | } 111 | //assert.Equal(t, actual, 1) 112 | tokenString, err := json.Marshal(actual) 113 | assert.NilError(t, err) 114 | expectJSON, err := json.Marshal(expect) 115 | assert.NilError(t, err) 116 | 117 | assert.NilError(t, actual.Diff(expect)) 118 | 119 | //assert.Assert(t, tokens, 1) 120 | assert.Equal(t, string(tokenString), string(expectJSON)) 121 | assert.Assert(t, actual.EqualTo(expect)) 122 | assert.Equal(t, len(actual), len(expect)) 123 | } 124 | 125 | func TestScanner_LexPanicUnclosedQuote(t *testing.T) { 126 | t.Parallel() 127 | defer func() { 128 | if r := recover(); r == nil { 129 | t.Errorf("The code did not panic") 130 | } 131 | }() 132 | 133 | lex(` 134 | server { 135 | directive "with an unclosed quote \t \r\n \\ with some escaped thing s\" good.; 136 | `).all() 137 | } 138 | 139 | func TestScanner_LexLuaCode(t *testing.T) { 140 | conf := ` 141 | server { 142 | location = /foo { 143 | rewrite_by_lua_block { 144 | res = ngx.location.capture("/memc", 145 | { args = { cmd = "incr", key = ngx.var.uri } } # comment contained unexpect '{' 146 | # comment contained unexpect '}' 147 | ) 148 | t = { key="foo", val="bar" } 149 | } 150 | } 151 | }` 152 | actual := lex(conf).all() 153 | var expect = token.Tokens{ 154 | {Type: token.EndOfLine, Literal: "\n", Line: 1, Column: 0}, 155 | {Type: token.Keyword, Literal: "server", Line: 2, Column: 1}, 156 | {Type: token.BlockStart, Literal: "{", Line: 2, Column: 8}, 157 | {Type: token.EndOfLine, Literal: "\n", Line: 2, Column: 9}, 158 | {Type: token.Keyword, Literal: "location", Line: 3, Column: 3}, 159 | {Type: token.Keyword, Literal: "=", Line: 3, Column: 12}, 160 | {Type: token.Keyword, Literal: "/foo", Line: 3, Column: 14}, 161 | {Type: token.BlockStart, Literal: "{", Line: 3, Column: 19}, 162 | {Type: token.EndOfLine, Literal: "\n", Line: 3, Column: 20}, 163 | {Type: token.Keyword, Literal: "rewrite_by_lua_block", Line: 4, Column: 5}, 164 | {Type: token.BlockStart, Literal: "{", Line: 4, Column: 26}, 165 | {Type: token.LuaCode, Literal: ` 166 | res = ngx.location.capture("/memc", 167 | { args = { cmd = "incr", key = ngx.var.uri } } # comment contained unexpect '{' 168 | # comment contained unexpect '}' 169 | ) 170 | t = { key="foo", val="bar" } 171 | `, Line: 4, Column: 27}, 172 | {Type: token.BlockEnd, Literal: "}", Line: 10, Column: 6}, 173 | {Type: token.EndOfLine, Literal: "\n", Line: 10, Column: 7}, 174 | {Type: token.BlockEnd, Literal: "}", Line: 11, Column: 3}, 175 | {Type: token.EndOfLine, Literal: "\n", Line: 11, Column: 4}, 176 | {Type: token.BlockEnd, Literal: "}", Line: 12, Column: 1}, 177 | } 178 | tokenString, err := json.Marshal(actual) 179 | assert.NilError(t, err) 180 | expectJSON, err := json.Marshal(expect) 181 | assert.NilError(t, err) 182 | assert.NilError(t, actual.Diff(expect)) 183 | assert.Equal(t, string(tokenString), string(expectJSON)) 184 | assert.Equal(t, len(actual), len(expect)) 185 | } 186 | -------------------------------------------------------------------------------- /parser/token/token_test.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestType_String(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | tt Type 13 | want string 14 | }{ 15 | { 16 | name: "QuotedString", 17 | tt: QuotedString, 18 | want: "QuotedString", 19 | }, 20 | { 21 | name: "Eof", 22 | tt: EOF, 23 | want: "Eof", 24 | }, 25 | { 26 | name: "Keyword", 27 | tt: Keyword, 28 | want: "Keyword", 29 | }, 30 | } 31 | for _, tt := range tests { 32 | tt2 := tt 33 | t.Run(tt.name, func(t *testing.T) { 34 | t.Parallel() 35 | if got := tt2.tt.String(); got != tt2.want { 36 | t.Errorf("Type.String() = %v, want %v", got, tt2.want) 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func TestToken_String(t *testing.T) { 43 | type fields struct { 44 | Type Type 45 | Literal string 46 | Line int 47 | Column int 48 | } 49 | tests := []struct { 50 | name string 51 | fields fields 52 | want string 53 | }{ 54 | { 55 | name: "serialize quoted string", 56 | fields: fields{ 57 | Type: QuotedString, 58 | Literal: "my test string", 59 | Line: 0, 60 | Column: 0, 61 | }, 62 | want: fmt.Sprintf("{Type:%s,Literal:\"%s\",Line:%d,Column:%d}", "QuotedString", "my test string", 0, 0), 63 | }, 64 | } 65 | for _, tt := range tests { 66 | tt2 := tt 67 | t.Run(tt.name, func(t *testing.T) { 68 | t.Parallel() 69 | tok := Token{ 70 | Type: tt2.fields.Type, 71 | Literal: tt2.fields.Literal, 72 | Line: tt2.fields.Line, 73 | Column: tt2.fields.Column, 74 | } 75 | if got := tok.String(); got != tt2.want { 76 | t.Errorf("Token.String() = %v, want %v", got, tt2.want) 77 | } 78 | }) 79 | } 80 | } 81 | 82 | func TestToken_Lit(t *testing.T) { 83 | type fields struct { 84 | Type Type 85 | Literal string 86 | Line int 87 | Column int 88 | } 89 | type args struct { 90 | literal string 91 | } 92 | tests := []struct { 93 | name string 94 | fields fields 95 | args args 96 | want Token 97 | }{ 98 | { 99 | name: "a string literal", 100 | fields: fields{ 101 | Type: QuotedString, 102 | Literal: "a test string", 103 | Line: 0, 104 | Column: 0, 105 | }, 106 | args: args{ 107 | literal: "new test string", 108 | }, 109 | want: Token{ 110 | Type: QuotedString, 111 | Literal: "new test string", 112 | Line: 0, 113 | Column: 0, 114 | }, 115 | }, 116 | } 117 | for _, tt := range tests { 118 | tt2 := tt 119 | t.Run(tt.name, func(t *testing.T) { 120 | t.Parallel() 121 | tok := Token{ 122 | Type: tt2.fields.Type, 123 | Literal: tt2.fields.Literal, 124 | Line: tt2.fields.Line, 125 | Column: tt2.fields.Column, 126 | } 127 | if got := tok.Lit(tt2.args.literal); !reflect.DeepEqual(got, tt2.want) { 128 | t.Errorf("Token.Lit() = %v, want %v", got, tt2.want) 129 | } 130 | }) 131 | } 132 | } 133 | 134 | func TestToken_EqualTo(t *testing.T) { 135 | 136 | tests := []struct { 137 | name string 138 | tok1 Token 139 | tok2 Token 140 | want bool 141 | }{ 142 | { 143 | name: "keyword is keyword", 144 | tok1: Token{ 145 | Type: Keyword, 146 | Literal: "server", 147 | }, 148 | tok2: Token{ 149 | Type: Keyword, 150 | Literal: "server", 151 | }, 152 | want: true, 153 | }, 154 | { 155 | name: "keyword is keyword but needs same directive", 156 | tok1: Token{ 157 | Type: Keyword, 158 | Literal: "server", 159 | }, 160 | tok2: Token{ 161 | Type: Keyword, 162 | Literal: "location", 163 | }, 164 | want: false, 165 | }, { 166 | name: "string is string", 167 | tok1: Token{ 168 | Type: QuotedString, 169 | Literal: "same quoted strings", 170 | }, 171 | tok2: Token{ 172 | Type: QuotedString, 173 | Literal: "same quoted strings", 174 | }, 175 | want: true, 176 | }, 177 | { 178 | name: "Blockstart is Blockstart even if they are in different lines", 179 | tok1: Token{ 180 | Type: BlockStart, 181 | Literal: "{", 182 | Line: 1, 183 | }, 184 | tok2: Token{ 185 | Type: BlockStart, 186 | Literal: "{", 187 | Line: 2, 188 | }, 189 | want: true, 190 | }, 191 | } 192 | for _, tt := range tests { 193 | tt2 := tt 194 | t.Run(tt2.name, func(t *testing.T) { 195 | if got := tt2.tok1.EqualTo(tt2.tok2); got != tt2.want { 196 | t.Errorf("Token.EqualTo() = %v, want %v", got, tt2.want) 197 | } 198 | }) 199 | } 200 | } 201 | 202 | func TestTokens_EqualTo(t *testing.T) { 203 | type args struct { 204 | tokens Tokens 205 | } 206 | tests := []struct { 207 | name string 208 | ts Tokens 209 | args args 210 | want bool 211 | }{ 212 | { 213 | name: "token array matching", 214 | ts: Tokens{ 215 | {Type: Keyword, Literal: "server", Line: 2, Column: 1}, 216 | {Type: BlockStart, Literal: "{", Line: 2, Column: 8}, 217 | {Type: Comment, Literal: "# simple reverse-proxy", Line: 2, Column: 10}, 218 | {Type: BlockEnd, Literal: "}", Line: 3, Column: 5}, 219 | }, 220 | args: args{ 221 | tokens: Tokens{ 222 | {Type: Keyword, Literal: "server", Line: 2, Column: 1}, 223 | {Type: BlockStart, Literal: "{", Line: 2, Column: 8}, 224 | {Type: Comment, Literal: "# simple reverse-proxy", Line: 2, Column: 10}, 225 | {Type: BlockEnd, Literal: "}", Line: 3, Column: 5}, 226 | }, 227 | }, 228 | want: true, 229 | }, 230 | { 231 | name: "token array matching", 232 | ts: Tokens{ 233 | {Type: Keyword, Literal: "server", Line: 2, Column: 1}, 234 | {Type: BlockStart, Literal: "{", Line: 2, Column: 8}, 235 | {Type: Comment, Literal: "# simple reverse-proxy", Line: 2, Column: 10}, 236 | {Type: BlockEnd, Literal: "}", Line: 3, Column: 5}, 237 | }, 238 | args: args{ 239 | tokens: Tokens{ 240 | {Type: Keyword, Literal: "server", Line: 2, Column: 1}, 241 | {Type: BlockStart, Literal: "{", Line: 2, Column: 8}, 242 | {Type: Comment, Literal: "# simple reverse-proxy", Line: 2, Column: 10}, 243 | }, 244 | }, 245 | want: false, 246 | }, 247 | { 248 | name: "token array matching", 249 | ts: Tokens{ 250 | {Type: Keyword, Literal: "server", Line: 2, Column: 1}, 251 | {Type: BlockStart, Literal: "{", Line: 2, Column: 8}, 252 | {Type: Comment, Literal: "# simple reverse-proxy", Line: 2, Column: 10}, 253 | {Type: BlockEnd, Literal: "}", Line: 3, Column: 5}, 254 | }, 255 | args: args{ 256 | tokens: Tokens{ 257 | {Type: Keyword, Literal: "server", Line: 2, Column: 1}, 258 | {Type: BlockStart, Literal: "{", Line: 2, Column: 8}, 259 | {Type: QuotedString, Literal: "simple reverse-proxy", Line: 2, Column: 10}, 260 | {Type: BlockEnd, Literal: "}", Line: 3, Column: 5}, 261 | }, 262 | }, 263 | want: false, 264 | }, 265 | } 266 | for _, tt := range tests { 267 | t.Run(tt.name, func(t *testing.T) { 268 | if got := tt.ts.EqualTo(tt.args.tokens); got != tt.want { 269 | t.Errorf("Tokens.EqualTo() = %v, want %v", got, tt.want) 270 | } 271 | }) 272 | } 273 | } 274 | 275 | func TestToken_Is(t *testing.T) { 276 | type fields struct { 277 | Type Type 278 | Literal string 279 | Line int 280 | Column int 281 | } 282 | type args struct { 283 | typ Type 284 | } 285 | tests := []struct { 286 | name string 287 | fields fields 288 | args args 289 | want bool 290 | }{ 291 | { 292 | name: "QuotedString Type", 293 | fields: fields{ 294 | Type: QuotedString, 295 | Literal: "hello", 296 | }, 297 | args: args{ 298 | typ: QuotedString, 299 | }, 300 | want: true, 301 | }, 302 | { 303 | name: "QuotedString Type", 304 | fields: fields{ 305 | Type: QuotedString, 306 | Literal: "hello", 307 | }, 308 | args: args{ 309 | typ: Keyword, 310 | }, 311 | want: false, 312 | }, 313 | } 314 | for _, tt := range tests { 315 | t.Run(tt.name, func(t *testing.T) { 316 | tok := Token{ 317 | Type: tt.fields.Type, 318 | Literal: tt.fields.Literal, 319 | Line: tt.fields.Line, 320 | Column: tt.fields.Column, 321 | } 322 | if got := tok.Is(tt.args.typ); got != tt.want { 323 | t.Errorf("Token.Is() = %v, want %v", got, tt.want) 324 | } 325 | }) 326 | } 327 | } 328 | 329 | func TestToken_IsParameterEligible(t *testing.T) { 330 | 331 | tests := []struct { 332 | name string 333 | token Token 334 | want bool 335 | }{ 336 | { 337 | name: "Keyword can be a parameter", 338 | token: Token{ 339 | Type: Keyword, 340 | }, 341 | want: true, 342 | }, 343 | { 344 | name: "Variable can be a parameter", 345 | token: Token{ 346 | Type: Variable, 347 | }, 348 | want: true, 349 | }, 350 | { 351 | name: "Quoted string can be a parameter", 352 | token: Token{ 353 | Type: QuotedString, 354 | }, 355 | want: true, 356 | }, 357 | { 358 | name: "Quoted string can be a parameter", 359 | token: Token{ 360 | Type: QuotedString, 361 | }, 362 | want: true, 363 | }, 364 | { 365 | name: "Regex string can be a parameter", 366 | token: Token{ 367 | Type: Regex, 368 | }, 369 | want: true, 370 | }, 371 | { 372 | name: "Blockstart cant can be a parameter", 373 | token: Token{ 374 | Type: BlockStart, 375 | }, 376 | want: false, 377 | }, 378 | { 379 | name: "Blockend cant can be a parameter", 380 | token: Token{ 381 | Type: BlockEnd, 382 | }, 383 | want: false, 384 | }, 385 | { 386 | name: "Comment cant can be a parameter", 387 | token: Token{ 388 | Type: Comment, 389 | }, 390 | want: false, 391 | }, 392 | } 393 | for _, tt := range tests { 394 | t.Run(tt.name, func(t *testing.T) { 395 | if got := tt.token.IsParameterEligible(); got != tt.want { 396 | t.Errorf("Token.IsParameterEligible() = %v, want %v", got, tt.want) 397 | } 398 | }) 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/tufanbarisyildirim/gonginx/config" 12 | "github.com/tufanbarisyildirim/gonginx/parser/token" 13 | ) 14 | 15 | // Option parsing option 16 | type Option func(*Parser) 17 | 18 | type options struct { 19 | parseInclude bool 20 | skipIncludeParsingErr bool 21 | skipComments bool 22 | customDirectives map[string]string 23 | skipValidSubDirectiveBlock map[string]struct{} 24 | skipValidDirectivesErr bool 25 | } 26 | 27 | func defaultOptions() options { 28 | return options{ 29 | parseInclude: false, 30 | skipIncludeParsingErr: false, 31 | skipComments: false, 32 | customDirectives: map[string]string{}, 33 | skipValidSubDirectiveBlock: map[string]struct{}{}, 34 | skipValidDirectivesErr: false, 35 | } 36 | } 37 | 38 | // Parser is an nginx config parser 39 | type Parser struct { 40 | opts options 41 | configRoot string // TODO: confirmation needed (whether this is the parent of nginx.conf) 42 | lexer *lexer 43 | currentToken token.Token 44 | followingToken token.Token 45 | parsedIncludes map[*config.Include]*config.Config 46 | statementParsers map[string]func() (config.IDirective, error) 47 | blockWrappers map[string]func(*config.Directive) (config.IDirective, error) 48 | directiveWrappers map[string]func(*config.Directive) (config.IDirective, error) 49 | includeWrappers map[string]func(*config.Directive) (config.IDirective, error) 50 | 51 | commentBuffer []string 52 | file *os.File 53 | } 54 | 55 | // WithSameOptions copy options from another parser 56 | func WithSameOptions(p *Parser) Option { 57 | return func(curr *Parser) { 58 | curr.opts = p.opts 59 | } 60 | } 61 | 62 | func withParsedIncludes(parsedIncludes map[*config.Include]*config.Config) Option { 63 | return func(p *Parser) { 64 | p.parsedIncludes = parsedIncludes 65 | } 66 | } 67 | 68 | func withConfigRoot(configRoot string) Option { 69 | return func(p *Parser) { 70 | p.configRoot = configRoot 71 | } 72 | } 73 | 74 | // WithSkipIncludeParsingErr ignores include parsing errors 75 | func WithSkipIncludeParsingErr() Option { 76 | return func(p *Parser) { 77 | p.opts.skipIncludeParsingErr = true 78 | } 79 | } 80 | 81 | // WithDefaultOptions default options 82 | func WithDefaultOptions() Option { 83 | return func(p *Parser) { 84 | p.opts = defaultOptions() 85 | } 86 | } 87 | 88 | // WithSkipComments default options 89 | func WithSkipComments() Option { 90 | return func(p *Parser) { 91 | p.opts.skipComments = true 92 | } 93 | } 94 | 95 | // WithIncludeParsing enable parsing included files 96 | func WithIncludeParsing() Option { 97 | return func(p *Parser) { 98 | p.opts.parseInclude = true 99 | } 100 | } 101 | 102 | // WithCustomDirectives add your custom directives as valid directives 103 | func WithCustomDirectives(directives ...string) Option { 104 | return func(p *Parser) { 105 | for _, directive := range directives { 106 | p.opts.customDirectives[directive] = directive 107 | } 108 | } 109 | } 110 | 111 | // WithSkipValidBlocks add your custom block as valid 112 | func WithSkipValidBlocks(directives ...string) Option { 113 | return func(p *Parser) { 114 | for _, directive := range directives { 115 | p.opts.skipValidSubDirectiveBlock[directive] = struct{}{} 116 | } 117 | } 118 | } 119 | 120 | // WithSkipValidDirectivesErr ignores unknown directive errors 121 | func WithSkipValidDirectivesErr() Option { 122 | return func(p *Parser) { 123 | p.opts.skipValidDirectivesErr = true 124 | } 125 | } 126 | 127 | // NewStringParser parses nginx conf from string 128 | func NewStringParser(str string, opts ...Option) *Parser { 129 | return NewParserFromLexer(lex(str), opts...) 130 | } 131 | 132 | // NewParser create new parser 133 | func NewParser(filePath string, opts ...Option) (*Parser, error) { 134 | f, err := os.Open(filePath) 135 | if err != nil { 136 | return nil, err 137 | } 138 | l := newLexer(bufio.NewReader(f)) 139 | l.file = filePath 140 | p := NewParserFromLexer(l, opts...) 141 | p.file = f 142 | return p, nil 143 | } 144 | 145 | // NewParserFromLexer initilizes a new Parser 146 | func NewParserFromLexer(lexer *lexer, opts ...Option) *Parser { 147 | configRoot, _ := filepath.Split(lexer.file) 148 | parser := &Parser{ 149 | lexer: lexer, 150 | opts: defaultOptions(), 151 | parsedIncludes: make(map[*config.Include]*config.Config), 152 | configRoot: configRoot, 153 | } 154 | 155 | for _, o := range opts { 156 | o(parser) 157 | } 158 | 159 | parser.nextToken() 160 | parser.nextToken() 161 | 162 | parser.blockWrappers = config.BlockWrappers 163 | parser.directiveWrappers = config.DirectiveWrappers 164 | parser.includeWrappers = config.IncludeWrappers 165 | return parser 166 | } 167 | 168 | func (p *Parser) nextToken() { 169 | p.currentToken = p.followingToken 170 | p.followingToken = p.lexer.scan() 171 | } 172 | 173 | func (p *Parser) curTokenIs(t token.Type) bool { 174 | return p.currentToken.Type == t 175 | } 176 | 177 | func (p *Parser) followingTokenIs(t token.Type) bool { 178 | return p.followingToken.Type == t 179 | } 180 | 181 | // Parse the gonginx. 182 | func (p *Parser) Parse() (*config.Config, error) { 183 | parsedBlock, err := p.parseBlock(false, false) 184 | if err != nil { 185 | return nil, err 186 | } 187 | c := &config.Config{ 188 | FilePath: p.lexer.file, //TODO: set filepath here, 189 | Block: parsedBlock, 190 | } 191 | err = p.Close() 192 | return c, err 193 | } 194 | 195 | // ParseBlock parse a block statement 196 | func (p *Parser) parseBlock(inBlock bool, isSkipValidDirective bool) (*config.Block, error) { 197 | 198 | context := &config.Block{ 199 | Directives: make([]config.IDirective, 0), 200 | } 201 | var s config.IDirective 202 | var err error 203 | var line int 204 | parsingLoop: 205 | for { 206 | switch { 207 | case p.curTokenIs(token.EOF): 208 | if inBlock { 209 | return nil, errors.New("unexpected eof in block") 210 | } 211 | break parsingLoop 212 | case p.curTokenIs(token.LuaCode): 213 | context.IsLuaBlock = true 214 | context.LiteralCode = p.currentToken.Literal 215 | case p.curTokenIs(token.BlockEnd): 216 | break parsingLoop 217 | case p.curTokenIs(token.Keyword) || p.curTokenIs(token.QuotedString): 218 | s, err = p.parseStatement(isSkipValidDirective) 219 | if err != nil { 220 | return nil, err 221 | } 222 | if s.GetBlock() == nil { 223 | s.SetParent(s) 224 | } else { 225 | // each directive should have a parent directive, not a block 226 | // find each directive in the block and set the parent directive 227 | b := s.GetBlock() 228 | for _, dir := range b.GetDirectives() { 229 | dir.SetParent(s) 230 | } 231 | } 232 | line = p.currentToken.Line 233 | s.SetLine(line) 234 | context.Directives = append(context.Directives, s) 235 | case p.curTokenIs(token.Comment): 236 | if p.opts.skipComments { 237 | break 238 | } 239 | // outline comment 240 | p.commentBuffer = append(p.commentBuffer, p.currentToken.Literal) 241 | } 242 | p.nextToken() 243 | } 244 | 245 | return context, nil 246 | } 247 | 248 | func (p *Parser) parseStatement(isSkipValidDirective bool) (config.IDirective, error) { 249 | d := &config.Directive{ 250 | Name: p.currentToken.Literal, 251 | } 252 | 253 | if !p.opts.skipValidDirectivesErr && !isSkipValidDirective { 254 | _, ok := ValidDirectives[d.Name] 255 | _, ok2 := p.opts.customDirectives[d.Name] 256 | 257 | if !ok && !ok2 { 258 | return nil, fmt.Errorf("unknown directive '%s' on line %d, column %d", d.Name, p.currentToken.Line, p.currentToken.Column) 259 | } 260 | } 261 | 262 | //if we have a special parser for the directive, we use it. 263 | if sp, ok := p.statementParsers[d.Name]; ok { 264 | return sp() 265 | } 266 | 267 | // set outline comment 268 | if len(p.commentBuffer) > 0 { 269 | d.Comment = p.commentBuffer 270 | p.commentBuffer = make([]string, 0) 271 | } 272 | 273 | directiveLineIndex := p.currentToken.Line // keep track of the line index of the directive 274 | // Parse parameters until reaching the semicolon that ends the directive. 275 | for { 276 | p.nextToken() 277 | if p.currentToken.IsParameterEligible() { 278 | d.Parameters = append(d.Parameters, config.Parameter{ 279 | Value: p.currentToken.Literal, 280 | RelativeLineIndex: p.currentToken.Line - directiveLineIndex}) // save the relative line index of the parameter 281 | if p.currentToken.Is(token.BlockEnd) { 282 | return d, nil 283 | } 284 | } else if p.curTokenIs(token.Semicolon) { 285 | // inline comment in following token 286 | if !p.opts.skipComments { 287 | if p.followingTokenIs(token.Comment) && p.followingToken.Line == p.currentToken.Line { 288 | // if following token is a comment, then it is an inline comment, fetch next token 289 | p.nextToken() 290 | d.SetInlineComment(config.InlineComment{ 291 | Value: p.currentToken.Literal, 292 | RelativeLineIndex: p.currentToken.Line - directiveLineIndex, 293 | }) 294 | } 295 | } 296 | if iw, ok := p.includeWrappers[d.Name]; ok { 297 | include, err := iw(d) 298 | if err != nil { 299 | return nil, err 300 | } 301 | return p.ParseInclude(include.(*config.Include)) 302 | } else if dw, ok := p.directiveWrappers[d.Name]; ok { 303 | return dw(d) 304 | } 305 | return d, nil 306 | } else if p.curTokenIs(token.Comment) { 307 | // param comment 308 | d.SetInlineComment(config.InlineComment{ 309 | Value: p.currentToken.Literal, 310 | RelativeLineIndex: p.currentToken.Line - directiveLineIndex, 311 | }) 312 | } else if p.curTokenIs(token.BlockStart) { 313 | _, blockSkip1 := SkipValidBlocks[d.Name] 314 | _, blockSkip2 := p.opts.skipValidSubDirectiveBlock[d.Name] 315 | isSkipBlockSubDirective := blockSkip1 || blockSkip2 || isSkipValidDirective 316 | 317 | // Special handling for *_by_lua_block directives 318 | if strings.HasSuffix(d.Name, "_by_lua_block") { 319 | // For Lua blocks, we need to capture the content without parsing it as nginx directives 320 | b := &config.Block{ 321 | IsLuaBlock: true, 322 | Directives: []config.IDirective{}, 323 | LiteralCode: "", 324 | } 325 | 326 | // Skip past the opening brace 327 | p.nextToken() 328 | 329 | // Collect all content until the matching closing brace 330 | // We need to count braces to handle nested blocks within Lua code 331 | braceCount := 1 332 | var luaCode strings.Builder 333 | 334 | for braceCount > 0 && !p.curTokenIs(token.EOF) { 335 | if p.curTokenIs(token.BlockStart) { 336 | braceCount++ 337 | } else if p.curTokenIs(token.BlockEnd) { 338 | braceCount-- 339 | if braceCount == 0 { 340 | // This is the closing brace of the Lua block 341 | break 342 | } 343 | } 344 | 345 | // Append token to Lua code if it's not the closing brace 346 | if !(p.curTokenIs(token.BlockEnd) && braceCount == 0) { 347 | luaCode.WriteString(p.currentToken.Literal) 348 | // Add space between tokens for readability 349 | if p.followingToken.Type != token.BlockEnd && 350 | p.followingToken.Type != token.Semicolon && 351 | p.followingToken.Type != token.EndOfLine { 352 | luaCode.WriteString(" ") 353 | } 354 | } 355 | 356 | p.nextToken() 357 | } 358 | 359 | b.LiteralCode = strings.TrimSpace(luaCode.String()) 360 | d.Block = b 361 | 362 | // Use the appropriate wrapper based on the directive name 363 | if strings.HasSuffix(d.Name, "_by_lua_block") { 364 | return p.blockWrappers["_by_lua_block"](d) 365 | } 366 | return d, nil 367 | } 368 | 369 | b, err := p.parseBlock(true, isSkipBlockSubDirective) 370 | if err != nil { 371 | return nil, err 372 | } 373 | d.Block = b 374 | 375 | if bw, ok := p.blockWrappers[d.Name]; ok { 376 | return bw(d) 377 | } 378 | return d, nil 379 | } else if p.currentToken.Is(token.EndOfLine) { 380 | continue 381 | } else { 382 | return nil, fmt.Errorf("unexpected token %s (%s) on line %d, column %d", p.currentToken.Type.String(), p.currentToken.Literal, p.currentToken.Line, p.currentToken.Column) 383 | } 384 | } 385 | } 386 | 387 | // ParseInclude just parse include confs 388 | func (p *Parser) ParseInclude(include *config.Include) (config.IDirective, error) { 389 | if p.opts.parseInclude { 390 | includePath := include.IncludePath 391 | if !filepath.IsAbs(includePath) { 392 | includePath = filepath.Join(p.configRoot, include.IncludePath) 393 | } 394 | includePaths, err := filepath.Glob(includePath) 395 | if err != nil && !p.opts.skipIncludeParsingErr { 396 | return nil, err 397 | } 398 | for _, includePath := range includePaths { 399 | if conf, ok := p.parsedIncludes[include]; ok { 400 | // same file includes itself? don't blow up the parser 401 | if conf == nil { 402 | continue 403 | } 404 | } else { 405 | p.parsedIncludes[include] = nil 406 | } 407 | 408 | parser, err := NewParser(includePath, 409 | WithSameOptions(p), 410 | withParsedIncludes(p.parsedIncludes), 411 | withConfigRoot(p.configRoot), 412 | ) 413 | 414 | if err != nil { 415 | if p.opts.skipIncludeParsingErr { 416 | continue 417 | } 418 | return nil, err 419 | } 420 | 421 | config, err := parser.Parse() 422 | if err != nil { 423 | return nil, err 424 | } 425 | //TODO: link parent config or include direcitve? 426 | p.parsedIncludes[include] = config 427 | include.Configs = append(include.Configs, config) 428 | } 429 | } 430 | return include, nil 431 | } 432 | 433 | // Close closes the file handler and releases the resources 434 | func (p *Parser) Close() (err error) { 435 | if p.file != nil { 436 | err = p.file.Close() 437 | } 438 | return err 439 | } 440 | -------------------------------------------------------------------------------- /GUIDE.md: -------------------------------------------------------------------------------- 1 | # Gonginx Guide 2 | 3 | This guide explains how to parse, modify and regenerate Nginx configuration 4 | files with Gonginx. The [examples](./examples) directory contains runnable 5 | programs showing each feature. 6 | 7 | ## Table of Contents 8 | - [Quick Start](#quick-start) 9 | - [Tips for Coding Agents](#tips-for-coding-agents) 10 | - [Examples](#examples) 11 | - [Library Reference](#library-reference) 12 | 13 | ## Quick Start 14 | You can find all examples in [examples](./examples). 15 | ### Parse nginx config file 16 | Parse Nginx config file, Get server listen port 17 | ```go 18 | func parseConfigAndGetPorts(filePath string) ([]string, error) { 19 | p, err := parser.NewParser(filePath) 20 | if err != nil { 21 | return nil, fmt.Errorf("failed to create parser: %w", err) 22 | } 23 | conf, err := p.Parse() 24 | if err != nil { 25 | return nil, fmt.Errorf("failed to parse config: %w", err) 26 | } 27 | servers := conf.FindDirectives("server") 28 | ports := make([]string, 0) 29 | for _, server := range servers { 30 | listens := server.GetBlock().FindDirectives("listen") 31 | if len(listens) > 0 { 32 | listenPorts := listens[0].GetParameters() 33 | ports = append(ports, listenPorts...) 34 | } 35 | } 36 | return ports, nil 37 | } 38 | func main() { 39 | ports, err := parseConfigAndGetPorts("../../testdata/full_conf/nginx.conf") 40 | if err != nil { 41 | panic(err) 42 | } 43 | fmt.Println(ports) 44 | } 45 | ``` 46 | 47 | ### Dump nginx config string to file with indent 48 | ```go 49 | func dumpConfigToFile(fullConf string, filePath string) error { 50 | p := parser.NewStringParser(fullConf) 51 | conf, err := p.Parse() 52 | if err != nil { 53 | return fmt.Errorf("failed to parse config: %w", err) 54 | } 55 | 56 | dumpString := dumper.DumpConfig(conf, dumper.IndentedStyle) 57 | if err := os.WriteFile(filePath, []byte(dumpString), 0644); err != nil { 58 | return fmt.Errorf("failed to write config file: %w", err) 59 | } 60 | return nil 61 | } 62 | 63 | func dumpAndWriteConfigFile(fullConf string, filePath string) error { 64 | p := parser.NewStringParser(fullConf) 65 | conf, err := p.Parse() 66 | if err != nil { 67 | return fmt.Errorf("failed to parse config: %w", err) 68 | } 69 | // set config file path 70 | conf.FilePath = filePath 71 | err = dumper.WriteConfig(conf, dumper.IndentedStyle, false) 72 | if err != nil { 73 | panic(err) 74 | } 75 | return nil 76 | } 77 | 78 | func main() { 79 | fullConf := `user www www; 80 | worker_processes 5; 81 | error_log logs/error.log; 82 | pid logs/nginx.pid; 83 | worker_rlimit_nofile 8192; 84 | events { worker_connections 4096; } http { 85 | include mime.types; 86 | include proxy.conf; 87 | include fastcgi.conf; 88 | index index.html index.htm index.php; 89 | default_type application/octet-stream; 90 | log_format main '$remote_addr - $remote_user [$time_local] $status ' 91 | '"$request" $body_bytes_sent "$http_referer" ' 92 | ' "$http_user_agent" "$http_x_forwarded_for"'; 93 | access_log logs/access.log main; 94 | sendfile on; 95 | tcp_nopush on; 96 | server_names_hash_bucket_size 128; 97 | server { 98 | listen 80; 99 | server_name domain1.com www.domain1.com; 100 | access_log logs/domain1.access.log main; 101 | root html; 102 | location ~ \.php$ { 103 | fastcgi_pass 127.0.0.1:1025; } } server { 104 | listen 80; 105 | server_name domain2.com www.domain2.com; 106 | access_log logs/domain2.access.log main; 107 | location ~ ^/(images|javascript|js|css|flash|media|static)/ { 108 | root /var/www/virtual/big.server.com/htdocs; 109 | expires 30d; 110 | } location / { proxy_pass http://127.0.0.1:8080; } } 111 | upstream big_server_com { 112 | server 127.0.0.3:8000 weight=5; 113 | server 127.0.0.3:8001 weight=5; 114 | server 192.168.0.1:8000; 115 | server 192.168.0.1:8001; 116 | } server { listen 80; 117 | server_name big.server.com; 118 | access_log logs/big.server.access.log main; 119 | location / { proxy_pass http://big_server_com; } } }` 120 | 121 | // dump config with indented style 122 | dumpConfigToFile(fullConf, "nginx-temp.conf") 123 | 124 | // dump config to file with indented style 125 | dumpAndWriteConfigFile(fullConf, "./nginx-temp2.conf") 126 | } 127 | ``` 128 | 129 | ## Tips for Coding Agents 130 | The Gonginx API exposes small, composable functions. Prefer operating on 131 | `config.Config` objects and helper methods like `FindDirectives` or `AddServer` 132 | instead of editing raw text. 133 | 134 | ## Examples 135 | 136 | The `examples` directory contains small programs you can run directly. Useful 137 | entries include: 138 | 139 | - `adding-server` – append a server to an upstream block. 140 | - `update-directive` – modify a directive in place. 141 | - `dump-nginx-config` – format and write a full configuration. 142 | - `update-server-listen-port` – change the listen port of an existing server. 143 | 144 | ### Add server in upstream 145 | ```go 146 | func main() { 147 | p := parser.NewStringParser(`http{ 148 | upstream my_backend{ 149 | server 127.0.0.1:443; 150 | server 127.0.0.2:443 backup; 151 | } 152 | }`) 153 | 154 | conf, err := p.Parse() 155 | if err != nil { 156 | panic(err) 157 | } 158 | upstreams := conf.FindUpstreams() 159 | 160 | upstreams[0].AddServer(&config.UpstreamServer{ 161 | Address: "127.0.0.1:443", 162 | Parameters: map[string]string{ 163 | "weight": "5", 164 | }, 165 | Flags: []string{"down"}, 166 | }) 167 | 168 | fmt.Println(dumper.DumpBlock(conf.Block, dumper.IndentedStyle)) 169 | } 170 | ``` 171 | 172 | ### Update directive 173 | 174 | ```go 175 | func main() { 176 | p := parser.NewStringParser(` 177 | user nginx; 178 | worker_processes auto; 179 | error_log /var/log/nginx/error.log; 180 | pid /run/nginx.pid; 181 | 182 | # Load dynamic modules. See /usr/share/doc/nginx/README.dynamic. 183 | include /usr/share/nginx/modules/*.conf; 184 | 185 | events { 186 | worker_connections 1024; 187 | } 188 | 189 | http { 190 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 191 | '$status $body_bytes_sent "$http_referer" ' 192 | '"$http_user_agent" "$http_x_forwarded_for"'; 193 | 194 | access_log /var/log/nginx/access.log main; 195 | 196 | sendfile on; 197 | tcp_nopush on; 198 | tcp_nodelay on; 199 | keepalive_timeout 65; 200 | types_hash_max_size 2048; 201 | 202 | include /etc/nginx/mime.types; 203 | default_type application/octet-stream; 204 | 205 | server { 206 | listen 80 default_server; 207 | listen [::]:80 default_server; 208 | server_name _; 209 | root /usr/share/nginx/html; 210 | 211 | # Load configuration files for the default server block. 212 | include /etc/nginx/default.d/*.conf; 213 | 214 | location / { 215 | proxy_pass http://www.google.com/; 216 | } 217 | 218 | error_page 404 /404.html; 219 | location = /40x.html { 220 | } 221 | 222 | error_page 500 502 503 504 /50x.html; 223 | location = /50x.html { 224 | } 225 | } 226 | 227 | }`) 228 | 229 | c, err := p.Parse() 230 | if err != nil { 231 | panic(err) 232 | } 233 | directives := c.FindDirectives("proxy_pass") 234 | for _, directive := range directives { 235 | fmt.Println("found a proxy_pass : ", directive.GetName(), directive.GetParameters()) 236 | if directive.GetParameters()[0] == "http://www.google.com/" { 237 | directive.GetParameters()[0] = "http://www.duckduckgo.com/" 238 | } 239 | } 240 | 241 | fmt.Println(dumper.DumpBlock(c.Block, dumper.IndentedStyle)) 242 | 243 | } 244 | ``` 245 | 246 | ### Update server listen port 247 | ```go 248 | func updateServerListenPort(filePath string, oldPort string, newPort string) (string, error) { 249 | p, err := parser.NewParser(filePath) 250 | if err != nil { 251 | return "", fmt.Errorf("failed to create parser: %w", err) 252 | } 253 | conf, err := p.Parse() 254 | if err != nil { 255 | return "", fmt.Errorf("failed to parse config: %w", err) 256 | } 257 | 258 | servers := conf.FindDirectives("server") 259 | for _, server := range servers { 260 | listens := server.GetBlock().FindDirectives("listen") 261 | for _, listen := range listens { 262 | if listen.GetParameters()[0] == oldPort { 263 | listenDirective := listen.(*config.Directive) 264 | listenDirective.Parameters[0] = newPort 265 | } 266 | } 267 | } 268 | changedConf := dumper.DumpConfig(conf, dumper.IndentedStyle) 269 | return changedConf, nil 270 | } 271 | func main() { 272 | 273 | filePath := "../../testdata/full_conf/nginx.conf" 274 | oldPort := "80" 275 | newPort := "8080" 276 | if changedConf, err := updateServerListenPort(filePath, oldPort, newPort); err != nil { 277 | log.Fatalf("Error updating server listen port: %v", err) 278 | } else { 279 | fmt.Println(changedConf) 280 | } 281 | } 282 | ``` 283 | 284 | ### Add custom directive in any block 285 | ```go 286 | func addCustomDirective(fullConf string, blockName string, directiveName string, directiveValue string) (string, error) { 287 | p := parser.NewStringParser(fullConf) 288 | conf, err := p.Parse() 289 | if err != nil { 290 | return "", fmt.Errorf("failed to parse config: %w", err) 291 | } 292 | 293 | blocks := conf.FindDirectives(blockName) 294 | if len(blocks) == 0 { 295 | return "", fmt.Errorf("no such block: %s", blockName) 296 | } 297 | 298 | block := blocks[0].GetBlock() 299 | newDirective := &config.Directive{ 300 | Name: directiveName, 301 | Parameters: []string{directiveValue}, 302 | } 303 | realBlock := block.(*config.Block) 304 | realBlock.Directives = append(realBlock.Directives, newDirective) 305 | 306 | return dumper.DumpConfig(conf, dumper.IndentedStyle), nil 307 | } 308 | 309 | func main() { 310 | fullConf := `http{ 311 | upstream my_backend{ 312 | server 127.0.0.1:443; 313 | server 127.0.0.2:443 backup; 314 | } 315 | server { 316 | listen 8080; 317 | location / { 318 | root /var/www/html; 319 | index index.html; 320 | } 321 | } 322 | 323 | server { 324 | listen 9090; 325 | location / { 326 | root /var/www/html; 327 | index index.html; 328 | } 329 | } 330 | }` 331 | 332 | blockName := "server" 333 | directiveName := "access_log" 334 | directiveValue := "/var/log/nginx/access.log" 335 | newFullConf, err := addCustomDirective(fullConf, blockName, directiveName, directiveValue) 336 | if err != nil { 337 | log.Fatalf("Error adding custom directive: %v", err) 338 | } 339 | fmt.Println("New Full Config:", newFullConf) 340 | } 341 | 342 | ``` 343 | 344 | ## Library Reference 345 | ### Parser 346 | Parser is the main package that analyzes and turns nginx structured files into objects. It basically has three pieces: `lexer` breaks the file into tokens and `parser` converts tokens into configuration objects defined in the `config` package. 347 | 348 | #### ```NewParser(filePath string, opts ...Option) (*Parser, error)``` 349 | + filePath is the path to the nginx config file. 350 | + opts is a list of options that can be passed to the parser. 351 | #### ```NewStringParser(content string, opts ...Option) (*Parser, error)``` 352 | + content is the content of the nginx config file. 353 | + opts is a list of options that can be passed to the parser. 354 | #### ```NewParserFromLexer(lexer *lexer, opts ...Option) *Parser``` 355 | + lexer is the lexer that is used to parse the config file. 356 | + opts is a list of options that can be passed to the parser. 357 | 358 | #### Options 359 | + **WithSkipIncludeParsingErr()**: If this option is set, the parser will not return an error if it encounters an include directive. 360 | + **WithDefaultOptions()**: WithDefaultOptions default options 361 | + **WithSkipComments()**: If this option is set, the parser will not parse comments. 362 | + **WithIncludeParsing()**: If this option is set, the parser will parse includes. 363 | + **WithCustomDirectives(directives ...string)**: If this option is set, the parser will parse custom directives without validation. 364 | + **WithSkipValidBlocks(blocks ...string)**: If this option is set, the parser will not validate directives that are within blocks(recursive) 365 | + **WithSkipValidDirectivesErr()**: If this option is set, the parser will not return an error if it encounters an invalid directive. 366 | 367 | #### Create a new parser with options 368 | ```go 369 | p, err := parser.NewParser("nginx.conf", WithSkipComments(), WithCustomDirectives("hello_world"), WithSkipValidBlocks("my_block")) 370 | ``` 371 | 372 | #### ```func (p *Parser) Parse() (*config.Config, error)``` 373 | Parse parses the config file(or from config strings) and returns a config object. **It's the only way to get the config object**. 374 | 375 | ---- 376 | ### Config 377 | The `config` package models contexts and directives in Go and forms the AST. 378 | 379 | #### ```func (c *Config) FindDirectives(directiveName string) []IDirective``` 380 | FindDirectives finds all directives with the given name. 381 | #### ```func (c *Config) FindUpstreams() []*Upstream``` 382 | FindUpstreams finds all upstreams. 383 | 384 | #### IDirective 385 | ```go 386 | type IDirective interface { 387 | GetName() string //the directive name. 388 | GetParameters() []string 389 | GetBlock() IBlock 390 | GetComment() []string 391 | SetComment(comment []string) 392 | SetParent(IDirective) 393 | GetParent() IDirective 394 | } 395 | ``` 396 | + GetName() string: the directive name. 397 | + GetParameters() []string: the directive parameters. 398 | + GetBlock() IBlock: the directive block. 399 | + GetComment() []string: the directive comment. 400 | + SetComment(comment []string): the directive comment. 401 | + GetParent() IDirective: the directive that contains or encloses the current directive. 402 | #### IBlock 403 | ```go 404 | type IBlock interface { 405 | GetDirectives() []IDirective 406 | FindDirectives(directiveName string) []IDirective 407 | GetCodeBlock() string 408 | SetParent(IDirective) 409 | GetParent() IDirective 410 | } 411 | ``` 412 | + GetDirectives() []IDirective: the block directives. 413 | + FindDirectives(directiveName string) []IDirective: the block directives. 414 | + GetCodeBlock() string: the block code. 415 | + GetParent() IDirective: the directive that contains or encloses the current directive. 416 | 417 | #### Directive (impl IDirective) 418 | ```go 419 | type Directive struct { 420 | Block IBlock 421 | Name string 422 | Parameters []string //TODO: Save parameters with their type 423 | Comment []string 424 | Parent IBlock 425 | } 426 | ``` 427 | #### Block (impl IBlock) 428 | ```go 429 | type Block struct { 430 | Directives []IDirective 431 | IsLuaBlock bool 432 | LiteralCode string 433 | Parent IBlock 434 | } 435 | ``` 436 | + ```func (b *Block) FindDirectives(directiveName string) []IDirective``` 437 | 438 | #### Upstream (impl IDirective) 439 | ```go 440 | type Upstream struct { 441 | UpstreamName string 442 | UpstreamServers []*UpstreamServer 443 | //Directives Other directives in upstream (ip_hash; etc) 444 | Directives []IDirective 445 | Comment []string 446 | Parent IBlock 447 | } 448 | ``` 449 | + ```func (us *Upstream) AddServer(server *UpstreamServer)``` 450 | 451 | #### UpstreamServer (impl IDirective) 452 | ```go 453 | type UpstreamServer struct { 454 | Address string 455 | Flags []string 456 | Parameters map[string]string 457 | Comment []string 458 | Parent IBlock 459 | } 460 | ``` 461 | 462 | #### HTTP (impl IDirective) 463 | ```go 464 | type HTTP struct { 465 | Servers []*Server 466 | Directives []IDirective 467 | Comment []string 468 | Parent IBlock 469 | } 470 | ``` 471 | + ```func (h *HTTP) FindDirectives(directiveName string) []IDirective``` 472 | 473 | #### Server (impl IDirective) 474 | ```go 475 | type Server struct { 476 | Block IBlock 477 | Comment []string 478 | Parent IBlock 479 | } 480 | ``` 481 | --- 482 | ### Dumper 483 | Dumper is the package that holds styling configuration only. 484 | 485 | #### ```func DumpConfig(c *config.Config, style *Style) string``` 486 | DumpConfig dump whole config. 487 | 488 | #### ```func DumpBlock(b config.IBlock, style *Style) string``` 489 | DumpBlock convert a directive to a string. 490 | 491 | #### ```func DumpDirective(d config.IDirective, style *Style) string``` 492 | DumpDirective convert a directive to a string 493 | 494 | #### ```func DumpInclude(i *config.Include, style *Style) map[string]string``` 495 | DumpInclude dump(stringify) the included AST 496 | 497 | #### ```func WriteConfig(c *config.Config, style *Style, writeInclude bool) error``` 498 | WriteConfig writes config. 499 | 500 | #### Style 501 | dumping style, you can use it to customize the output style. 502 | ```go 503 | type Style struct { 504 | SortDirectives bool 505 | SpaceBeforeBlocks bool 506 | StartIndent int 507 | Indent int 508 | Debug bool 509 | } 510 | ``` 511 | #### Styles by default 512 | + NoIndentStyle 513 | ```go 514 | NoIndentStyle = &Style{ 515 | SortDirectives: false, 516 | StartIndent: 0, 517 | Indent: 0, 518 | Debug: false, 519 | } 520 | ``` 521 | + IndentStyle 522 | ```go 523 | IndentedStyle = &Style{ 524 | SortDirectives: false, 525 | StartIndent: 0, 526 | Indent: 4, 527 | Debug: false, 528 | } 529 | ``` 530 | + NoIndentSortedStyle 531 | ```go 532 | NoIndentSortedStyle = &Style{ 533 | SortDirectives: true, 534 | StartIndent: 0, 535 | Indent: 0, 536 | Debug: false, 537 | } 538 | ``` 539 | + NoIndentSortedSpaceStyle 540 | ```go 541 | NoIndentSortedSpaceStyle = &Style{ 542 | SortDirectives: true, 543 | SpaceBeforeBlocks: true, 544 | StartIndent: 0, 545 | Indent: 0, 546 | Debug: false, 547 | } 548 | ``` 549 | -------------------------------------------------------------------------------- /parser/valid_directives.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // got the list from: https://nginx.org/en/docs/dirindex.html 8 | var validDirectivesRawList = `absolute_redirect 9 | accept_mutex 10 | accept_mutex_delay 11 | access_log 12 | add_after_body 13 | add_before_body 14 | add_header 15 | add_trailer 16 | addition_types 17 | aio 18 | aio_write 19 | alias 20 | allow 21 | ancient_browser 22 | ancient_browser_value 23 | api 24 | auth_basic 25 | auth_basic_user_file 26 | auth_delay 27 | auth_http 28 | auth_http_header 29 | auth_http_pass_client_cert 30 | auth_http_timeout 31 | auth_jwt 32 | auth_jwt_claim_set 33 | auth_jwt_header_set 34 | auth_jwt_key_cache 35 | auth_jwt_key_file 36 | auth_jwt_key_request 37 | auth_jwt_leeway 38 | auth_jwt_require 39 | auth_jwt_type 40 | auth_request 41 | auth_request_set 42 | autoindex 43 | autoindex_exact_size 44 | autoindex_format 45 | autoindex_localtime 46 | break 47 | charset 48 | charset_map 49 | charset_types 50 | chunked_transfer_encoding 51 | client_body_buffer_size 52 | client_body_in_file_only 53 | client_body_in_single_buffer 54 | client_body_temp_path 55 | client_body_timeout 56 | client_header_buffer_size 57 | client_header_timeout 58 | client_max_body_size 59 | connect_timeout 60 | connection_pool_size 61 | create_full_put_path 62 | daemon 63 | dav_access 64 | dav_methods 65 | debug_connection 66 | debug_points 67 | default 68 | default_type 69 | deny 70 | directio 71 | directio_alignment 72 | disable_symlinks 73 | empty_gif 74 | env 75 | error_log 76 | error_page 77 | etag 78 | events 79 | expires 80 | f4f 81 | f4f_buffer_size 82 | fastcgi_bind 83 | fastcgi_buffer_size 84 | fastcgi_buffering 85 | fastcgi_buffers 86 | fastcgi_busy_buffers_size 87 | fastcgi_cache 88 | fastcgi_cache_background_update 89 | fastcgi_cache_bypass 90 | fastcgi_cache_key 91 | fastcgi_cache_lock 92 | fastcgi_cache_lock_age 93 | fastcgi_cache_lock_timeout 94 | fastcgi_cache_max_range_offset 95 | fastcgi_cache_methods 96 | fastcgi_cache_min_uses 97 | fastcgi_cache_path 98 | fastcgi_cache_purge 99 | fastcgi_cache_revalidate 100 | fastcgi_cache_use_stale 101 | fastcgi_cache_valid 102 | fastcgi_catch_stderr 103 | fastcgi_connect_timeout 104 | fastcgi_force_ranges 105 | fastcgi_hide_header 106 | fastcgi_ignore_client_abort 107 | fastcgi_ignore_headers 108 | fastcgi_index 109 | fastcgi_intercept_errors 110 | fastcgi_keep_conn 111 | fastcgi_limit_rate 112 | fastcgi_max_temp_file_size 113 | fastcgi_next_upstream 114 | fastcgi_next_upstream_timeout 115 | fastcgi_next_upstream_tries 116 | fastcgi_no_cache 117 | fastcgi_param 118 | fastcgi_pass 119 | fastcgi_pass_header 120 | fastcgi_pass_request_body 121 | fastcgi_pass_request_headers 122 | fastcgi_read_timeout 123 | fastcgi_request_buffering 124 | fastcgi_send_lowat 125 | fastcgi_send_timeout 126 | fastcgi_socket_keepalive 127 | fastcgi_split_path_info 128 | fastcgi_store 129 | fastcgi_store_access 130 | fastcgi_temp_file_write_size 131 | fastcgi_temp_path 132 | flv 133 | geo 134 | geoip_city 135 | geoip_country 136 | geoip_org 137 | geoip_proxy 138 | geoip_proxy_recursive 139 | google_perftools_profiles 140 | grpc_bind 141 | grpc_buffer_size 142 | grpc_connect_timeout 143 | grpc_hide_header 144 | grpc_ignore_headers 145 | grpc_intercept_errors 146 | grpc_next_upstream 147 | grpc_next_upstream_timeout 148 | grpc_next_upstream_tries 149 | grpc_pass 150 | grpc_pass_header 151 | grpc_read_timeout 152 | grpc_send_timeout 153 | grpc_set_header 154 | grpc_socket_keepalive 155 | grpc_ssl_certificate 156 | grpc_ssl_certificate_key 157 | grpc_ssl_ciphers 158 | grpc_ssl_conf_command 159 | grpc_ssl_crl 160 | grpc_ssl_name 161 | grpc_ssl_password_file 162 | grpc_ssl_protocols 163 | grpc_ssl_server_name 164 | grpc_ssl_session_reuse 165 | grpc_ssl_trusted_certificate 166 | grpc_ssl_verify 167 | grpc_ssl_verify_depth 168 | gunzip 169 | gunzip_buffers 170 | gzip 171 | gzip_buffers 172 | gzip_comp_level 173 | gzip_disable 174 | gzip_http_version 175 | gzip_min_length 176 | gzip_proxied 177 | gzip_static 178 | gzip_types 179 | gzip_vary 180 | hash 181 | health_check 182 | health_check_timeout 183 | hls 184 | hls_buffers 185 | hls_forward_args 186 | hls_fragment 187 | hls_mp4_buffer_size 188 | hls_mp4_max_buffer_size 189 | http 190 | http2 191 | http2_body_preread_size 192 | http2_chunk_size 193 | http2_idle_timeout 194 | http2_max_concurrent_pushes 195 | http2_max_concurrent_streams 196 | http2_max_field_size 197 | http2_max_header_size 198 | http2_max_requests 199 | http2_push 200 | http2_push_preload 201 | http2_recv_buffer_size 202 | http2_recv_timeout 203 | http3 204 | http3_hq 205 | http3_max_concurrent_streams 206 | http3_stream_buffer_size 207 | if 208 | if_modified_since 209 | ignore_invalid_headers 210 | image_filter 211 | image_filter_buffer 212 | image_filter_interlace 213 | image_filter_jpeg_quality 214 | image_filter_sharpen 215 | image_filter_transparency 216 | image_filter_webp_quality 217 | imap_auth 218 | imap_capabilities 219 | imap_client_buffer 220 | include 221 | index 222 | internal 223 | internal_redirect 224 | ip_hash 225 | js_access 226 | js_body_filter 227 | js_content 228 | js_fetch_buffer_size 229 | js_fetch_ciphers 230 | js_fetch_max_response_buffer_size 231 | js_fetch_protocols 232 | js_fetch_timeout 233 | js_fetch_trusted_certificate 234 | js_fetch_verify 235 | js_fetch_verify_depth 236 | js_filter 237 | js_header_filter 238 | js_import 239 | js_include 240 | js_path 241 | js_periodic 242 | js_preload_object 243 | js_preread 244 | js_set 245 | js_shared_dict_zone 246 | js_var 247 | keepalive 248 | keepalive_disable 249 | keepalive_requests 250 | keepalive_time 251 | keepalive_timeout 252 | keyval 253 | keyval_zone 254 | large_client_header_buffers 255 | least_conn 256 | least_time 257 | limit_conn 258 | limit_conn_dry_run 259 | limit_conn_log_level 260 | limit_conn_status 261 | limit_conn_zone 262 | limit_except 263 | limit_rate 264 | limit_rate_after 265 | limit_req 266 | limit_req_dry_run 267 | limit_req_log_level 268 | limit_req_status 269 | limit_req_zone 270 | limit_zone 271 | lingering_close 272 | lingering_time 273 | lingering_timeout 274 | listen 275 | load_module 276 | location 277 | lock_file 278 | log_format 279 | log_format 280 | log_not_found 281 | log_subrequest 282 | mail 283 | map 284 | map_hash_bucket_size 285 | map_hash_max_size 286 | master_process 287 | match 288 | match 289 | max_errors 290 | max_ranges 291 | memcached_bind 292 | memcached_buffer_size 293 | memcached_connect_timeout 294 | memcached_gzip_flag 295 | memcached_next_upstream 296 | memcached_next_upstream_timeout 297 | memcached_next_upstream_tries 298 | memcached_pass 299 | memcached_read_timeout 300 | memcached_send_timeout 301 | memcached_socket_keepalive 302 | merge_slashes 303 | mgmt 304 | min_delete_depth 305 | mirror 306 | mirror_request_body 307 | modern_browser 308 | modern_browser_value 309 | mp4 310 | mp4_buffer_size 311 | mp4_limit_rate 312 | mp4_limit_rate_after 313 | mp4_max_buffer_size 314 | mp4_start_key_frame 315 | mqtt 316 | mqtt_buffers 317 | mqtt_preread 318 | mqtt_rewrite_buffer_size 319 | mqtt_set_connect 320 | msie_padding 321 | msie_refresh 322 | multi_accept 323 | ntlm 324 | open_file_cache 325 | open_file_cache_errors 326 | open_file_cache_min_uses 327 | open_file_cache_valid 328 | open_log_file_cache 329 | open_log_file_cache 330 | otel_exporter 331 | otel_service_name 332 | otel_span_attr 333 | otel_span_name 334 | otel_trace 335 | otel_trace_context 336 | output_buffers 337 | override_charset 338 | pcre_jit 339 | perl 340 | perl_modules 341 | perl_require 342 | perl_set 343 | pid 344 | pop3_auth 345 | pop3_capabilities 346 | port_in_redirect 347 | postpone_output 348 | preread_buffer_size 349 | preread_timeout 350 | protocol 351 | proxy_bind 352 | proxy_buffer 353 | proxy_buffer_size 354 | proxy_buffer_size 355 | proxy_buffering 356 | proxy_buffers 357 | proxy_busy_buffers_size 358 | proxy_cache 359 | proxy_cache_background_update 360 | proxy_cache_bypass 361 | proxy_cache_convert_head 362 | proxy_cache_key 363 | proxy_cache_lock 364 | proxy_cache_lock_age 365 | proxy_cache_lock_timeout 366 | proxy_cache_max_range_offset 367 | proxy_cache_methods 368 | proxy_cache_min_uses 369 | proxy_cache_path 370 | proxy_cache_purge 371 | proxy_cache_revalidate 372 | proxy_cache_use_stale 373 | proxy_cache_valid 374 | proxy_connect_timeout 375 | proxy_cookie_domain 376 | proxy_cookie_flags 377 | proxy_cookie_path 378 | proxy_download_rate 379 | proxy_force_ranges 380 | proxy_half_close 381 | proxy_headers_hash_bucket_size 382 | proxy_headers_hash_max_size 383 | proxy_hide_header 384 | proxy_http_version 385 | proxy_ignore_client_abort 386 | proxy_ignore_headers 387 | proxy_intercept_errors 388 | proxy_limit_rate 389 | proxy_max_temp_file_size 390 | proxy_method 391 | proxy_next_upstream 392 | proxy_next_upstream_timeout 393 | proxy_next_upstream_tries 394 | proxy_no_cache 395 | proxy_pass 396 | proxy_pass_error_message 397 | proxy_pass_header 398 | proxy_pass_request_body 399 | proxy_pass_request_headers 400 | proxy_protocol 401 | proxy_protocol_timeout 402 | proxy_read_timeout 403 | proxy_redirect 404 | proxy_request_buffering 405 | proxy_requests 406 | proxy_responses 407 | proxy_send_lowat 408 | proxy_send_timeout 409 | proxy_session_drop 410 | proxy_set_body 411 | proxy_set_header 412 | proxy_smtp_auth 413 | proxy_socket_keepalive 414 | proxy_ssl 415 | proxy_ssl_certificate 416 | proxy_ssl_certificate_key 417 | proxy_ssl_ciphers 418 | proxy_ssl_conf_command 419 | proxy_ssl_crl 420 | proxy_ssl_name 421 | proxy_ssl_password_file 422 | proxy_ssl_protocols 423 | proxy_ssl_server_name 424 | proxy_ssl_session_reuse 425 | proxy_ssl_trusted_certificate 426 | proxy_ssl_verify 427 | proxy_ssl_verify_depth 428 | proxy_store 429 | proxy_store_access 430 | proxy_temp_file_write_size 431 | proxy_temp_path 432 | proxy_timeout 433 | proxy_timeout 434 | proxy_upload_rate 435 | queue 436 | quic_active_connection_id_limit 437 | quic_bpf 438 | quic_gso 439 | quic_host_key 440 | quic_retry 441 | random 442 | random_index 443 | read_ahead 444 | read_timeout 445 | real_ip_header 446 | real_ip_recursive 447 | recursive_error_pages 448 | referer_hash_bucket_size 449 | referer_hash_max_size 450 | request_pool_size 451 | reset_timedout_connection 452 | resolver 453 | resolver_timeout 454 | return 455 | return 456 | rewrite 457 | rewrite_log 458 | root 459 | satisfy 460 | scgi_bind 461 | scgi_buffer_size 462 | scgi_buffering 463 | scgi_buffers 464 | scgi_busy_buffers_size 465 | scgi_cache 466 | scgi_cache_background_update 467 | scgi_cache_bypass 468 | scgi_cache_key 469 | scgi_cache_lock 470 | scgi_cache_lock_age 471 | scgi_cache_lock_timeout 472 | scgi_cache_max_range_offset 473 | scgi_cache_methods 474 | scgi_cache_min_uses 475 | scgi_cache_path 476 | scgi_cache_purge 477 | scgi_cache_revalidate 478 | scgi_cache_use_stale 479 | scgi_cache_valid 480 | scgi_connect_timeout 481 | scgi_force_ranges 482 | scgi_hide_header 483 | scgi_ignore_client_abort 484 | scgi_ignore_headers 485 | scgi_intercept_errors 486 | scgi_limit_rate 487 | scgi_max_temp_file_size 488 | scgi_next_upstream 489 | scgi_next_upstream_timeout 490 | scgi_next_upstream_tries 491 | scgi_no_cache 492 | scgi_param 493 | scgi_pass 494 | scgi_pass_header 495 | scgi_pass_request_body 496 | scgi_pass_request_headers 497 | scgi_read_timeout 498 | scgi_request_buffering 499 | scgi_send_timeout 500 | scgi_socket_keepalive 501 | scgi_store 502 | scgi_store_access 503 | scgi_temp_file_write_size 504 | scgi_temp_path 505 | secure_link 506 | secure_link_md5 507 | secure_link_secret 508 | send_lowat 509 | send_timeout 510 | sendfile 511 | sendfile_max_chunk 512 | server 513 | server_name 514 | server_name_in_redirect 515 | server_names_hash_bucket_size 516 | server_names_hash_max_size 517 | server_tokens 518 | session_log 519 | session_log_format 520 | session_log_zone 521 | set 522 | set_real_ip_from 523 | slice 524 | smtp_auth 525 | smtp_capabilities 526 | smtp_client_buffer 527 | smtp_greeting_delay 528 | source_charset 529 | split_clients 530 | ssi 531 | ssi_last_modified 532 | ssi_min_file_chunk 533 | ssi_silent_errors 534 | ssi_types 535 | ssi_value_length 536 | ssl 537 | ssl_alpn 538 | ssl_buffer_size 539 | ssl_certificate 540 | ssl_certificate_key 541 | ssl_ciphers 542 | ssl_client_certificate 543 | ssl_conf_command 544 | ssl_crl 545 | ssl_dhparam 546 | ssl_early_data 547 | ssl_ecdh_curve 548 | ssl_engine 549 | ssl_handshake_timeout 550 | ssl_name 551 | ssl_ocsp 552 | ssl_ocsp_cache 553 | ssl_ocsp_responder 554 | ssl_password_file 555 | ssl_prefer_server_ciphers 556 | ssl_preread 557 | ssl_protocols 558 | ssl_reject_handshake 559 | ssl_server_name 560 | ssl_session_cache 561 | ssl_session_ticket_key 562 | ssl_session_ticket_key 563 | ssl_session_ticket_key 564 | ssl_session_tickets 565 | ssl_session_timeout 566 | ssl_stapling 567 | ssl_stapling_file 568 | ssl_stapling_responder 569 | ssl_stapling_verify 570 | ssl_trusted_certificate 571 | ssl_verify 572 | ssl_verify_client 573 | ssl_verify_depth 574 | starttls 575 | state 576 | status 577 | status_format 578 | status_zone 579 | status_zone 580 | sticky 581 | sticky_cookie_insert 582 | stream 583 | stub_status 584 | sub_filter 585 | sub_filter_last_modified 586 | sub_filter_once 587 | sub_filter_types 588 | subrequest_output_buffer_size 589 | tcp_nodelay 590 | tcp_nopush 591 | thread_pool 592 | timeout 593 | timer_resolution 594 | try_files 595 | types 596 | types_hash_bucket_size 597 | types_hash_max_size 598 | underscores_in_headers 599 | uninitialized_variable_warn 600 | upstream 601 | upstream_conf 602 | usage_report 603 | use 604 | user 605 | userid 606 | userid_domain 607 | userid_expires 608 | userid_flags 609 | userid_mark 610 | userid_name 611 | userid_p3p 612 | userid_path 613 | userid_service 614 | uuid_file 615 | uwsgi_bind 616 | uwsgi_buffer_size 617 | uwsgi_buffering 618 | uwsgi_buffers 619 | uwsgi_busy_buffers_size 620 | uwsgi_cache 621 | uwsgi_cache_background_update 622 | uwsgi_cache_bypass 623 | uwsgi_cache_key 624 | uwsgi_cache_lock 625 | uwsgi_cache_lock_age 626 | uwsgi_cache_lock_timeout 627 | uwsgi_cache_max_range_offset 628 | uwsgi_cache_methods 629 | uwsgi_cache_min_uses 630 | uwsgi_cache_path 631 | uwsgi_cache_purge 632 | uwsgi_cache_revalidate 633 | uwsgi_cache_use_stale 634 | uwsgi_cache_valid 635 | uwsgi_connect_timeout 636 | uwsgi_force_ranges 637 | uwsgi_hide_header 638 | uwsgi_ignore_client_abort 639 | uwsgi_ignore_headers 640 | uwsgi_intercept_errors 641 | uwsgi_limit_rate 642 | uwsgi_max_temp_file_size 643 | uwsgi_modifier1 644 | uwsgi_modifier2 645 | uwsgi_next_upstream 646 | uwsgi_next_upstream_timeout 647 | uwsgi_next_upstream_tries 648 | uwsgi_no_cache 649 | uwsgi_param 650 | uwsgi_pass 651 | uwsgi_pass_header 652 | uwsgi_pass_request_body 653 | uwsgi_pass_request_headers 654 | uwsgi_read_timeout 655 | uwsgi_request_buffering 656 | uwsgi_send_timeout 657 | uwsgi_socket_keepalive 658 | uwsgi_ssl_certificate 659 | uwsgi_ssl_certificate_key 660 | uwsgi_ssl_ciphers 661 | uwsgi_ssl_conf_command 662 | uwsgi_ssl_crl 663 | uwsgi_ssl_name 664 | uwsgi_ssl_password_file 665 | uwsgi_ssl_protocols 666 | uwsgi_ssl_server_name 667 | uwsgi_ssl_session_reuse 668 | uwsgi_ssl_trusted_certificate 669 | uwsgi_ssl_verify 670 | uwsgi_ssl_verify_depth 671 | uwsgi_store 672 | uwsgi_store_access 673 | uwsgi_temp_file_write_size 674 | uwsgi_temp_path 675 | valid_referers 676 | variables_hash_bucket_size 677 | variables_hash_bucket_size 678 | variables_hash_max_size 679 | variables_hash_max_size 680 | worker_aio_requests 681 | worker_connections 682 | worker_cpu_affinity 683 | worker_priority 684 | worker_processes 685 | worker_rlimit_core 686 | worker_rlimit_nofile 687 | worker_shutdown_timeout 688 | working_directory 689 | xclient 690 | xml_entities 691 | xslt_last_modified 692 | xslt_param 693 | xslt_string_param 694 | xslt_stylesheet 695 | xslt_types 696 | zone 697 | zone 698 | zone_sync 699 | zone_sync_buffers 700 | zone_sync_connect_retry_interval 701 | zone_sync_connect_timeout 702 | zone_sync_interval 703 | zone_sync_recv_buffer_size 704 | zone_sync_server 705 | zone_sync_ssl 706 | zone_sync_ssl_certificate 707 | zone_sync_ssl_certificate_key 708 | zone_sync_ssl_ciphers 709 | zone_sync_ssl_conf_command 710 | zone_sync_ssl_crl 711 | zone_sync_ssl_name 712 | zone_sync_ssl_password_file 713 | zone_sync_ssl_protocols 714 | zone_sync_ssl_server_name 715 | zone_sync_ssl_trusted_certificate 716 | zone_sync_ssl_verify 717 | zone_sync_ssl_verify_depth 718 | zone_sync_timeout 719 | ` 720 | 721 | // https://github.com/openresty/lua-nginx-module?tab=readme-ov-file#directives 722 | var validLuaDirectivesRawList = `lua_load_resty_core 723 | lua_capture_error_log 724 | lua_use_default_type 725 | lua_malloc_trim 726 | lua_code_cache 727 | lua_thread_cache_max_entries 728 | lua_regex_cache_max_entries 729 | lua_regex_match_limit 730 | lua_package_path 731 | lua_package_cpath 732 | init_by_lua 733 | init_by_lua_block 734 | init_by_lua_file 735 | init_worker_by_lua 736 | init_worker_by_lua_block 737 | init_worker_by_lua_file 738 | exit_worker_by_lua_block 739 | exit_worker_by_lua_file 740 | set_by_lua 741 | set_by_lua_block 742 | set_by_lua_file 743 | content_by_lua 744 | content_by_lua_block 745 | content_by_lua_file 746 | server_rewrite_by_lua_block 747 | server_rewrite_by_lua_file 748 | rewrite_by_lua 749 | rewrite_by_lua_block 750 | rewrite_by_lua_file 751 | access_by_lua 752 | access_by_lua_block 753 | access_by_lua_file 754 | header_filter_by_lua 755 | header_filter_by_lua_block 756 | header_filter_by_lua_file 757 | body_filter_by_lua 758 | body_filter_by_lua_block 759 | body_filter_by_lua_file 760 | log_by_lua 761 | log_by_lua_block 762 | log_by_lua_file 763 | balancer_by_lua_block 764 | balancer_by_lua_file 765 | balancer_keepalive 766 | lua_need_request_body 767 | ssl_client_hello_by_lua_block 768 | ssl_client_hello_by_lua_file 769 | ssl_certificate_by_lua_block 770 | ssl_certificate_by_lua_file 771 | ssl_session_fetch_by_lua_block 772 | ssl_session_fetch_by_lua_file 773 | ssl_session_store_by_lua_block 774 | ssl_session_store_by_lua_file 775 | lua_shared_dict 776 | lua_socket_connect_timeout 777 | lua_socket_send_timeout 778 | lua_socket_send_lowat 779 | lua_socket_read_timeout 780 | lua_socket_buffer_size 781 | lua_socket_pool_size 782 | lua_socket_keepalive_timeout 783 | lua_socket_log_errors 784 | lua_ssl_ciphers 785 | lua_ssl_crl 786 | lua_ssl_protocols 787 | lua_ssl_certificate 788 | lua_ssl_certificate_key 789 | lua_ssl_trusted_certificate 790 | lua_ssl_verify_depth 791 | lua_ssl_conf_command 792 | lua_http10_buffering 793 | rewrite_by_lua_no_postpone 794 | access_by_lua_no_postpone 795 | lua_transform_underscores_in_response_headers 796 | lua_check_client_abort 797 | lua_max_pending_timers 798 | lua_max_running_timers 799 | lua_sa_restart 800 | lua_worker_thread_vm_pool_size 801 | ` 802 | 803 | // ValidDirectives mapped directives easily find 804 | // todo: this could handle the allowed blocks as well 805 | var ValidDirectives map[string]string = map[string]string{} 806 | 807 | func init() { 808 | validDirective := validDirectivesRawList + validLuaDirectivesRawList 809 | directives := strings.Split(validDirective, "\n") 810 | for _, directive := range directives { 811 | ValidDirectives[strings.TrimSpace(directive)] = strings.TrimSpace(directive) 812 | } 813 | } 814 | --------------------------------------------------------------------------------