package caddyfile import ( "bytes" "encoding/json" "fmt" "sort" "strconv" "strings" ) const filename = "Caddyfile" // ToJSON converts caddyfile to its JSON representation. func ToJSON(caddyfile []byte) ([]byte, error) { var j EncodedCaddyfile serverBlocks, err := Parse(filename, bytes.NewReader(caddyfile), nil) if err != nil { return nil, err } for _, sb := range serverBlocks { block := EncodedServerBlock{ Keys: sb.Keys, Body: [][]interface{}{}, } // Extract directives deterministically by sorting them var directives = make([]string, len(sb.Tokens)) for dir := range sb.Tokens { directives = append(directives, dir) } sort.Strings(directives) // Convert each directive's tokens into our JSON structure for _, dir := range directives { disp := NewDispenserTokens(filename, sb.Tokens[dir]) for disp.Next() { block.Body = append(block.Body, constructLine(&disp)) } } // tack this block onto the end of the list 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 *Dispenser) []interface{} { var args []interface{} args = append(args, d.Val()) for d.NextArg() { if d.Val() == "{" { args = append(args, constructBlock(d)) continue } args = append(args, d.Val()) } return args } // constructBlock recursively processes tokens into a // JSON-encodable structure. To be used in a directive's // block. Goes to end of block. func constructBlock(d *Dispenser) [][]interface{} { block := [][]interface{}{} for d.Next() { if d.Val() == "}" { break } block = append(block, constructLine(d)) } return block } // FromJSON converts JSON-encoded jsonBytes to Caddyfile text func FromJSON(jsonBytes []byte) ([]byte, error) { var j EncodedCaddyfile var result string err := json.Unmarshal(jsonBytes, &j) if err != nil { return nil, err } for sbPos, sb := range j { if sbPos > 0 { result += "\n\n" } for i, key := range sb.Keys { if i > 0 { result += ", " } //result += standardizeScheme(key) result += key } 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 [][]interface{}: result += " {\n" for _, arg := range val { result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n" } result += strings.Repeat("\t", depth-1) + "}" case []interface{}: for i, v := range val { if block, ok := v.([]interface{}); ok { result += "{\n" for _, arg := range block { result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n" } result += strings.Repeat("\t", depth-1) + "}" continue } result += jsonToText(v, depth) if i < len(val)-1 { result += " " } } } return result } // TODO: Will this function come in handy somewhere else? /* // standardizeScheme turns an address like host:https into https://host, // or "host:" into "host". func standardizeScheme(addr string) string { if hostname, port, err := net.SplitHostPort(addr); err == nil { if port == "http" || port == "https" { addr = port + "://" + hostname } } return strings.TrimSuffix(addr, ":") } */ // EncodedCaddyfile encapsulates a slice of EncodedServerBlocks. type EncodedCaddyfile []EncodedServerBlock // EncodedServerBlock represents a server block ripe for encoding. type EncodedServerBlock struct { Keys []string `json:"keys"` Body [][]interface{} `json:"body"` }