1// Copyright 2015 Light Code Labs, LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package caddyfile
16
17import (
18	"bytes"
19	"encoding/json"
20	"fmt"
21	"sort"
22	"strconv"
23	"strings"
24)
25
26const filename = "Caddyfile"
27
28// ToJSON converts caddyfile to its JSON representation.
29func ToJSON(caddyfile []byte) ([]byte, error) {
30	var j EncodedCaddyfile
31
32	serverBlocks, err := Parse(filename, bytes.NewReader(caddyfile), nil)
33	if err != nil {
34		return nil, err
35	}
36
37	for _, sb := range serverBlocks {
38		block := EncodedServerBlock{
39			Keys: sb.Keys,
40			Body: [][]interface{}{},
41		}
42
43		// Extract directives deterministically by sorting them
44		var directives = make([]string, len(sb.Tokens))
45		for dir := range sb.Tokens {
46			directives = append(directives, dir)
47		}
48		sort.Strings(directives)
49
50		// Convert each directive's tokens into our JSON structure
51		for _, dir := range directives {
52			disp := NewDispenserTokens(filename, sb.Tokens[dir])
53			for disp.Next() {
54				block.Body = append(block.Body, constructLine(&disp))
55			}
56		}
57
58		// tack this block onto the end of the list
59		j = append(j, block)
60	}
61
62	result, err := json.Marshal(j)
63	if err != nil {
64		return nil, err
65	}
66
67	return result, nil
68}
69
70// constructLine transforms tokens into a JSON-encodable structure;
71// but only one line at a time, to be used at the top-level of
72// a server block only (where the first token on each line is a
73// directive) - not to be used at any other nesting level.
74func constructLine(d *Dispenser) []interface{} {
75	var args []interface{}
76
77	args = append(args, d.Val())
78
79	for d.NextArg() {
80		if d.Val() == "{" {
81			args = append(args, constructBlock(d))
82			continue
83		}
84		args = append(args, d.Val())
85	}
86
87	return args
88}
89
90// constructBlock recursively processes tokens into a
91// JSON-encodable structure. To be used in a directive's
92// block. Goes to end of block.
93func constructBlock(d *Dispenser) [][]interface{} {
94	block := [][]interface{}{}
95
96	for d.Next() {
97		if d.Val() == "}" {
98			break
99		}
100		block = append(block, constructLine(d))
101	}
102
103	return block
104}
105
106// FromJSON converts JSON-encoded jsonBytes to Caddyfile text
107func FromJSON(jsonBytes []byte) ([]byte, error) {
108	var j EncodedCaddyfile
109	var result string
110
111	err := json.Unmarshal(jsonBytes, &j)
112	if err != nil {
113		return nil, err
114	}
115
116	for sbPos, sb := range j {
117		if sbPos > 0 {
118			result += "\n\n"
119		}
120		for i, key := range sb.Keys {
121			if i > 0 {
122				result += ", "
123			}
124			//result += standardizeScheme(key)
125			result += key
126		}
127		result += jsonToText(sb.Body, 1)
128	}
129
130	return []byte(result), nil
131}
132
133// jsonToText recursively transforms a scope of JSON into plain
134// Caddyfile text.
135func jsonToText(scope interface{}, depth int) string {
136	var result string
137
138	switch val := scope.(type) {
139	case string:
140		if strings.ContainsAny(val, "\" \n\t\r") {
141			result += `"` + strings.Replace(val, "\"", "\\\"", -1) + `"`
142		} else {
143			result += val
144		}
145	case int:
146		result += strconv.Itoa(val)
147	case float64:
148		result += fmt.Sprintf("%v", val)
149	case bool:
150		result += fmt.Sprintf("%t", val)
151	case [][]interface{}:
152		result += " {\n"
153		for _, arg := range val {
154			result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
155		}
156		result += strings.Repeat("\t", depth-1) + "}"
157	case []interface{}:
158		for i, v := range val {
159			if block, ok := v.([]interface{}); ok {
160				result += "{\n"
161				for _, arg := range block {
162					result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
163				}
164				result += strings.Repeat("\t", depth-1) + "}"
165				continue
166			}
167			result += jsonToText(v, depth)
168			if i < len(val)-1 {
169				result += " "
170			}
171		}
172	}
173
174	return result
175}
176
177// TODO: Will this function come in handy somewhere else?
178/*
179// standardizeScheme turns an address like host:https into https://host,
180// or "host:" into "host".
181func standardizeScheme(addr string) string {
182	if hostname, port, err := net.SplitHostPort(addr); err == nil {
183		if port == "http" || port == "https" {
184			addr = port + "://" + hostname
185		}
186	}
187	return strings.TrimSuffix(addr, ":")
188}
189*/
190
191// EncodedCaddyfile encapsulates a slice of EncodedServerBlocks.
192type EncodedCaddyfile []EncodedServerBlock
193
194// EncodedServerBlock represents a server block ripe for encoding.
195type EncodedServerBlock struct {
196	Keys []string        `json:"keys"`
197	Body [][]interface{} `json:"body"`
198}
199