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:
commit
89ad7593bd
6 changed files with 268 additions and 9 deletions
163
caddy/caddyfile/json.go
Normal file
163
caddy/caddyfile/json.go
Normal 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"`
|
||||||
|
}
|
91
caddy/caddyfile/json_test.go
Normal file
91
caddy/caddyfile/json_test.go
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue