0
Fork 0
mirror of https://github.com/caddyserver/caddy.git synced 2024-12-23 22:27:38 -05:00

Merge branch 'caddyfile' into letsencrypt

This commit is contained in:
Matthew Holt 2015-10-29 15:41:34 -06:00
commit 89ad7593bd
6 changed files with 268 additions and 9 deletions

163
caddy/caddyfile/json.go Normal file
View file

@ -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"`
}

View file

@ -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))
}
}
}

View file

@ -28,7 +28,7 @@ func load(filename string, input io.Reader) ([]server.Config, error) {
flags := log.Flags() flags := log.Flags()
log.SetFlags(0) log.SetFlags(0)
serverBlocks, err := parse.ServerBlocks(filename, input) serverBlocks, err := parse.ServerBlocks(filename, input, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -5,9 +5,11 @@ import "io"
// ServerBlocks parses the input just enough to organize tokens, // ServerBlocks parses the input just enough to organize tokens,
// in order, by server block. No further parsing is performed. // in order, by server block. No further parsing is performed.
// Server blocks are returned in the order in which they appear. // If checkDirectives is true, only valid directives will be allowed
func ServerBlocks(filename string, input io.Reader) ([]serverBlock, error) { // otherwise we consider it a parse error. Server blocks are returned
p := parser{Dispenser: NewDispenser(filename, input)} // 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() blocks, err := p.parseAll()
return blocks, err return blocks, err
} }

View file

@ -11,6 +11,7 @@ type parser struct {
Dispenser Dispenser
block serverBlock // current server block being parsed block serverBlock // current server block being parsed
eof bool // if we encounter a valid EOF in a hard place 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) { func (p *parser) parseAll() ([]serverBlock, error) {
@ -220,9 +221,11 @@ func (p *parser) directive() error {
dir := p.Val() dir := p.Val()
nesting := 0 nesting := 0
if p.checkDirectives {
if _, ok := ValidDirectives[dir]; !ok { if _, ok := ValidDirectives[dir]; !ok {
return p.Errf("Unknown directive '%s'", dir) return p.Errf("Unknown directive '%s'", dir)
} }
}
// The directive itself is appended as a relevant token // The directive itself is appended as a relevant token
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor]) p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])

View file

@ -375,6 +375,6 @@ func setupParseTests() {
func testParser(input string) parser { func testParser(input string) parser {
buf := strings.NewReader(input) buf := strings.NewReader(input)
p := parser{Dispenser: NewDispenser("Test", buf)} p := parser{Dispenser: NewDispenser("Test", buf), checkDirectives: true}
return p return p
} }