diff --git a/caddy/caddyfile/json.go b/caddy/caddyfile/json.go new file mode 100644 index 00000000..42171e7a --- /dev/null +++ b/caddy/caddyfile/json.go @@ -0,0 +1,163 @@ +package caddyfile + +import ( + "bytes" + "encoding/json" + "fmt" + "net" + "strconv" + "strings" + + "github.com/mholt/caddy/caddy/parse" +) + +const filename = "Caddyfile" + +// ToJSON converts caddyfile to its JSON representation. +func ToJSON(caddyfile []byte) ([]byte, error) { + var j Caddyfile + + serverBlocks, err := parse.ServerBlocks(filename, bytes.NewReader(caddyfile), false) + if err != nil { + return nil, err + } + + for _, sb := range serverBlocks { + block := ServerBlock{Body: make(map[string]interface{})} + + for _, host := range sb.HostList() { + block.Hosts = append(block.Hosts, strings.TrimSuffix(host, ":")) + } + + for dir, tokens := range sb.Tokens { + disp := parse.NewDispenserTokens(filename, tokens) + disp.Next() // the first token is the directive; skip it + block.Body[dir] = constructLine(disp) + } + + j = append(j, block) + } + + result, err := json.Marshal(j) + if err != nil { + return nil, err + } + + return result, nil +} + +// constructLine transforms tokens into a JSON-encodable structure; +// but only one line at a time, to be used at the top-level of +// a server block only (where the first token on each line is a +// directive) - not to be used at any other nesting level. +func constructLine(d parse.Dispenser) interface{} { + var args []interface{} + + all := d.RemainingArgs() + for _, arg := range all { + args = append(args, arg) + } + + d.Next() + if d.Val() == "{" { + args = append(args, constructBlock(d)) + } + + return args +} + +// constructBlock recursively processes tokens into a +// JSON-encodable structure. +func constructBlock(d parse.Dispenser) interface{} { + block := make(map[string]interface{}) + + for d.Next() { + if d.Val() == "}" { + break + } + + dir := d.Val() + all := d.RemainingArgs() + + var args []interface{} + for _, arg := range all { + args = append(args, arg) + } + if d.Val() == "{" { + args = append(args, constructBlock(d)) + } + + block[dir] = args + } + + return block +} + +// FromJSON converts JSON-encoded jsonBytes to Caddyfile text +func FromJSON(jsonBytes []byte) ([]byte, error) { + var j Caddyfile + var result string + + err := json.Unmarshal(jsonBytes, &j) + if err != nil { + return nil, err + } + + for _, sb := range j { + for i, host := range sb.Hosts { + if hostname, port, err := net.SplitHostPort(host); err == nil { + if port == "http" || port == "https" { + host = port + "://" + hostname + } + } + if i > 0 { + result += ", " + } + result += strings.TrimSuffix(host, ":") + } + result += jsonToText(sb.Body, 1) + } + + return []byte(result), nil +} + +// jsonToText recursively transforms a scope of JSON into plain +// Caddyfile text. +func jsonToText(scope interface{}, depth int) string { + var result string + + switch val := scope.(type) { + case string: + if strings.ContainsAny(val, "\" \n\t\r") { + result += ` "` + strings.Replace(val, "\"", "\\\"", -1) + `"` + } else { + result += " " + val + } + case int: + result += " " + strconv.Itoa(val) + case float64: + result += " " + fmt.Sprintf("%v", val) + case bool: + result += " " + fmt.Sprintf("%t", val) + case map[string]interface{}: + result += " {\n" + for param, args := range val { + result += strings.Repeat("\t", depth) + param + result += jsonToText(args, depth+1) + "\n" + } + result += strings.Repeat("\t", depth-1) + "}" + case []interface{}: + for _, v := range val { + result += jsonToText(v, depth) + } + } + + return result +} + +type Caddyfile []ServerBlock + +type ServerBlock struct { + Hosts []string `json:"hosts"` + Body map[string]interface{} `json:"body"` +} diff --git a/caddy/caddyfile/json_test.go b/caddy/caddyfile/json_test.go new file mode 100644 index 00000000..44abf982 --- /dev/null +++ b/caddy/caddyfile/json_test.go @@ -0,0 +1,91 @@ +package caddyfile + +import "testing" + +var tests = []struct { + caddyfile, json string +}{ + { // 0 + caddyfile: `foo { + root /bar +}`, + json: `[{"hosts":["foo"],"body":{"root":["/bar"]}}]`, + }, + { // 1 + caddyfile: `host1, host2 { + dir { + def + } +}`, + json: `[{"hosts":["host1","host2"],"body":{"dir":[{"def":null}]}}]`, + }, + { // 2 + caddyfile: `host1, host2 { + dir abc { + def ghi + } +}`, + json: `[{"hosts":["host1","host2"],"body":{"dir":["abc",{"def":["ghi"]}]}}]`, + }, + { // 3 + caddyfile: `host1:1234, host2:5678 { + dir abc { + } +}`, + json: `[{"hosts":["host1:1234","host2:5678"],"body":{"dir":["abc",{}]}}]`, + }, + { // 4 + caddyfile: `host { + foo "bar baz" +}`, + json: `[{"hosts":["host"],"body":{"foo":["bar baz"]}}]`, + }, + { // 5 + caddyfile: `host, host:80 { + foo "bar \"baz\"" +}`, + json: `[{"hosts":["host","host:80"],"body":{"foo":["bar \"baz\""]}}]`, + }, + { // 6 + caddyfile: `host { + foo "bar +baz" +}`, + json: `[{"hosts":["host"],"body":{"foo":["bar\nbaz"]}}]`, + }, + { // 7 + caddyfile: `host { + dir 123 4.56 true +}`, + json: `[{"hosts":["host"],"body":{"dir":["123","4.56","true"]}}]`, // NOTE: I guess we assume numbers and booleans should be encoded as strings...? + }, + { // 8 + caddyfile: `http://host, https://host { +}`, + json: `[{"hosts":["host:http","host:https"],"body":{}}]`, // hosts in JSON are always host:port format (if port is specified) + }, +} + +func TestToJSON(t *testing.T) { + for i, test := range tests { + output, err := ToJSON([]byte(test.caddyfile)) + if err != nil { + t.Errorf("Test %d: %v", i, err) + } + if string(output) != test.json { + t.Errorf("Test %d\nExpected:\n'%s'\nActual:\n'%s'", i, test.json, string(output)) + } + } +} + +func TestFromJSON(t *testing.T) { + for i, test := range tests { + output, err := FromJSON([]byte(test.json)) + if err != nil { + t.Errorf("Test %d: %v", i, err) + } + if string(output) != test.caddyfile { + t.Errorf("Test %d\nExpected:\n'%s'\nActual:\n'%s'", i, test.caddyfile, string(output)) + } + } +} diff --git a/caddy/config.go b/caddy/config.go index bc9ec603..53432b4e 100644 --- a/caddy/config.go +++ b/caddy/config.go @@ -28,7 +28,7 @@ func load(filename string, input io.Reader) ([]server.Config, error) { flags := log.Flags() log.SetFlags(0) - serverBlocks, err := parse.ServerBlocks(filename, input) + serverBlocks, err := parse.ServerBlocks(filename, input, true) if err != nil { return nil, err } diff --git a/caddy/parse/parse.go b/caddy/parse/parse.go index b44041d4..84043e60 100644 --- a/caddy/parse/parse.go +++ b/caddy/parse/parse.go @@ -5,9 +5,11 @@ import "io" // ServerBlocks parses the input just enough to organize tokens, // in order, by server block. No further parsing is performed. -// Server blocks are returned in the order in which they appear. -func ServerBlocks(filename string, input io.Reader) ([]serverBlock, error) { - p := parser{Dispenser: NewDispenser(filename, input)} +// If checkDirectives is true, only valid directives will be allowed +// otherwise we consider it a parse error. Server blocks are returned +// in the order in which they appear. +func ServerBlocks(filename string, input io.Reader, checkDirectives bool) ([]serverBlock, error) { + p := parser{Dispenser: NewDispenser(filename, input), checkDirectives: checkDirectives} blocks, err := p.parseAll() return blocks, err } diff --git a/caddy/parse/parsing.go b/caddy/parse/parsing.go index 59455391..b24b46ab 100644 --- a/caddy/parse/parsing.go +++ b/caddy/parse/parsing.go @@ -9,8 +9,9 @@ import ( type parser struct { Dispenser - block serverBlock // current server block being parsed - eof bool // if we encounter a valid EOF in a hard place + block serverBlock // current server block being parsed + eof bool // if we encounter a valid EOF in a hard place + checkDirectives bool // if true, directives must be known } func (p *parser) parseAll() ([]serverBlock, error) { @@ -220,8 +221,10 @@ func (p *parser) directive() error { dir := p.Val() nesting := 0 - if _, ok := ValidDirectives[dir]; !ok { - return p.Errf("Unknown directive '%s'", dir) + if p.checkDirectives { + if _, ok := ValidDirectives[dir]; !ok { + return p.Errf("Unknown directive '%s'", dir) + } } // The directive itself is appended as a relevant token diff --git a/caddy/parse/parsing_test.go b/caddy/parse/parsing_test.go index c8a7ef0b..afd5870f 100644 --- a/caddy/parse/parsing_test.go +++ b/caddy/parse/parsing_test.go @@ -375,6 +375,6 @@ func setupParseTests() { func testParser(input string) parser { buf := strings.NewReader(input) - p := parser{Dispenser: NewDispenser("Test", buf)} + p := parser{Dispenser: NewDispenser("Test", buf), checkDirectives: true} return p }