Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
Implement config adapters and beginning of Caddyfile adapter
Along with several other changes, such as renaming caddyhttp.ServerRoute to caddyhttp.Route, exporting some types that were not exported before, and tweaking the caddytls TLS values to be more consistent. Notably, we also now disable automatic cert management for names which already have a cert (manually) loaded into the cache. These names no longer need to be specified in the "skip_certificates" field of the automatic HTTPS config, because they will be skipped automatically.
Matthew Holt committed
Aug 9, 2019 at 12:05 UTC
ab885f07b844fd60adb9d49ed7884f3cd2d939a7
54 files changed
+4875
-232
admin.go
+4
-2
@@ -96,8 +96,10 @@ func StartAdmin(initialConfigJSON []byte) error {
96
///// END PPROF STUFF //////
97
98
for _, m := range GetModules("admin") {
99
- route := m.New().(AdminRoute)
100
- mux.Handle(route.Pattern, route)
99
+ routes := m.New().([]AdminRoute)
100
+ for _, route := range routes {
101
+ mux.Handle(route.Pattern, route)
102
+ }
103
}
104
105
handler := cors.Default().Handler(mux)
caddyconfig/caddyfile/adapter.go
new
+93
@@ -0,0 +1,93 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
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
+
15
+package caddyfile
16
+
17
+import (
18
+ "bytes"
19
+ "encoding/json"
20
+ "fmt"
21
+
22
+ "github.com/caddyserver/caddy/v2"
23
+ "github.com/caddyserver/caddy/v2/caddyconfig"
24
+)
25
+
26
+// Adapter adapts Caddyfile to Caddy JSON.
27
+type Adapter struct {
28
+ ServerType ServerType
29
+}
30
+
31
+// Adapt converts the Caddyfile config in body to Caddy JSON.
32
+func (a Adapter) Adapt(body []byte, options map[string]string) ([]byte, []caddyconfig.Warning, error) {
33
+ if a.ServerType == nil {
34
+ return nil, nil, fmt.Errorf("no server type")
35
+ }
36
+ if options == nil {
37
+ options = make(map[string]string)
38
+ }
39
+
40
+ directives := a.ServerType.ValidDirectives()
41
+
42
+ filename := options["filename"]
43
+ if filename == "" {
44
+ filename = "Caddyfile"
45
+ }
46
+
47
+ serverBlocks, err := Parse(filename, bytes.NewReader(body), directives)
48
+ if err != nil {
49
+ return nil, nil, err
50
+ }
51
+
52
+ cfg, warnings, err := a.ServerType.Setup(serverBlocks, options)
53
+ if err != nil {
54
+ return nil, warnings, err
55
+ }
56
+
57
+ marshalFunc := json.Marshal
58
+ if options["pretty"] == "true" {
59
+ marshalFunc = caddyconfig.JSONIndent
60
+ }
61
+ result, err := marshalFunc(cfg)
62
+
63
+ return result, warnings, err
64
+}
65
+
66
+// Unmarshaler is a type that can unmarshal
67
+// Caddyfile tokens to set itself up for a
68
+// JSON encoding. The goal of an unmarshaler
69
+// is not to set itself up for actual use,
70
+// but to set itself up for being marshaled
71
+// into JSON. Caddyfile-unmarshaled values
72
+// will not be used directly; they will be
73
+// encoded as JSON and then used from that.
74
+type Unmarshaler interface {
75
+ UnmarshalCaddyfile(d *Dispenser) error
76
+}
77
+
78
+// ServerType is a type that can evaluate a Caddyfile and set up a caddy config.
79
+type ServerType interface {
80
+ // ValidDirectives returns a list of the
81
+ // server type's recognized directives.
82
+ ValidDirectives() []string
83
+
84
+ // Setup takes the server blocks which
85
+ // contain tokens, as well as options
86
+ // (e.g. CLI flags) and creates a Caddy
87
+ // config, along with any warnings or
88
+ // an error.
89
+ Setup([]ServerBlock, map[string]string) (*caddy.Config, []caddyconfig.Warning, error)
90
+}
91
+
92
+// Interface guard
93
+var _ caddyconfig.Adapter = (*Adapter)(nil)
caddyconfig/caddyfile/dispenser.go
new
+333
@@ -0,0 +1,333 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
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
+
15
+package caddyfile
16
+
17
+import (
18
+ "errors"
19
+ "fmt"
20
+ "strings"
21
+)
22
+
23
+// Dispenser is a type that dispenses tokens, similarly to a lexer,
24
+// except that it can do so with some notion of structure. An empty
25
+// Dispenser is invalid; call NewDispenser to make a proper instance.
26
+type Dispenser struct {
27
+ filename string
28
+ tokens []Token
29
+ cursor int
30
+ nesting int
31
+}
32
+
33
+// NewDispenser returns a Dispenser filled with the given tokens.
34
+func NewDispenser(filename string, tokens []Token) *Dispenser {
35
+ return &Dispenser{
36
+ filename: filename,
37
+ tokens: tokens,
38
+ cursor: -1,
39
+ }
40
+}
41
+
42
+// Next loads the next token. Returns true if a token
43
+// was loaded; false otherwise. If false, all tokens
44
+// have been consumed.
45
+func (d *Dispenser) Next() bool {
46
+ if d.cursor < len(d.tokens)-1 {
47
+ d.cursor++
48
+ return true
49
+ }
50
+ return false
51
+}
52
+
53
+// Prev moves to the previous token. It does the inverse
54
+// of Next(). Generally, this should only be used in
55
+// special cases such as deleting a token from the slice
56
+// that d is iterating. In that case, without using Prev(),
57
+// the dispenser would be pointing at the wrong token since
58
+// deleting a token implicitly advances the cursor.
59
+func (d *Dispenser) Prev() bool {
60
+ if d.cursor > 0 {
61
+ d.cursor--
62
+ return true
63
+ }
64
+ return false
65
+}
66
+
67
+// NextArg loads the next token if it is on the same
68
+// line and if it is not a block opening (open curly
69
+// brace). Returns true if an argument token was
70
+// loaded; false otherwise. If false, all tokens on
71
+// the line have been consumed except for potentially
72
+// a block opening. It handles imported tokens
73
+// correctly.
74
+func (d *Dispenser) NextArg() bool {
75
+ if !d.nextOnSameLine() {
76
+ return false
77
+ }
78
+ if d.Val() == "{" {
79
+ // roll back; a block opening is not an argument
80
+ d.cursor--
81
+ return false
82
+ }
83
+ return true
84
+}
85
+
86
+// nextOnSameLine advances the cursor if the next
87
+// token is on the same line of the same file.
88
+func (d *Dispenser) nextOnSameLine() bool {
89
+ if d.cursor < 0 {
90
+ d.cursor++
91
+ return true
92
+ }
93
+ if d.cursor >= len(d.tokens) {
94
+ return false
95
+ }
96
+ if d.cursor < len(d.tokens)-1 &&
97
+ d.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&
98
+ d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {
99
+ d.cursor++
100
+ return true
101
+ }
102
+ return false
103
+}
104
+
105
+// NextLine loads the next token only if it is not on the same
106
+// line as the current token, and returns true if a token was
107
+// loaded; false otherwise. If false, there is not another token
108
+// or it is on the same line. It handles imported tokens correctly.
109
+func (d *Dispenser) NextLine() bool {
110
+ if d.cursor < 0 {
111
+ d.cursor++
112
+ return true
113
+ }
114
+ if d.cursor >= len(d.tokens) {
115
+ return false
116
+ }
117
+ if d.cursor < len(d.tokens)-1 &&
118
+ (d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||
119
+ d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {
120
+ d.cursor++
121
+ return true
122
+ }
123
+ return false
124
+}
125
+
126
+// NextBlock can be used as the condition of a for loop
127
+// to load the next token as long as it opens a block or
128
+// is already in a block. It returns true if a token was
129
+// loaded, or false when the block's closing curly brace
130
+// was loaded and thus the block ended. Nested blocks are
131
+// not supported.
132
+func (d *Dispenser) NextBlock() bool {
133
+ if d.nesting > 0 {
134
+ d.Next()
135
+ if d.Val() == "}" {
136
+ d.nesting--
137
+ return false
138
+ }
139
+ return true
140
+ }
141
+ if !d.nextOnSameLine() { // block must open on same line
142
+ return false
143
+ }
144
+ if d.Val() != "{" {
145
+ d.cursor-- // roll back if not opening brace
146
+ return false
147
+ }
148
+ d.Next()
149
+ if d.Val() == "}" {
150
+ // open and then closed right away
151
+ return false
152
+ }
153
+ d.nesting++
154
+ return true
155
+}
156
+
157
+// Nested returns true if the token is currently nested
158
+// inside a block (i.e. an open curly brace was consumed).
159
+func (d *Dispenser) Nested() bool {
160
+ return d.nesting > 0
161
+}
162
+
163
+// Val gets the text of the current token. If there is no token
164
+// loaded, it returns empty string.
165
+func (d *Dispenser) Val() string {
166
+ if d.cursor < 0 || d.cursor >= len(d.tokens) {
167
+ return ""
168
+ }
169
+ return d.tokens[d.cursor].Text
170
+}
171
+
172
+// Line gets the line number of the current token. If there is no token
173
+// loaded, it returns 0.
174
+func (d *Dispenser) Line() int {
175
+ if d.cursor < 0 || d.cursor >= len(d.tokens) {
176
+ return 0
177
+ }
178
+ return d.tokens[d.cursor].Line
179
+}
180
+
181
+// File gets the filename of the current token. If there is no token loaded,
182
+// it returns the filename originally given when parsing started.
183
+func (d *Dispenser) File() string {
184
+ if d.cursor < 0 || d.cursor >= len(d.tokens) {
185
+ return d.filename
186
+ }
187
+ if tokenFilename := d.tokens[d.cursor].File; tokenFilename != "" {
188
+ return tokenFilename
189
+ }
190
+ return d.filename
191
+}
192
+
193
+// Args is a convenience function that loads the next arguments
194
+// (tokens on the same line) into an arbitrary number of strings
195
+// pointed to in targets. If there are fewer tokens available
196
+// than string pointers, the remaining strings will not be changed
197
+// and false will be returned. If there were enough tokens available
198
+// to fill the arguments, then true will be returned.
199
+func (d *Dispenser) Args(targets ...*string) bool {
200
+ for i := 0; i < len(targets); i++ {
201
+ if !d.NextArg() {
202
+ return false
203
+ }
204
+ *targets[i] = d.Val()
205
+ }
206
+ return true
207
+}
208
+
209
+// RemainingArgs loads any more arguments (tokens on the same line)
210
+// into a slice and returns them. Open curly brace tokens also indicate
211
+// the end of arguments, and the curly brace is not included in
212
+// the return value nor is it loaded.
213
+func (d *Dispenser) RemainingArgs() []string {
214
+ var args []string
215
+ for d.NextArg() {
216
+ args = append(args, d.Val())
217
+ }
218
+ return args
219
+}
220
+
221
+// NewFromNextTokens returns a new dispenser with a copy of
222
+// the tokens from the current token until the end of the
223
+// "directive" whether that be to the end of the line or
224
+// the end of a block that starts at the end of the line.
225
+func (d *Dispenser) NewFromNextTokens() *Dispenser {
226
+ var tkns []Token
227
+ tkns = append(tkns, d.Token())
228
+ for d.NextArg() {
229
+ tkns = append(tkns, d.Token())
230
+ }
231
+ if d.Next() && d.Val() == "{" {
232
+ tkns = append(tkns, d.Token())
233
+ for d.NextBlock() {
234
+ for d.Nested() {
235
+ tkns = append(tkns, d.Token())
236
+ d.NextBlock()
237
+ }
238
+ }
239
+ tkns = append(tkns, d.Token())
240
+ } else {
241
+ d.cursor--
242
+ }
243
+ return NewDispenser(d.filename, tkns)
244
+}
245
+
246
+// Token returns the current token.
247
+func (d *Dispenser) Token() Token {
248
+ if d.cursor < 0 || d.cursor >= len(d.tokens) {
249
+ return Token{}
250
+ }
251
+ return d.tokens[d.cursor]
252
+}
253
+
254
+// Cursor returns the current cursor (token index).
255
+func (d *Dispenser) Cursor() int {
256
+ return d.cursor
257
+}
258
+
259
+// ArgErr returns an argument error, meaning that another
260
+// argument was expected but not found. In other words,
261
+// a line break or open curly brace was encountered instead of
262
+// an argument.
263
+func (d *Dispenser) ArgErr() error {
264
+ if d.Val() == "{" {
265
+ return d.Err("Unexpected token '{', expecting argument")
266
+ }
267
+ return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
268
+}
269
+
270
+// SyntaxErr creates a generic syntax error which explains what was
271
+// found and what was expected.
272
+func (d *Dispenser) SyntaxErr(expected string) error {
273
+ msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected)
274
+ return errors.New(msg)
275
+}
276
+
277
+// EOFErr returns an error indicating that the dispenser reached
278
+// the end of the input when searching for the next token.
279
+func (d *Dispenser) EOFErr() error {
280
+ return d.Errf("Unexpected EOF")
281
+}
282
+
283
+// Err generates a custom parse-time error with a message of msg.
284
+func (d *Dispenser) Err(msg string) error {
285
+ msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg)
286
+ return errors.New(msg)
287
+}
288
+
289
+// Errf is like Err, but for formatted error messages
290
+func (d *Dispenser) Errf(format string, args ...interface{}) error {
291
+ return d.Err(fmt.Sprintf(format, args...))
292
+}
293
+
294
+// Delete deletes the current token and returns the updated slice
295
+// of tokens. The cursor is not advanced to the next token.
296
+// Because deletion modifies the underlying slice, this method
297
+// should only be called if you have access to the original slice
298
+// of tokens and/or are using the slice of tokens outside this
299
+// Dispenser instance. If you do not re-assign the slice with the
300
+// return value of this method, inconsistencies in the token
301
+// array will become apparent (or worse, hide from you like they
302
+// did me for 3 and a half freaking hours late one night).
303
+func (d *Dispenser) Delete() []Token {
304
+ if d.cursor >= 0 && d.cursor < len(d.tokens)-1 {
305
+ d.tokens = append(d.tokens[:d.cursor], d.tokens[d.cursor+1:]...)
306
+ d.cursor--
307
+ }
308
+ return d.tokens
309
+}
310
+
311
+// numLineBreaks counts how many line breaks are in the token
312
+// value given by the token index tknIdx. It returns 0 if the
313
+// token does not exist or there are no line breaks.
314
+func (d *Dispenser) numLineBreaks(tknIdx int) int {
315
+ if tknIdx < 0 || tknIdx >= len(d.tokens) {
316
+ return 0
317
+ }
318
+ return strings.Count(d.tokens[tknIdx].Text, "\n")
319
+}
320
+
321
+// isNewLine determines whether the current token is on a different
322
+// line (higher line number) than the previous token. It handles imported
323
+// tokens correctly. If there isn't a previous token, it returns true.
324
+func (d *Dispenser) isNewLine() bool {
325
+ if d.cursor < 1 {
326
+ return true
327
+ }
328
+ if d.cursor > len(d.tokens)-1 {
329
+ return false
330
+ }
331
+ return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
332
+ d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
333
+}
caddyconfig/caddyfile/dispenser_test.go
new
+316
@@ -0,0 +1,316 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
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
+
15
+package caddyfile
16
+
17
+import (
18
+ "io"
19
+ "log"
20
+ "reflect"
21
+ "strings"
22
+ "testing"
23
+)
24
+
25
+func TestDispenser_Val_Next(t *testing.T) {
26
+ input := `host:port
27
+ dir1 arg1
28
+ dir2 arg2 arg3
29
+ dir3`
30
+ d := newTestDispenser(input)
31
+
32
+ if val := d.Val(); val != "" {
33
+ t.Fatalf("Val(): Should return empty string when no token loaded; got '%s'", val)
34
+ }
35
+
36
+ assertNext := func(shouldLoad bool, expectedCursor int, expectedVal string) {
37
+ if loaded := d.Next(); loaded != shouldLoad {
38
+ t.Errorf("Next(): Expected %v but got %v instead (val '%s')", shouldLoad, loaded, d.Val())
39
+ }
40
+ if d.cursor != expectedCursor {
41
+ t.Errorf("Expected cursor to be %d, but was %d", expectedCursor, d.cursor)
42
+ }
43
+ if d.nesting != 0 {
44
+ t.Errorf("Nesting should be 0, was %d instead", d.nesting)
45
+ }
46
+ if val := d.Val(); val != expectedVal {
47
+ t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
48
+ }
49
+ }
50
+
51
+ assertNext(true, 0, "host:port")
52
+ assertNext(true, 1, "dir1")
53
+ assertNext(true, 2, "arg1")
54
+ assertNext(true, 3, "dir2")
55
+ assertNext(true, 4, "arg2")
56
+ assertNext(true, 5, "arg3")
57
+ assertNext(true, 6, "dir3")
58
+ // Note: This next test simply asserts existing behavior.
59
+ // If desired, we may wish to empty the token value after
60
+ // reading past the EOF. Open an issue if you want this change.
61
+ assertNext(false, 6, "dir3")
62
+}
63
+
64
+func TestDispenser_NextArg(t *testing.T) {
65
+ input := `dir1 arg1
66
+ dir2 arg2 arg3
67
+ dir3`
68
+ d := newTestDispenser(input)
69
+
70
+ assertNext := func(shouldLoad bool, expectedVal string, expectedCursor int) {
71
+ if d.Next() != shouldLoad {
72
+ t.Errorf("Next(): Should load token but got false instead (val: '%s')", d.Val())
73
+ }
74
+ if d.cursor != expectedCursor {
75
+ t.Errorf("Next(): Expected cursor to be at %d, but it was %d", expectedCursor, d.cursor)
76
+ }
77
+ if val := d.Val(); val != expectedVal {
78
+ t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
79
+ }
80
+ }
81
+
82
+ assertNextArg := func(expectedVal string, loadAnother bool, expectedCursor int) {
83
+ if !d.NextArg() {
84
+ t.Error("NextArg(): Should load next argument but got false instead")
85
+ }
86
+ if d.cursor != expectedCursor {
87
+ t.Errorf("NextArg(): Expected cursor to be at %d, but it was %d", expectedCursor, d.cursor)
88
+ }
89
+ if val := d.Val(); val != expectedVal {
90
+ t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
91
+ }
92
+ if !loadAnother {
93
+ if d.NextArg() {
94
+ t.Fatalf("NextArg(): Should NOT load another argument, but got true instead (val: '%s')", d.Val())
95
+ }
96
+ if d.cursor != expectedCursor {
97
+ t.Errorf("NextArg(): Expected cursor to remain at %d, but it was %d", expectedCursor, d.cursor)
98
+ }
99
+ }
100
+ }
101
+
102
+ assertNext(true, "dir1", 0)
103
+ assertNextArg("arg1", false, 1)
104
+ assertNext(true, "dir2", 2)
105
+ assertNextArg("arg2", true, 3)
106
+ assertNextArg("arg3", false, 4)
107
+ assertNext(true, "dir3", 5)
108
+ assertNext(false, "dir3", 5)
109
+}
110
+
111
+func TestDispenser_NextLine(t *testing.T) {
112
+ input := `host:port
113
+ dir1 arg1
114
+ dir2 arg2 arg3`
115
+ d := newTestDispenser(input)
116
+
117
+ assertNextLine := func(shouldLoad bool, expectedVal string, expectedCursor int) {
118
+ if d.NextLine() != shouldLoad {
119
+ t.Errorf("NextLine(): Should load token but got false instead (val: '%s')", d.Val())
120
+ }
121
+ if d.cursor != expectedCursor {
122
+ t.Errorf("NextLine(): Expected cursor to be %d, instead was %d", expectedCursor, d.cursor)
123
+ }
124
+ if val := d.Val(); val != expectedVal {
125
+ t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
126
+ }
127
+ }
128
+
129
+ assertNextLine(true, "host:port", 0)
130
+ assertNextLine(true, "dir1", 1)
131
+ assertNextLine(false, "dir1", 1)
132
+ d.Next() // arg1
133
+ assertNextLine(true, "dir2", 3)
134
+ assertNextLine(false, "dir2", 3)
135
+ d.Next() // arg2
136
+ assertNextLine(false, "arg2", 4)
137
+ d.Next() // arg3
138
+ assertNextLine(false, "arg3", 5)
139
+}
140
+
141
+func TestDispenser_NextBlock(t *testing.T) {
142
+ input := `foobar1 {
143
+ sub1 arg1
144
+ sub2
145
+ }
146
+ foobar2 {
147
+ }`
148
+ d := newTestDispenser(input)
149
+
150
+ assertNextBlock := func(shouldLoad bool, expectedCursor, expectedNesting int) {
151
+ if loaded := d.NextBlock(); loaded != shouldLoad {
152
+ t.Errorf("NextBlock(): Should return %v but got %v", shouldLoad, loaded)
153
+ }
154
+ if d.cursor != expectedCursor {
155
+ t.Errorf("NextBlock(): Expected cursor to be %d, was %d", expectedCursor, d.cursor)
156
+ }
157
+ if d.nesting != expectedNesting {
158
+ t.Errorf("NextBlock(): Nesting should be %d, not %d", expectedNesting, d.nesting)
159
+ }
160
+ }
161
+
162
+ assertNextBlock(false, -1, 0)
163
+ d.Next() // foobar1
164
+ assertNextBlock(true, 2, 1)
165
+ assertNextBlock(true, 3, 1)
166
+ assertNextBlock(true, 4, 1)
167
+ assertNextBlock(false, 5, 0)
168
+ d.Next() // foobar2
169
+ assertNextBlock(false, 8, 0) // empty block is as if it didn't exist
170
+}
171
+
172
+func TestDispenser_Args(t *testing.T) {
173
+ var s1, s2, s3 string
174
+ input := `dir1 arg1 arg2 arg3
175
+ dir2 arg4 arg5
176
+ dir3 arg6 arg7
177
+ dir4`
178
+ d := newTestDispenser(input)
179
+
180
+ d.Next() // dir1
181
+
182
+ // As many strings as arguments
183
+ if all := d.Args(&s1, &s2, &s3); !all {
184
+ t.Error("Args(): Expected true, got false")
185
+ }
186
+ if s1 != "arg1" {
187
+ t.Errorf("Args(): Expected s1 to be 'arg1', got '%s'", s1)
188
+ }
189
+ if s2 != "arg2" {
190
+ t.Errorf("Args(): Expected s2 to be 'arg2', got '%s'", s2)
191
+ }
192
+ if s3 != "arg3" {
193
+ t.Errorf("Args(): Expected s3 to be 'arg3', got '%s'", s3)
194
+ }
195
+
196
+ d.Next() // dir2
197
+
198
+ // More strings than arguments
199
+ if all := d.Args(&s1, &s2, &s3); all {
200
+ t.Error("Args(): Expected false, got true")
201
+ }
202
+ if s1 != "arg4" {
203
+ t.Errorf("Args(): Expected s1 to be 'arg4', got '%s'", s1)
204
+ }
205
+ if s2 != "arg5" {
206
+ t.Errorf("Args(): Expected s2 to be 'arg5', got '%s'", s2)
207
+ }
208
+ if s3 != "arg3" {
209
+ t.Errorf("Args(): Expected s3 to be unchanged ('arg3'), instead got '%s'", s3)
210
+ }
211
+
212
+ // (quick cursor check just for kicks and giggles)
213
+ if d.cursor != 6 {
214
+ t.Errorf("Cursor should be 6, but is %d", d.cursor)
215
+ }
216
+
217
+ d.Next() // dir3
218
+
219
+ // More arguments than strings
220
+ if all := d.Args(&s1); !all {
221
+ t.Error("Args(): Expected true, got false")
222
+ }
223
+ if s1 != "arg6" {
224
+ t.Errorf("Args(): Expected s1 to be 'arg6', got '%s'", s1)
225
+ }
226
+
227
+ d.Next() // dir4
228
+
229
+ // No arguments or strings
230
+ if all := d.Args(); !all {
231
+ t.Error("Args(): Expected true, got false")
232
+ }
233
+
234
+ // No arguments but at least one string
235
+ if all := d.Args(&s1); all {
236
+ t.Error("Args(): Expected false, got true")
237
+ }
238
+}
239
+
240
+func TestDispenser_RemainingArgs(t *testing.T) {
241
+ input := `dir1 arg1 arg2 arg3
242
+ dir2 arg4 arg5
243
+ dir3 arg6 { arg7
244
+ dir4`
245
+ d := newTestDispenser(input)
246
+
247
+ d.Next() // dir1
248
+
249
+ args := d.RemainingArgs()
250
+ if expected := []string{"arg1", "arg2", "arg3"}; !reflect.DeepEqual(args, expected) {
251
+ t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args)
252
+ }
253
+
254
+ d.Next() // dir2
255
+
256
+ args = d.RemainingArgs()
257
+ if expected := []string{"arg4", "arg5"}; !reflect.DeepEqual(args, expected) {
258
+ t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args)
259
+ }
260
+
261
+ d.Next() // dir3
262
+
263
+ args = d.RemainingArgs()
264
+ if expected := []string{"arg6"}; !reflect.DeepEqual(args, expected) {
265
+ t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args)
266
+ }
267
+
268
+ d.Next() // {
269
+ d.Next() // arg7
270
+ d.Next() // dir4
271
+
272
+ args = d.RemainingArgs()
273
+ if len(args) != 0 {
274
+ t.Errorf("RemainingArgs(): Expected %v, got %v", []string{}, args)
275
+ }
276
+}
277
+
278
+func TestDispenser_ArgErr_Err(t *testing.T) {
279
+ input := `dir1 {
280
+ }
281
+ dir2 arg1 arg2`
282
+ d := newTestDispenser(input)
283
+
284
+ d.cursor = 1 // {
285
+
286
+ if err := d.ArgErr(); err == nil || !strings.Contains(err.Error(), "{") {
287
+ t.Errorf("ArgErr(): Expected an error message with { in it, but got '%v'", err)
288
+ }
289
+
290
+ d.cursor = 5 // arg2
291
+
292
+ if err := d.ArgErr(); err == nil || !strings.Contains(err.Error(), "arg2") {
293
+ t.Errorf("ArgErr(): Expected an error message with 'arg2' in it; got '%v'", err)
294
+ }
295
+
296
+ err := d.Err("foobar")
297
+ if err == nil {
298
+ t.Fatalf("Err(): Expected an error, got nil")
299
+ }
300
+
301
+ if !strings.Contains(err.Error(), "Testfile:3") {
302
+ t.Errorf("Expected error message with filename:line in it; got '%v'", err)
303
+ }
304
+
305
+ if !strings.Contains(err.Error(), "foobar") {
306
+ t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
307
+ }
308
+}
309
+
310
+func newTestDispenser(input string) *Dispenser {
311
+ tokens, err := allTokens(strings.NewReader(input))
312
+ if err != nil && err != io.EOF {
313
+ log.Fatalf("getting all tokens from input: %v", err)
314
+ }
315
+ return NewDispenser("Testfile", tokens)
316
+}
caddyconfig/caddyfile/lexer.go
new
+150
@@ -0,0 +1,150 @@
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
+
15
+package caddyfile
16
+
17
+import (
18
+ "bufio"
19
+ "io"
20
+ "unicode"
21
+)
22
+
23
+type (
24
+ // lexer is a utility which can get values, token by
25
+ // token, from a Reader. A token is a word, and tokens
26
+ // are separated by whitespace. A word can be enclosed
27
+ // in quotes if it contains whitespace.
28
+ lexer struct {
29
+ reader *bufio.Reader
30
+ token Token
31
+ line int
32
+ }
33
+
34
+ // Token represents a single parsable unit.
35
+ Token struct {
36
+ File string
37
+ Line int
38
+ Text string
39
+ }
40
+)
41
+
42
+// load prepares the lexer to scan an input for tokens.
43
+// It discards any leading byte order mark.
44
+func (l *lexer) load(input io.Reader) error {
45
+ l.reader = bufio.NewReader(input)
46
+ l.line = 1
47
+
48
+ // discard byte order mark, if present
49
+ firstCh, _, err := l.reader.ReadRune()
50
+ if err != nil {
51
+ return err
52
+ }
53
+ if firstCh != 0xFEFF {
54
+ err := l.reader.UnreadRune()
55
+ if err != nil {
56
+ return err
57
+ }
58
+ }
59
+
60
+ return nil
61
+}
62
+
63
+// next loads the next token into the lexer.
64
+// A token is delimited by whitespace, unless
65
+// the token starts with a quotes character (")
66
+// in which case the token goes until the closing
67
+// quotes (the enclosing quotes are not included).
68
+// Inside quoted strings, quotes may be escaped
69
+// with a preceding \ character. No other chars
70
+// may be escaped. The rest of the line is skipped
71
+// if a "#" character is read in. Returns true if
72
+// a token was loaded; false otherwise.
73
+func (l *lexer) next() bool {
74
+ var val []rune
75
+ var comment, quoted, escaped bool
76
+
77
+ makeToken := func() bool {
78
+ l.token.Text = string(val)
79
+ return true
80
+ }
81
+
82
+ for {
83
+ ch, _, err := l.reader.ReadRune()
84
+ if err != nil {
85
+ if len(val) > 0 {
86
+ return makeToken()
87
+ }
88
+ if err == io.EOF {
89
+ return false
90
+ }
91
+ panic(err)
92
+ }
93
+
94
+ if quoted {
95
+ if !escaped {
96
+ if ch == '\\' {
97
+ escaped = true
98
+ continue
99
+ } else if ch == '"' {
100
+ quoted = false
101
+ return makeToken()
102
+ }
103
+ }
104
+ if ch == '\n' {
105
+ l.line++
106
+ }
107
+ if escaped {
108
+ // only escape quotes and newlines
109
+ if ch != '"' && ch != '\n' {
110
+ val = append(val, '\\')
111
+ }
112
+ }
113
+ val = append(val, ch)
114
+ escaped = false
115
+ continue
116
+ }
117
+
118
+ if unicode.IsSpace(ch) {
119
+ if ch == '\r' {
120
+ continue
121
+ }
122
+ if ch == '\n' {
123
+ l.line++
124
+ comment = false
125
+ }
126
+ if len(val) > 0 {
127
+ return makeToken()
128
+ }
129
+ continue
130
+ }
131
+
132
+ if ch == '#' {
133
+ comment = true
134
+ }
135
+
136
+ if comment {
137
+ continue
138
+ }
139
+
140
+ if len(val) == 0 {
141
+ l.token = Token{Line: l.line}
142
+ if ch == '"' {
143
+ quoted = true
144
+ continue
145
+ }
146
+ }
147
+
148
+ val = append(val, ch)
149
+ }
150
+}
caddyconfig/caddyfile/lexer_test.go
new
+196
@@ -0,0 +1,196 @@
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
+
15
+package caddyfile
16
+
17
+import (
18
+ "log"
19
+ "strings"
20
+ "testing"
21
+)
22
+
23
+type lexerTestCase struct {
24
+ input string
25
+ expected []Token
26
+}
27
+
28
+func TestLexer(t *testing.T) {
29
+ testCases := []lexerTestCase{
30
+ {
31
+ input: `host:123`,
32
+ expected: []Token{
33
+ {Line: 1, Text: "host:123"},
34
+ },
35
+ },
36
+ {
37
+ input: `host:123
38
+
39
+ directive`,
40
+ expected: []Token{
41
+ {Line: 1, Text: "host:123"},
42
+ {Line: 3, Text: "directive"},
43
+ },
44
+ },
45
+ {
46
+ input: `host:123 {
47
+ directive
48
+ }`,
49
+ expected: []Token{
50
+ {Line: 1, Text: "host:123"},
51
+ {Line: 1, Text: "{"},
52
+ {Line: 2, Text: "directive"},
53
+ {Line: 3, Text: "}"},
54
+ },
55
+ },
56
+ {
57
+ input: `host:123 { directive }`,
58
+ expected: []Token{
59
+ {Line: 1, Text: "host:123"},
60
+ {Line: 1, Text: "{"},
61
+ {Line: 1, Text: "directive"},
62
+ {Line: 1, Text: "}"},
63
+ },
64
+ },
65
+ {
66
+ input: `host:123 {
67
+ #comment
68
+ directive
69
+ # comment
70
+ foobar # another comment
71
+ }`,
72
+ expected: []Token{
73
+ {Line: 1, Text: "host:123"},
74
+ {Line: 1, Text: "{"},
75
+ {Line: 3, Text: "directive"},
76
+ {Line: 5, Text: "foobar"},
77
+ {Line: 6, Text: "}"},
78
+ },
79
+ },
80
+ {
81
+ input: `a "quoted value" b
82
+ foobar`,
83
+ expected: []Token{
84
+ {Line: 1, Text: "a"},
85
+ {Line: 1, Text: "quoted value"},
86
+ {Line: 1, Text: "b"},
87
+ {Line: 2, Text: "foobar"},
88
+ },
89
+ },
90
+ {
91
+ input: `A "quoted \"value\" inside" B`,
92
+ expected: []Token{
93
+ {Line: 1, Text: "A"},
94
+ {Line: 1, Text: `quoted "value" inside`},
95
+ {Line: 1, Text: "B"},
96
+ },
97
+ },
98
+ {
99
+ input: "A \"newline \\\ninside\" quotes",
100
+ expected: []Token{
101
+ {Line: 1, Text: "A"},
102
+ {Line: 1, Text: "newline \ninside"},
103
+ {Line: 2, Text: "quotes"},
104
+ },
105
+ },
106
+ {
107
+ input: `"don't\escape"`,
108
+ expected: []Token{
109
+ {Line: 1, Text: `don't\escape`},
110
+ },
111
+ },
112
+ {
113
+ input: `"don't\\escape"`,
114
+ expected: []Token{
115
+ {Line: 1, Text: `don't\\escape`},
116
+ },
117
+ },
118
+ {
119
+ input: `A "quoted value with line
120
+ break inside" {
121
+ foobar
122
+ }`,
123
+ expected: []Token{
124
+ {Line: 1, Text: "A"},
125
+ {Line: 1, Text: "quoted value with line\n\t\t\t\t\tbreak inside"},
126
+ {Line: 2, Text: "{"},
127
+ {Line: 3, Text: "foobar"},
128
+ {Line: 4, Text: "}"},
129
+ },
130
+ },
131
+ {
132
+ input: `"C:\php\php-cgi.exe"`,
133
+ expected: []Token{
134
+ {Line: 1, Text: `C:\php\php-cgi.exe`},
135
+ },
136
+ },
137
+ {
138
+ input: `empty "" string`,
139
+ expected: []Token{
140
+ {Line: 1, Text: `empty`},
141
+ {Line: 1, Text: ``},
142
+ {Line: 1, Text: `string`},
143
+ },
144
+ },
145
+ {
146
+ input: "skip those\r\nCR characters",
147
+ expected: []Token{
148
+ {Line: 1, Text: "skip"},
149
+ {Line: 1, Text: "those"},
150
+ {Line: 2, Text: "CR"},
151
+ {Line: 2, Text: "characters"},
152
+ },
153
+ },
154
+ {
155
+ input: "\xEF\xBB\xBF:8080", // test with leading byte order mark
156
+ expected: []Token{
157
+ {Line: 1, Text: ":8080"},
158
+ },
159
+ },
160
+ }
161
+
162
+ for i, testCase := range testCases {
163
+ actual := tokenize(testCase.input)
164
+ lexerCompare(t, i, testCase.expected, actual)
165
+ }
166
+}
167
+
168
+func tokenize(input string) (tokens []Token) {
169
+ l := lexer{}
170
+ if err := l.load(strings.NewReader(input)); err != nil {
171
+ log.Printf("[ERROR] load failed: %v", err)
172
+ }
173
+ for l.next() {
174
+ tokens = append(tokens, l.token)
175
+ }
176
+ return
177
+}
178
+
179
+func lexerCompare(t *testing.T, n int, expected, actual []Token) {
180
+ if len(expected) != len(actual) {
181
+ t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
182
+ }
183
+
184
+ for i := 0; i < len(actual) && i < len(expected); i++ {
185
+ if actual[i].Line != expected[i].Line {
186
+ t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d",
187
+ n, i, expected[i].Text, expected[i].Line, actual[i].Line)
188
+ break
189
+ }
190
+ if actual[i].Text != expected[i].Text {
191
+ t.Errorf("Test case %d token %d: expected text '%s' but was '%s'",
192
+ n, i, expected[i].Text, actual[i].Text)
193
+ break
194
+ }
195
+ }
196
+}
caddyconfig/caddyfile/parse.go
new
+492
@@ -0,0 +1,492 @@
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
+
15
+package caddyfile
16
+
17
+import (
18
+ "io"
19
+ "log"
20
+ "os"
21
+ "path/filepath"
22
+ "strings"
23
+)
24
+
25
+// Parse parses the input just enough to group tokens, in
26
+// order, by server block. No further parsing is performed.
27
+// Server blocks are returned in the order in which they appear.
28
+// Directives that do not appear in validDirectives will cause
29
+// an error. If you do not want to check for valid directives,
30
+// pass in nil instead.
31
+func Parse(filename string, input io.Reader, validDirectives []string) ([]ServerBlock, error) {
32
+ tokens, err := allTokens(input)
33
+ if err != nil {
34
+ return nil, err
35
+ }
36
+ p := parser{Dispenser: NewDispenser(filename, tokens), validDirectives: validDirectives}
37
+ return p.parseAll()
38
+}
39
+
40
+// allTokens lexes the entire input, but does not parse it.
41
+// It returns all the tokens from the input, unstructured
42
+// and in order.
43
+func allTokens(input io.Reader) ([]Token, error) {
44
+ l := new(lexer)
45
+ err := l.load(input)
46
+ if err != nil {
47
+ return nil, err
48
+ }
49
+ var tokens []Token
50
+ for l.next() {
51
+ tokens = append(tokens, l.token)
52
+ }
53
+ return tokens, nil
54
+}
55
+
56
+type parser struct {
57
+ *Dispenser
58
+ block ServerBlock // current server block being parsed
59
+ validDirectives []string // a directive must be valid or it's an error
60
+ eof bool // if we encounter a valid EOF in a hard place
61
+ definedSnippets map[string][]Token
62
+}
63
+
64
+func (p *parser) parseAll() ([]ServerBlock, error) {
65
+ var blocks []ServerBlock
66
+
67
+ for p.Next() {
68
+ err := p.parseOne()
69
+ if err != nil {
70
+ return blocks, err
71
+ }
72
+ if len(p.block.Keys) > 0 {
73
+ blocks = append(blocks, p.block)
74
+ }
75
+ }
76
+
77
+ return blocks, nil
78
+}
79
+
80
+func (p *parser) parseOne() error {
81
+ p.block = ServerBlock{Tokens: make(map[string][]Token)}
82
+
83
+ return p.begin()
84
+}
85
+
86
+func (p *parser) begin() error {
87
+ if len(p.tokens) == 0 {
88
+ return nil
89
+ }
90
+
91
+ err := p.addresses()
92
+
93
+ if err != nil {
94
+ return err
95
+ }
96
+
97
+ if p.eof {
98
+ // this happens if the Caddyfile consists of only
99
+ // a line of addresses and nothing else
100
+ return nil
101
+ }
102
+
103
+ if ok, name := p.isSnippet(); ok {
104
+ if p.definedSnippets == nil {
105
+ p.definedSnippets = map[string][]Token{}
106
+ }
107
+ if _, found := p.definedSnippets[name]; found {
108
+ return p.Errf("redeclaration of previously declared snippet %s", name)
109
+ }
110
+ // consume all tokens til matched close brace
111
+ tokens, err := p.snippetTokens()
112
+ if err != nil {
113
+ return err
114
+ }
115
+ p.definedSnippets[name] = tokens
116
+ // empty block keys so we don't save this block as a real server.
117
+ p.block.Keys = nil
118
+ return nil
119
+ }
120
+
121
+ return p.blockContents()
122
+}
123
+
124
+func (p *parser) addresses() error {
125
+ var expectingAnother bool
126
+
127
+ for {
128
+ tkn := replaceEnvVars(p.Val())
129
+
130
+ // special case: import directive replaces tokens during parse-time
131
+ if tkn == "import" && p.isNewLine() {
132
+ err := p.doImport()
133
+ if err != nil {
134
+ return err
135
+ }
136
+ continue
137
+ }
138
+
139
+ // Open brace definitely indicates end of addresses
140
+ if tkn == "{" {
141
+ if expectingAnother {
142
+ return p.Errf("Expected another address but had '%s' - check for extra comma", tkn)
143
+ }
144
+ break
145
+ }
146
+
147
+ if tkn != "" { // empty token possible if user typed ""
148
+ // Trailing comma indicates another address will follow, which
149
+ // may possibly be on the next line
150
+ if tkn[len(tkn)-1] == ',' {
151
+ tkn = tkn[:len(tkn)-1]
152
+ expectingAnother = true
153
+ } else {
154
+ expectingAnother = false // but we may still see another one on this line
155
+ }
156
+
157
+ p.block.Keys = append(p.block.Keys, tkn)
158
+ }
159
+
160
+ // Advance token and possibly break out of loop or return error
161
+ hasNext := p.Next()
162
+ if expectingAnother && !hasNext {
163
+ return p.EOFErr()
164
+ }
165
+ if !hasNext {
166
+ p.eof = true
167
+ break // EOF
168
+ }
169
+ if !expectingAnother && p.isNewLine() {
170
+ break
171
+ }
172
+ }
173
+
174
+ return nil
175
+}
176
+
177
+func (p *parser) blockContents() error {
178
+ errOpenCurlyBrace := p.openCurlyBrace()
179
+ if errOpenCurlyBrace != nil {
180
+ // single-server configs don't need curly braces
181
+ p.cursor--
182
+ }
183
+
184
+ err := p.directives()
185
+ if err != nil {
186
+ return err
187
+ }
188
+
189
+ // Only look for close curly brace if there was an opening
190
+ if errOpenCurlyBrace == nil {
191
+ err = p.closeCurlyBrace()
192
+ if err != nil {
193
+ return err
194
+ }
195
+ }
196
+
197
+ return nil
198
+}
199
+
200
+// directives parses through all the lines for directives
201
+// and it expects the next token to be the first
202
+// directive. It goes until EOF or closing curly brace
203
+// which ends the server block.
204
+func (p *parser) directives() error {
205
+ for p.Next() {
206
+ // end of server block
207
+ if p.Val() == "}" {
208
+ break
209
+ }
210
+
211
+ // special case: import directive replaces tokens during parse-time
212
+ if p.Val() == "import" {
213
+ err := p.doImport()
214
+ if err != nil {
215
+ return err
216
+ }
217
+ p.cursor-- // cursor is advanced when we continue, so roll back one more
218
+ continue
219
+ }
220
+
221
+ // normal case: parse a directive on this line
222
+ if err := p.directive(); err != nil {
223
+ return err
224
+ }
225
+ }
226
+ return nil
227
+}
228
+
229
+// doImport swaps out the import directive and its argument
230
+// (a total of 2 tokens) with the tokens in the specified file
231
+// or globbing pattern. When the function returns, the cursor
232
+// is on the token before where the import directive was. In
233
+// other words, call Next() to access the first token that was
234
+// imported.
235
+func (p *parser) doImport() error {
236
+ // syntax checks
237
+ if !p.NextArg() {
238
+ return p.ArgErr()
239
+ }
240
+ importPattern := replaceEnvVars(p.Val())
241
+ if importPattern == "" {
242
+ return p.Err("Import requires a non-empty filepath")
243
+ }
244
+ if p.NextArg() {
245
+ return p.Err("Import takes only one argument (glob pattern or file)")
246
+ }
247
+ // splice out the import directive and its argument (2 tokens total)
248
+ tokensBefore := p.tokens[:p.cursor-1]
249
+ tokensAfter := p.tokens[p.cursor+1:]
250
+ var importedTokens []Token
251
+
252
+ // first check snippets. That is a simple, non-recursive replacement
253
+ if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil {
254
+ importedTokens = p.definedSnippets[importPattern]
255
+ } else {
256
+ // make path relative to the file of the _token_ being processed rather
257
+ // than current working directory (issue #867) and then use glob to get
258
+ // list of matching filenames
259
+ absFile, err := filepath.Abs(p.Dispenser.File())
260
+ if err != nil {
261
+ return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
262
+ }
263
+
264
+ var matches []string
265
+ var globPattern string
266
+ if !filepath.IsAbs(importPattern) {
267
+ globPattern = filepath.Join(filepath.Dir(absFile), importPattern)
268
+ } else {
269
+ globPattern = importPattern
270
+ }
271
+ if strings.Count(globPattern, "*") > 1 || strings.Count(globPattern, "?") > 1 ||
272
+ (strings.Contains(globPattern, "[") && strings.Contains(globPattern, "]")) {
273
+ // See issue #2096 - a pattern with many glob expansions can hang for too long
274
+ return p.Errf("Glob pattern may only contain one wildcard (*), but has others: %s", globPattern)
275
+ }
276
+ matches, err = filepath.Glob(globPattern)
277
+
278
+ if err != nil {
279
+ return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
280
+ }
281
+ if len(matches) == 0 {
282
+ if strings.ContainsAny(globPattern, "*?[]") {
283
+ log.Printf("[WARNING] No files matching import glob pattern: %s", importPattern)
284
+ } else {
285
+ return p.Errf("File to import not found: %s", importPattern)
286
+ }
287
+ }
288
+
289
+ // collect all the imported tokens
290
+
291
+ for _, importFile := range matches {
292
+ newTokens, err := p.doSingleImport(importFile)
293
+ if err != nil {
294
+ return err
295
+ }
296
+ importedTokens = append(importedTokens, newTokens...)
297
+ }
298
+ }
299
+
300
+ // splice the imported tokens in the place of the import statement
301
+ // and rewind cursor so Next() will land on first imported token
302
+ p.tokens = append(tokensBefore, append(importedTokens, tokensAfter...)...)
303
+ p.cursor--
304
+
305
+ return nil
306
+}
307
+
308
+// doSingleImport lexes the individual file at importFile and returns
309
+// its tokens or an error, if any.
310
+func (p *parser) doSingleImport(importFile string) ([]Token, error) {
311
+ file, err := os.Open(importFile)
312
+ if err != nil {
313
+ return nil, p.Errf("Could not import %s: %v", importFile, err)
314
+ }
315
+ defer file.Close()
316
+
317
+ if info, err := file.Stat(); err != nil {
318
+ return nil, p.Errf("Could not import %s: %v", importFile, err)
319
+ } else if info.IsDir() {
320
+ return nil, p.Errf("Could not import %s: is a directory", importFile)
321
+ }
322
+
323
+ importedTokens, err := allTokens(file)
324
+ if err != nil {
325
+ return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err)
326
+ }
327
+
328
+ // Tack the file path onto these tokens so errors show the imported file's name
329
+ // (we use full, absolute path to avoid bugs: issue #1892)
330
+ filename, err := filepath.Abs(importFile)
331
+ if err != nil {
332
+ return nil, p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
333
+ }
334
+ for i := 0; i < len(importedTokens); i++ {
335
+ importedTokens[i].File = filename
336
+ }
337
+
338
+ return importedTokens, nil
339
+}
340
+
341
+// directive collects tokens until the directive's scope
342
+// closes (either end of line or end of curly brace block).
343
+// It expects the currently-loaded token to be a directive
344
+// (or } that ends a server block). The collected tokens
345
+// are loaded into the current server block for later use
346
+// by directive setup functions.
347
+func (p *parser) directive() error {
348
+ dir := replaceEnvVars(p.Val())
349
+ nesting := 0
350
+
351
+ if !p.validDirective(dir) {
352
+ return p.Errf("Unknown directive '%s'", dir)
353
+ }
354
+
355
+ // The directive itself is appended as a relevant token
356
+ p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
357
+
358
+ for p.Next() {
359
+ if p.Val() == "{" {
360
+ nesting++
361
+ } else if p.isNewLine() && nesting == 0 {
362
+ p.cursor-- // read too far
363
+ break
364
+ } else if p.Val() == "}" && nesting > 0 {
365
+ nesting--
366
+ } else if p.Val() == "}" && nesting == 0 {
367
+ return p.Err("Unexpected '}' because no matching opening brace")
368
+ } else if p.Val() == "import" && p.isNewLine() {
369
+ if err := p.doImport(); err != nil {
370
+ return err
371
+ }
372
+ p.cursor-- // cursor is advanced when we continue, so roll back one more
373
+ continue
374
+ }
375
+ p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text)
376
+ p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
377
+ }
378
+
379
+ if nesting > 0 {
380
+ return p.EOFErr()
381
+ }
382
+ return nil
383
+}
384
+
385
+// openCurlyBrace expects the current token to be an
386
+// opening curly brace. This acts like an assertion
387
+// because it returns an error if the token is not
388
+// a opening curly brace. It does NOT advance the token.
389
+func (p *parser) openCurlyBrace() error {
390
+ if p.Val() != "{" {
391
+ return p.SyntaxErr("{")
392
+ }
393
+ return nil
394
+}
395
+
396
+// closeCurlyBrace expects the current token to be
397
+// a closing curly brace. This acts like an assertion
398
+// because it returns an error if the token is not
399
+// a closing curly brace. It does NOT advance the token.
400
+func (p *parser) closeCurlyBrace() error {
401
+ if p.Val() != "}" {
402
+ return p.SyntaxErr("}")
403
+ }
404
+ return nil
405
+}
406
+
407
+// validDirective returns true if dir is in p.validDirectives.
408
+func (p *parser) validDirective(dir string) bool {
409
+ if p.validDirectives == nil {
410
+ return true
411
+ }
412
+ for _, d := range p.validDirectives {
413
+ if d == dir {
414
+ return true
415
+ }
416
+ }
417
+ return false
418
+}
419
+
420
+// replaceEnvVars replaces environment variables that appear in the token
421
+// and understands both the $UNIX and %WINDOWS% syntaxes.
422
+func replaceEnvVars(s string) string {
423
+ s = replaceEnvReferences(s, "{%", "%}")
424
+ s = replaceEnvReferences(s, "{$", "}")
425
+ return s
426
+}
427
+
428
+// replaceEnvReferences performs the actual replacement of env variables
429
+// in s, given the placeholder start and placeholder end strings.
430
+func replaceEnvReferences(s, refStart, refEnd string) string {
431
+ index := strings.Index(s, refStart)
432
+ for index != -1 {
433
+ endIndex := strings.Index(s[index:], refEnd)
434
+ if endIndex == -1 {
435
+ break
436
+ }
437
+
438
+ endIndex += index
439
+ if endIndex > index+len(refStart) {
440
+ ref := s[index : endIndex+len(refEnd)]
441
+ s = strings.Replace(s, ref, os.Getenv(ref[len(refStart):len(ref)-len(refEnd)]), -1)
442
+ } else {
443
+ return s
444
+ }
445
+ index = strings.Index(s, refStart)
446
+ }
447
+ return s
448
+}
449
+
450
+// ServerBlock associates any number of keys (usually addresses
451
+// of some sort) with tokens (grouped by directive name).
452
+type ServerBlock struct {
453
+ Keys []string
454
+ Tokens map[string][]Token
455
+}
456
+
457
+func (p *parser) isSnippet() (bool, string) {
458
+ keys := p.block.Keys
459
+ // A snippet block is a single key with parens. Nothing else qualifies.
460
+ if len(keys) == 1 && strings.HasPrefix(keys[0], "(") && strings.HasSuffix(keys[0], ")") {
461
+ return true, strings.TrimSuffix(keys[0][1:], ")")
462
+ }
463
+ return false, ""
464
+}
465
+
466
+// read and store everything in a block for later replay.
467
+func (p *parser) snippetTokens() ([]Token, error) {
468
+ // snippet must have curlies.
469
+ err := p.openCurlyBrace()
470
+ if err != nil {
471
+ return nil, err
472
+ }
473
+ count := 1
474
+ tokens := []Token{}
475
+ for p.Next() {
476
+ if p.Val() == "}" {
477
+ count--
478
+ if count == 0 {
479
+ break
480
+ }
481
+ }
482
+ if p.Val() == "{" {
483
+ count++
484
+ }
485
+ tokens = append(tokens, p.tokens[p.cursor])
486
+ }
487
+ // make sure we're matched up
488
+ if count != 0 {
489
+ return nil, p.SyntaxErr("}")
490
+ }
491
+ return tokens, nil
492
+}
caddyconfig/caddyfile/parse_test.go
new
+718
@@ -0,0 +1,718 @@
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
+
15
+package caddyfile
16
+
17
+import (
18
+ "io/ioutil"
19
+ "os"
20
+ "path/filepath"
21
+ "strings"
22
+ "testing"
23
+)
24
+
25
+func TestAllTokens(t *testing.T) {
26
+ input := strings.NewReader("a b c\nd e")
27
+ expected := []string{"a", "b", "c", "d", "e"}
28
+ tokens, err := allTokens(input)
29
+
30
+ if err != nil {
31
+ t.Fatalf("Expected no error, got %v", err)
32
+ }
33
+ if len(tokens) != len(expected) {
34
+ t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens))
35
+ }
36
+
37
+ for i, val := range expected {
38
+ if tokens[i].Text != val {
39
+ t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].Text)
40
+ }
41
+ }
42
+}
43
+
44
+func TestParseOneAndImport(t *testing.T) {
45
+ testParseOne := func(input string) (ServerBlock, error) {
46
+ p := testParser(input)
47
+ p.Next() // parseOne doesn't call Next() to start, so we must
48
+ err := p.parseOne()
49
+ return p.block, err
50
+ }
51
+
52
+ for i, test := range []struct {
53
+ input string
54
+ shouldErr bool
55
+ keys []string
56
+ tokens map[string]int // map of directive name to number of tokens expected
57
+ }{
58
+ {`localhost`, false, []string{
59
+ "localhost",
60
+ }, map[string]int{}},
61
+
62
+ {`localhost
63
+ dir1`, false, []string{
64
+ "localhost",
65
+ }, map[string]int{
66
+ "dir1": 1,
67
+ }},
68
+
69
+ {`localhost:1234
70
+ dir1 foo bar`, false, []string{
71
+ "localhost:1234",
72
+ }, map[string]int{
73
+ "dir1": 3,
74
+ }},
75
+
76
+ {`localhost {
77
+ dir1
78
+ }`, false, []string{
79
+ "localhost",
80
+ }, map[string]int{
81
+ "dir1": 1,
82
+ }},
83
+
84
+ {`localhost:1234 {
85
+ dir1 foo bar
86
+ dir2
87
+ }`, false, []string{
88
+ "localhost:1234",
89
+ }, map[string]int{
90
+ "dir1": 3,
91
+ "dir2": 1,
92
+ }},
93
+
94
+ {`http://localhost https://localhost
95
+ dir1 foo bar`, false, []string{
96
+ "http://localhost",
97
+ "https://localhost",
98
+ }, map[string]int{
99
+ "dir1": 3,
100
+ }},
101
+
102
+ {`http://localhost https://localhost {
103
+ dir1 foo bar
104
+ }`, false, []string{
105
+ "http://localhost",
106
+ "https://localhost",
107
+ }, map[string]int{
108
+ "dir1": 3,
109
+ }},
110
+
111
+ {`http://localhost, https://localhost {
112
+ dir1 foo bar
113
+ }`, false, []string{
114
+ "http://localhost",
115
+ "https://localhost",
116
+ }, map[string]int{
117
+ "dir1": 3,
118
+ }},
119
+
120
+ {`http://localhost, {
121
+ }`, true, []string{
122
+ "http://localhost",
123
+ }, map[string]int{}},
124
+
125
+ {`host1:80, http://host2.com
126
+ dir1 foo bar
127
+ dir2 baz`, false, []string{
128
+ "host1:80",
129
+ "http://host2.com",
130
+ }, map[string]int{
131
+ "dir1": 3,
132
+ "dir2": 2,
133
+ }},
134
+
135
+ {`http://host1.com,
136
+ http://host2.com,
137
+ https://host3.com`, false, []string{
138
+ "http://host1.com",
139
+ "http://host2.com",
140
+ "https://host3.com",
141
+ }, map[string]int{}},
142
+
143
+ {`http://host1.com:1234, https://host2.com
144
+ dir1 foo {
145
+ bar baz
146
+ }
147
+ dir2`, false, []string{
148
+ "http://host1.com:1234",
149
+ "https://host2.com",
150
+ }, map[string]int{
151
+ "dir1": 6,
152
+ "dir2": 1,
153
+ }},
154
+
155
+ {`127.0.0.1
156
+ dir1 {
157
+ bar baz
158
+ }
159
+ dir2 {
160
+ foo bar
161
+ }`, false, []string{
162
+ "127.0.0.1",
163
+ }, map[string]int{
164
+ "dir1": 5,
165
+ "dir2": 5,
166
+ }},
167
+
168
+ {`localhost
169
+ dir1 {
170
+ foo`, true, []string{
171
+ "localhost",
172
+ }, map[string]int{
173
+ "dir1": 3,
174
+ }},
175
+
176
+ {`localhost
177
+ dir1 {
178
+ }`, false, []string{
179
+ "localhost",
180
+ }, map[string]int{
181
+ "dir1": 3,
182
+ }},
183
+
184
+ {`localhost
185
+ dir1 {
186
+ } }`, true, []string{
187
+ "localhost",
188
+ }, map[string]int{
189
+ "dir1": 3,
190
+ }},
191
+
192
+ {`localhost
193
+ dir1 {
194
+ nested {
195
+ foo
196
+ }
197
+ }
198
+ dir2 foo bar`, false, []string{
199
+ "localhost",
200
+ }, map[string]int{
201
+ "dir1": 7,
202
+ "dir2": 3,
203
+ }},
204
+
205
+ {``, false, []string{}, map[string]int{}},
206
+
207
+ {`localhost
208
+ dir1 arg1
209
+ import testdata/import_test1.txt`, false, []string{
210
+ "localhost",
211
+ }, map[string]int{
212
+ "dir1": 2,
213
+ "dir2": 3,
214
+ "dir3": 1,
215
+ }},
216
+
217
+ {`import testdata/import_test2.txt`, false, []string{
218
+ "host1",
219
+ }, map[string]int{
220
+ "dir1": 1,
221
+ "dir2": 2,
222
+ }},
223
+
224
+ {`import testdata/import_test1.txt testdata/import_test2.txt`, true, []string{}, map[string]int{}},
225
+
226
+ {`import testdata/not_found.txt`, true, []string{}, map[string]int{}},
227
+
228
+ {`""`, false, []string{}, map[string]int{}},
229
+
230
+ {``, false, []string{}, map[string]int{}},
231
+
232
+ // test cases found by fuzzing!
233
+ {`import }{$"`, true, []string{}, map[string]int{}},
234
+ {`import /*/*.txt`, true, []string{}, map[string]int{}},
235
+ {`import /???/?*?o`, true, []string{}, map[string]int{}},
236
+ {`import /??`, true, []string{}, map[string]int{}},
237
+ {`import /[a-z]`, true, []string{}, map[string]int{}},
238
+ {`import {$}`, true, []string{}, map[string]int{}},
239
+ {`import {%}`, true, []string{}, map[string]int{}},
240
+ {`import {$$}`, true, []string{}, map[string]int{}},
241
+ {`import {%%}`, true, []string{}, map[string]int{}},
242
+ } {
243
+ result, err := testParseOne(test.input)
244
+
245
+ if test.shouldErr && err == nil {
246
+ t.Errorf("Test %d: Expected an error, but didn't get one", i)
247
+ }
248
+ if !test.shouldErr && err != nil {
249
+ t.Errorf("Test %d: Expected no error, but got: %v", i, err)
250
+ }
251
+
252
+ if len(result.Keys) != len(test.keys) {
253
+ t.Errorf("Test %d: Expected %d keys, got %d",
254
+ i, len(test.keys), len(result.Keys))
255
+ continue
256
+ }
257
+ for j, addr := range result.Keys {
258
+ if addr != test.keys[j] {
259
+ t.Errorf("Test %d, key %d: Expected '%s', but was '%s'",
260
+ i, j, test.keys[j], addr)
261
+ }
262
+ }
263
+
264
+ if len(result.Tokens) != len(test.tokens) {
265
+ t.Errorf("Test %d: Expected %d directives, had %d",
266
+ i, len(test.tokens), len(result.Tokens))
267
+ continue
268
+ }
269
+ for directive, tokens := range result.Tokens {
270
+ if len(tokens) != test.tokens[directive] {
271
+ t.Errorf("Test %d, directive '%s': Expected %d tokens, counted %d",
272
+ i, directive, test.tokens[directive], len(tokens))
273
+ continue
274
+ }
275
+ }
276
+ }
277
+}
278
+
279
+func TestRecursiveImport(t *testing.T) {
280
+ testParseOne := func(input string) (ServerBlock, error) {
281
+ p := testParser(input)
282
+ p.Next() // parseOne doesn't call Next() to start, so we must
283
+ err := p.parseOne()
284
+ return p.block, err
285
+ }
286
+
287
+ isExpected := func(got ServerBlock) bool {
288
+ if len(got.Keys) != 1 || got.Keys[0] != "localhost" {
289
+ t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys)
290
+ return false
291
+ }
292
+ if len(got.Tokens) != 2 {
293
+ t.Errorf("got wrong number of tokens: expect 2, got %d", len(got.Tokens))
294
+ return false
295
+ }
296
+ if len(got.Tokens["dir1"]) != 1 || len(got.Tokens["dir2"]) != 2 {
297
+ t.Errorf("got unexpect tokens: %v", got.Tokens)
298
+ return false
299
+ }
300
+ return true
301
+ }
302
+
303
+ recursiveFile1, err := filepath.Abs("testdata/recursive_import_test1")
304
+ if err != nil {
305
+ t.Fatal(err)
306
+ }
307
+ recursiveFile2, err := filepath.Abs("testdata/recursive_import_test2")
308
+ if err != nil {
309
+ t.Fatal(err)
310
+ }
311
+
312
+ // test relative recursive import
313
+ err = ioutil.WriteFile(recursiveFile1, []byte(
314
+ `localhost
315
+ dir1
316
+ import recursive_import_test2`), 0644)
317
+ if err != nil {
318
+ t.Fatal(err)
319
+ }
320
+ defer os.Remove(recursiveFile1)
321
+
322
+ err = ioutil.WriteFile(recursiveFile2, []byte("dir2 1"), 0644)
323
+ if err != nil {
324
+ t.Fatal(err)
325
+ }
326
+ defer os.Remove(recursiveFile2)
327
+
328
+ // import absolute path
329
+ result, err := testParseOne("import " + recursiveFile1)
330
+ if err != nil {
331
+ t.Fatal(err)
332
+ }
333
+ if !isExpected(result) {
334
+ t.Error("absolute+relative import failed")
335
+ }
336
+
337
+ // import relative path
338
+ result, err = testParseOne("import testdata/recursive_import_test1")
339
+ if err != nil {
340
+ t.Fatal(err)
341
+ }
342
+ if !isExpected(result) {
343
+ t.Error("relative+relative import failed")
344
+ }
345
+
346
+ // test absolute recursive import
347
+ err = ioutil.WriteFile(recursiveFile1, []byte(
348
+ `localhost
349
+ dir1
350
+ import `+recursiveFile2), 0644)
351
+ if err != nil {
352
+ t.Fatal(err)
353
+ }
354
+
355
+ // import absolute path
356
+ result, err = testParseOne("import " + recursiveFile1)
357
+ if err != nil {
358
+ t.Fatal(err)
359
+ }
360
+ if !isExpected(result) {
361
+ t.Error("absolute+absolute import failed")
362
+ }
363
+
364
+ // import relative path
365
+ result, err = testParseOne("import testdata/recursive_import_test1")
366
+ if err != nil {
367
+ t.Fatal(err)
368
+ }
369
+ if !isExpected(result) {
370
+ t.Error("relative+absolute import failed")
371
+ }
372
+}
373
+
374
+func TestDirectiveImport(t *testing.T) {
375
+ testParseOne := func(input string) (ServerBlock, error) {
376
+ p := testParser(input)
377
+ p.Next() // parseOne doesn't call Next() to start, so we must
378
+ err := p.parseOne()
379
+ return p.block, err
380
+ }
381
+
382
+ isExpected := func(got ServerBlock) bool {
383
+ if len(got.Keys) != 1 || got.Keys[0] != "localhost" {
384
+ t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys)
385
+ return false
386
+ }
387
+ if len(got.Tokens) != 2 {
388
+ t.Errorf("got wrong number of tokens: expect 2, got %d", len(got.Tokens))
389
+ return false
390
+ }
391
+ if len(got.Tokens["dir1"]) != 1 || len(got.Tokens["proxy"]) != 8 {
392
+ t.Errorf("got unexpect tokens: %v", got.Tokens)
393
+ return false
394
+ }
395
+ return true
396
+ }
397
+
398
+ directiveFile, err := filepath.Abs("testdata/directive_import_test")
399
+ if err != nil {
400
+ t.Fatal(err)
401
+ }
402
+
403
+ err = ioutil.WriteFile(directiveFile, []byte(`prop1 1
404
+ prop2 2`), 0644)
405
+ if err != nil {
406
+ t.Fatal(err)
407
+ }
408
+ defer os.Remove(directiveFile)
409
+
410
+ // import from existing file
411
+ result, err := testParseOne(`localhost
412
+ dir1
413
+ proxy {
414
+ import testdata/directive_import_test
415
+ transparent
416
+ }`)
417
+ if err != nil {
418
+ t.Fatal(err)
419
+ }
420
+ if !isExpected(result) {
421
+ t.Error("directive import failed")
422
+ }
423
+
424
+ // import from nonexistent file
425
+ _, err = testParseOne(`localhost
426
+ dir1
427
+ proxy {
428
+ import testdata/nonexistent_file
429
+ transparent
430
+ }`)
431
+ if err == nil {
432
+ t.Fatal("expected error when importing a nonexistent file")
433
+ }
434
+}
435
+
436
+func TestParseAll(t *testing.T) {
437
+ for i, test := range []struct {
438
+ input string
439
+ shouldErr bool
440
+ keys [][]string // keys per server block, in order
441
+ }{
442
+ {`localhost`, false, [][]string{
443
+ {"localhost"},
444
+ }},
445
+
446
+ {`localhost:1234`, false, [][]string{
447
+ {"localhost:1234"},
448
+ }},
449
+
450
+ {`localhost:1234 {
451
+ }
452
+ localhost:2015 {
453
+ }`, false, [][]string{
454
+ {"localhost:1234"},
455
+ {"localhost:2015"},
456
+ }},
457
+
458
+ {`localhost:1234, http://host2`, false, [][]string{
459
+ {"localhost:1234", "http://host2"},
460
+ }},
461
+
462
+ {`localhost:1234, http://host2,`, true, [][]string{}},
463
+
464
+ {`http://host1.com, http://host2.com {
465
+ }
466
+ https://host3.com, https://host4.com {
467
+ }`, false, [][]string{
468
+ {"http://host1.com", "http://host2.com"},
469
+ {"https://host3.com", "https://host4.com"},
470
+ }},
471
+
472
+ {`import testdata/import_glob*.txt`, false, [][]string{
473
+ {"glob0.host0"},
474
+ {"glob0.host1"},
475
+ {"glob1.host0"},
476
+ {"glob2.host0"},
477
+ }},
478
+
479
+ {`import notfound/*`, false, [][]string{}}, // glob needn't error with no matches
480
+ {`import notfound/file.conf`, true, [][]string{}}, // but a specific file should
481
+ } {
482
+ p := testParser(test.input)
483
+ blocks, err := p.parseAll()
484
+
485
+ if test.shouldErr && err == nil {
486
+ t.Errorf("Test %d: Expected an error, but didn't get one", i)
487
+ }
488
+ if !test.shouldErr && err != nil {
489
+ t.Errorf("Test %d: Expected no error, but got: %v", i, err)
490
+ }
491
+
492
+ if len(blocks) != len(test.keys) {
493
+ t.Errorf("Test %d: Expected %d server blocks, got %d",
494
+ i, len(test.keys), len(blocks))
495
+ continue
496
+ }
497
+ for j, block := range blocks {
498
+ if len(block.Keys) != len(test.keys[j]) {
499
+ t.Errorf("Test %d: Expected %d keys in block %d, got %d",
500
+ i, len(test.keys[j]), j, len(block.Keys))
501
+ continue
502
+ }
503
+ for k, addr := range block.Keys {
504
+ if addr != test.keys[j][k] {
505
+ t.Errorf("Test %d, block %d, key %d: Expected '%s', but got '%s'",
506
+ i, j, k, test.keys[j][k], addr)
507
+ }
508
+ }
509
+ }
510
+ }
511
+}
512
+
513
+func TestEnvironmentReplacement(t *testing.T) {
514
+ os.Setenv("PORT", "8080")
515
+ os.Setenv("ADDRESS", "servername.com")
516
+ os.Setenv("FOOBAR", "foobar")
517
+ os.Setenv("PARTIAL_DIR", "r1")
518
+
519
+ // basic test; unix-style env vars
520
+ p := testParser(`{$ADDRESS}`)
521
+ blocks, _ := p.parseAll()
522
+ if actual, expected := blocks[0].Keys[0], "servername.com"; expected != actual {
523
+ t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
524
+ }
525
+
526
+ // basic test; unix-style env vars
527
+ p = testParser(`di{$PARTIAL_DIR}`)
528
+ blocks, _ = p.parseAll()
529
+ if actual, expected := blocks[0].Keys[0], "dir1"; expected != actual {
530
+ t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
531
+ }
532
+
533
+ // multiple vars per token
534
+ p = testParser(`{$ADDRESS}:{$PORT}`)
535
+ blocks, _ = p.parseAll()
536
+ if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
537
+ t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
538
+ }
539
+
540
+ // windows-style var and unix style in same token
541
+ p = testParser(`{%ADDRESS%}:{$PORT}`)
542
+ blocks, _ = p.parseAll()
543
+ if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
544
+ t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
545
+ }
546
+
547
+ // reverse order
548
+ p = testParser(`{$ADDRESS}:{%PORT%}`)
549
+ blocks, _ = p.parseAll()
550
+ if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
551
+ t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
552
+ }
553
+
554
+ // env var in server block body as argument
555
+ p = testParser(":{%PORT%}\ndir1 {$FOOBAR}")
556
+ blocks, _ = p.parseAll()
557
+ if actual, expected := blocks[0].Keys[0], ":8080"; expected != actual {
558
+ t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
559
+ }
560
+ if actual, expected := blocks[0].Tokens["dir1"][1].Text, "foobar"; expected != actual {
561
+ t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
562
+ }
563
+
564
+ // combined windows env vars in argument
565
+ p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}")
566
+ blocks, _ = p.parseAll()
567
+ if actual, expected := blocks[0].Tokens["dir1"][1].Text, "servername.com/foobar"; expected != actual {
568
+ t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
569
+ }
570
+
571
+ // malformed env var (windows)
572
+ p = testParser(":1234\ndir1 {%ADDRESS}")
573
+ blocks, _ = p.parseAll()
574
+ if actual, expected := blocks[0].Tokens["dir1"][1].Text, "{%ADDRESS}"; expected != actual {
575
+ t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
576
+ }
577
+
578
+ // malformed (non-existent) env var (unix)
579
+ p = testParser(`:{$PORT$}`)
580
+ blocks, _ = p.parseAll()
581
+ if actual, expected := blocks[0].Keys[0], ":"; expected != actual {
582
+ t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
583
+ }
584
+
585
+ // in quoted field
586
+ p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"")
587
+ blocks, _ = p.parseAll()
588
+ if actual, expected := blocks[0].Tokens["dir1"][1].Text, "Test foobar test"; expected != actual {
589
+ t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
590
+ }
591
+
592
+ // after end token
593
+ p = testParser(":1234\nanswer \"{{ .Name }} {$FOOBAR}\"")
594
+ blocks, _ = p.parseAll()
595
+ if actual, expected := blocks[0].Tokens["answer"][1].Text, "{{ .Name }} foobar"; expected != actual {
596
+ t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
597
+ }
598
+}
599
+
600
+func testParser(input string) parser {
601
+ return parser{Dispenser: newTestDispenser(input)}
602
+}
603
+
604
+func TestSnippets(t *testing.T) {
605
+ p := testParser(`
606
+ (common) {
607
+ gzip foo
608
+ errors stderr
609
+ }
610
+ http://example.com {
611
+ import common
612
+ }
613
+ `)
614
+ blocks, err := p.parseAll()
615
+ if err != nil {
616
+ t.Fatal(err)
617
+ }
618
+ for _, b := range blocks {
619
+ t.Log(b.Keys)
620
+ t.Log(b.Tokens)
621
+ }
622
+ if len(blocks) != 1 {
623
+ t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
624
+ }
625
+ if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual {
626
+ t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
627
+ }
628
+ if len(blocks[0].Tokens) != 2 {
629
+ t.Fatalf("Server block should have tokens from import")
630
+ }
631
+ if actual, expected := blocks[0].Tokens["gzip"][0].Text, "gzip"; expected != actual {
632
+ t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
633
+ }
634
+ if actual, expected := blocks[0].Tokens["errors"][1].Text, "stderr"; expected != actual {
635
+ t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
636
+ }
637
+
638
+}
639
+
640
+func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) {
641
+ file, err := ioutil.TempFile("", t.Name())
642
+ if err != nil {
643
+ panic(err) // get a stack trace so we know where this was called from.
644
+ }
645
+ if _, err := file.WriteString(str); err != nil {
646
+ panic(err)
647
+ }
648
+ if err := file.Close(); err != nil {
649
+ panic(err)
650
+ }
651
+ return file.Name()
652
+}
653
+
654
+func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) {
655
+ fileName := writeStringToTempFileOrDie(t, `
656
+ http://example.com {
657
+ # This isn't an import directive, it's just an arg with value 'import'
658
+ basicauth / import password
659
+ }
660
+ `)
661
+ // Parse the root file that imports the other one.
662
+ p := testParser(`import ` + fileName)
663
+ blocks, err := p.parseAll()
664
+ if err != nil {
665
+ t.Fatal(err)
666
+ }
667
+ for _, b := range blocks {
668
+ t.Log(b.Keys)
669
+ t.Log(b.Tokens)
670
+ }
671
+ auth := blocks[0].Tokens["basicauth"]
672
+ line := auth[0].Text + " " + auth[1].Text + " " + auth[2].Text + " " + auth[3].Text
673
+ if line != "basicauth / import password" {
674
+ // Previously, it would be changed to:
675
+ // basicauth / import /path/to/test/dir/password
676
+ // referencing a file that (probably) doesn't exist and changing the
677
+ // password!
678
+ t.Errorf("Expected basicauth tokens to be 'basicauth / import password' but got %#q", line)
679
+ }
680
+}
681
+
682
+func TestSnippetAcrossMultipleFiles(t *testing.T) {
683
+ // Make the derived Caddyfile that expects (common) to be defined.
684
+ fileName := writeStringToTempFileOrDie(t, `
685
+ http://example.com {
686
+ import common
687
+ }
688
+ `)
689
+
690
+ // Parse the root file that defines (common) and then imports the other one.
691
+ p := testParser(`
692
+ (common) {
693
+ gzip foo
694
+ }
695
+ import ` + fileName + `
696
+ `)
697
+
698
+ blocks, err := p.parseAll()
699
+ if err != nil {
700
+ t.Fatal(err)
701
+ }
702
+ for _, b := range blocks {
703
+ t.Log(b.Keys)
704
+ t.Log(b.Tokens)
705
+ }
706
+ if len(blocks) != 1 {
707
+ t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
708
+ }
709
+ if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual {
710
+ t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
711
+ }
712
+ if len(blocks[0].Tokens) != 1 {
713
+ t.Fatalf("Server block should have tokens from import")
714
+ }
715
+ if actual, expected := blocks[0].Tokens["gzip"][0].Text, "gzip"; expected != actual {
716
+ t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
717
+ }
718
+}
caddyconfig/caddyfile/testdata/import_glob0.txt
new
+6
@@ -0,0 +1,6 @@
1
+glob0.host0 {
2
+ dir2 arg1
3
+}
4
+
5
+glob0.host1 {
6
+}
caddyconfig/caddyfile/testdata/import_glob1.txt
new
+4
@@ -0,0 +1,4 @@
1
+glob1.host0 {
2
+ dir1
3
+ dir2 arg1
4
+}
caddyconfig/caddyfile/testdata/import_glob2.txt
new
+3
@@ -0,0 +1,3 @@
1
+glob2.host0 {
2
+ dir2 arg1
3
+}
caddyconfig/caddyfile/testdata/import_test1.txt
new
+2
@@ -0,0 +1,2 @@
1
+dir2 arg1 arg2
2
+dir3
\ No newline at end of file
caddyconfig/caddyfile/testdata/import_test2.txt
new
+4
@@ -0,0 +1,4 @@
1
+host1 {
2
+ dir1
3
+ dir2 arg1
4
+}
\ No newline at end of file
caddyconfig/configadapters.go
new
+113
@@ -0,0 +1,113 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
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
+
15
+package caddyconfig
16
+
17
+import (
18
+ "encoding/json"
19
+ "fmt"
20
+)
21
+
22
+// Adapter is a type which can adapt a configuration to Caddy JSON.
23
+// It returns the results and any warnings, or an error.
24
+type Adapter interface {
25
+ Adapt(body []byte, options map[string]string) ([]byte, []Warning, error)
26
+}
27
+
28
+// Warning represents a warning or notice related to conversion.
29
+type Warning struct {
30
+ File string
31
+ Line int
32
+ Directive string
33
+ Message string
34
+}
35
+
36
+// JSON encodes val as JSON, returning it as a json.RawMessage. Any
37
+// marshaling errors (which are highly unlikely with correct code)
38
+// are converted to warnings. This is convenient when filling config
39
+// structs that require a json.RawMessage, without having to worry
40
+// about errors.
41
+func JSON(val interface{}, warnings *[]Warning) json.RawMessage {
42
+ b, err := json.Marshal(val)
43
+ if err != nil {
44
+ if warnings != nil {
45
+ *warnings = append(*warnings, Warning{Message: err.Error()})
46
+ }
47
+ return nil
48
+ }
49
+ return b
50
+}
51
+
52
+// JSONModuleObject is like JSON, except it marshals val into a JSON object
53
+// and then adds a key to that object named fieldName with the value fieldVal.
54
+// This is useful for JSON-encoding module values where the module name has to
55
+// be described within the object by a certain key; for example,
56
+// "responder": "file_server" for a file server HTTP responder. The val must
57
+// encode into a map[string]interface{} (i.e. it must be a struct or map),
58
+// and any errors are converted into warnings, so this can be conveniently
59
+// used when filling a struct. For correct code, there should be no errors.
60
+func JSONModuleObject(val interface{}, fieldName, fieldVal string, warnings *[]Warning) json.RawMessage {
61
+ // encode to a JSON object first
62
+ enc, err := json.Marshal(val)
63
+ if err != nil {
64
+ if warnings != nil {
65
+ *warnings = append(*warnings, Warning{Message: err.Error()})
66
+ }
67
+ return nil
68
+ }
69
+
70
+ // then decode the object
71
+ var tmp map[string]interface{}
72
+ err = json.Unmarshal(enc, &tmp)
73
+ if err != nil {
74
+ if warnings != nil {
75
+ *warnings = append(*warnings, Warning{Message: err.Error()})
76
+ }
77
+ return nil
78
+ }
79
+
80
+ // so we can easily add the module's field with its appointed value
81
+ tmp[fieldName] = fieldVal
82
+
83
+ // then re-marshal as JSON
84
+ result, err := json.Marshal(tmp)
85
+ if err != nil {
86
+ if warnings != nil {
87
+ *warnings = append(*warnings, Warning{Message: err.Error()})
88
+ }
89
+ return nil
90
+ }
91
+
92
+ return result
93
+}
94
+
95
+// JSONIndent is used to JSON-marshal the final resulting Caddy
96
+// configuration in a consistent, human-readable way.
97
+func JSONIndent(val interface{}) ([]byte, error) {
98
+ return json.MarshalIndent(val, "", "\t")
99
+}
100
+
101
+func RegisterAdapter(name string, adapter Adapter) error {
102
+ if _, ok := configAdapters[name]; ok {
103
+ return fmt.Errorf("%s: already registered", name)
104
+ }
105
+ configAdapters[name] = adapter
106
+ return nil
107
+}
108
+
109
+func GetAdapter(name string) Adapter {
110
+ return configAdapters[name]
111
+}
112
+
113
+var configAdapters = make(map[string]Adapter)
caddyconfig/httpcaddyfile/addresses.go
new
+350
@@ -0,0 +1,350 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
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
+
15
+package httpcaddyfile
16
+
17
+import (
18
+ "fmt"
19
+ "net"
20
+ "net/url"
21
+ "reflect"
22
+ "strconv"
23
+ "strings"
24
+
25
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
26
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
27
+ "github.com/mholt/certmagic"
28
+)
29
+
30
+// mapAddressToServerBlocks returns a map of listener address to list of server
31
+// blocks that will be served on that address. To do this, each server block is
32
+// expanded so that each one is considered individually, although keys of a
33
+// server block that share the same address stay grouped together so the config
34
+// isn't repeated unnecessarily. For example, this Caddyfile:
35
+//
36
+// example.com {
37
+// bind 127.0.0.1
38
+// }
39
+// www.example.com, example.net/path, localhost:9999 {
40
+// bind 127.0.0.1 1.2.3.4
41
+// }
42
+//
43
+// has two server blocks to start with. But expressed in this Caddyfile are
44
+// actually 4 listener addresses: 127.0.0.1:443, 1.2.3.4:443, 127.0.0.1:9999,
45
+// and 127.0.0.1:9999. This is because the bind directive is applied to each
46
+// key of its server block (specifying the host part), and each key may have
47
+// a different port. And we definitely need to be sure that a site which is
48
+// bound to be served on a specific interface is not served on others just
49
+// beceause that is more convenient: it would be a potential security risk
50
+// if the difference between interfaces means private vs. public.
51
+//
52
+// So what this function does for the example above is iterate each server
53
+// block, and for each server block, iterate its keys. For the first, it
54
+// finds one key (example.com) and determines its listener address
55
+// (127.0.0.1:443 - because of 'bind' and automatic HTTPS). It then adds
56
+// the listener address to the map value returned by this function, with
57
+// the first server block as one of its associations.
58
+//
59
+// It then iterates each key on the second server block and associates them
60
+// with one or more listener addresses. Indeed, each key in this block has
61
+// two listener addresses because of the 'bind' directive. Once we know
62
+// which addresses serve which keys, we can create a new server block for
63
+// each address containing the contents of the server block and only those
64
+// specific keys of the server block which use that address.
65
+//
66
+// It is possible and even likely that some keys in the returned map have
67
+// the exact same list of server blocks (i.e. they are identical). This
68
+// happens when multiple hosts are declared with a 'bind' directive and
69
+// the resulting listener addresses are not shared by any other server
70
+// block (or the other server blocks are exactly identical in their token
71
+// contents). This happens with our example above because 1.2.3.4:443
72
+// and 1.2.3.4:9999 are used exclusively with the second server block. This
73
+// repetition may be undesirable, so call consolidateAddrMappings() to map
74
+// multiple addresses to the same lists of server blocks (a many:many mapping).
75
+// (Doing this is essentially a map-reduce technique.)
76
+func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []caddyfile.ServerBlock) (map[string][]caddyfile.ServerBlock, error) {
77
+ sbmap := make(map[string][]caddyfile.ServerBlock)
78
+
79
+ for i, sblock := range originalServerBlocks {
80
+ // within a server block, we need to map all the listener addresses
81
+ // implied by the server block to the keys of the server block which
82
+ // will be served by them; this has the effect of treating each
83
+ // key of a server block as its own, but without having to repeat its
84
+ // contents in cases where multiple keys really can be served together
85
+ addrToKeys := make(map[string][]string)
86
+ for j, key := range sblock.Keys {
87
+ // a key can have multiple listener addresses if there are multiple
88
+ // arguments to the 'bind' directive (although they will all have
89
+ // the same port, since the port is defined by the key or is implicit
90
+ // through automatic HTTPS)
91
+ addrs, err := st.listenerAddrsForServerBlockKey(sblock, key)
92
+ if err != nil {
93
+ return nil, fmt.Errorf("server block %d, key %d (%s): determining listener address: %v", i, j, key, err)
94
+ }
95
+
96
+ // associate this key with each listener address it is served on
97
+ for _, addr := range addrs {
98
+ addrToKeys[addr] = append(addrToKeys[addr], key)
99
+ }
100
+ }
101
+
102
+ // now that we know which addresses serve which keys of this
103
+ // server block, we iterate that mapping and create a list of
104
+ // new server blocks for each address where the keys of the
105
+ // server block are only the ones which use the address; but
106
+ // the contents (tokens) are of course the same
107
+ for addr, keys := range addrToKeys {
108
+ sbmap[addr] = append(sbmap[addr], caddyfile.ServerBlock{
109
+ Keys: keys,
110
+ Tokens: sblock.Tokens,
111
+ })
112
+ }
113
+ }
114
+
115
+ return sbmap, nil
116
+}
117
+
118
+// consolidateAddrMappings eliminates repetition of identical server blocks in a mapping of
119
+// single listener addresses to lists of server blocks. Since multiple addresses may serve
120
+// identical sites (server block contents), this function turns a 1:many mapping into a
121
+// many:many mapping. Server block contents (tokens) must be exactly identical so that
122
+// reflect.DeepEqual returns true in order for the addresses to be combined. Identical
123
+// entries are deleted from the addrToServerBlocks map. Essentially, each pairing (each
124
+// association from multiple addresses to multiple server blocks; i.e. each element of
125
+// the returned slice) becomes a server definition in the output JSON.
126
+func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]caddyfile.ServerBlock) []sbAddrAssociation {
127
+ var sbaddrs []sbAddrAssociation
128
+ for addr, sblocks := range addrToServerBlocks {
129
+ // we start with knowing that at least this address
130
+ // maps to these server blocks
131
+ a := sbAddrAssociation{
132
+ addresses: []string{addr},
133
+ serverBlocks: sblocks,
134
+ }
135
+
136
+ // now find other addresses that map to identical
137
+ // server blocks and add them to our list of
138
+ // addresses, while removing them from the map
139
+ for otherAddr, otherSblocks := range addrToServerBlocks {
140
+ if addr == otherAddr {
141
+ continue
142
+ }
143
+ if reflect.DeepEqual(sblocks, otherSblocks) {
144
+ a.addresses = append(a.addresses, otherAddr)
145
+ delete(addrToServerBlocks, otherAddr)
146
+ }
147
+ }
148
+
149
+ sbaddrs = append(sbaddrs, a)
150
+ }
151
+ return sbaddrs
152
+}
153
+
154
+func (st *ServerType) listenerAddrsForServerBlockKey(sblock caddyfile.ServerBlock, key string) ([]string, error) {
155
+ addr, err := standardizeAddress(key)
156
+ if err != nil {
157
+ return nil, fmt.Errorf("parsing key: %v", err)
158
+ }
159
+
160
+ lnPort := defaultPort
161
+ if addr.Port != "" {
162
+ // port explicitly defined
163
+ lnPort = addr.Port
164
+ } else if certmagic.HostQualifies(addr.Host) {
165
+ // automatic HTTPS
166
+ lnPort = strconv.Itoa(certmagic.HTTPSPort)
167
+ }
168
+
169
+ // the bind directive specifies hosts, but is optional
170
+ var lnHosts []string
171
+ for i, token := range sblock.Tokens["bind"] {
172
+ if i == 0 {
173
+ continue
174
+ }
175
+ lnHosts = append(lnHosts, token.Text)
176
+ }
177
+ if len(lnHosts) == 0 {
178
+ lnHosts = []string{""}
179
+ }
180
+
181
+ // use a map to prevent duplication
182
+ listeners := make(map[string]struct{})
183
+ for _, host := range lnHosts {
184
+ listeners[net.JoinHostPort(host, lnPort)] = struct{}{}
185
+ }
186
+
187
+ // now turn map into list
188
+ var listenersList []string
189
+ for lnStr := range listeners {
190
+ listenersList = append(listenersList, lnStr)
191
+ }
192
+ // sort.Strings(listenersList) // TODO: is sorting necessary?
193
+
194
+ return listenersList, nil
195
+}
196
+
197
+// Address represents a site address. It contains
198
+// the original input value, and the component
199
+// parts of an address. The component parts may be
200
+// updated to the correct values as setup proceeds,
201
+// but the original value should never be changed.
202
+//
203
+// The Host field must be in a normalized form.
204
+type Address struct {
205
+ Original, Scheme, Host, Port, Path string
206
+}
207
+
208
+// String returns a human-friendly print of the address.
209
+func (a Address) String() string {
210
+ if a.Host == "" && a.Port == "" {
211
+ return ""
212
+ }
213
+ scheme := a.Scheme
214
+ if scheme == "" {
215
+ if a.Port == strconv.Itoa(certmagic.HTTPSPort) {
216
+ scheme = "https"
217
+ } else {
218
+ scheme = "http"
219
+ }
220
+ }
221
+ s := scheme
222
+ if s != "" {
223
+ s += "://"
224
+ }
225
+ if a.Port != "" &&
226
+ ((scheme == "https" && a.Port != strconv.Itoa(caddyhttp.DefaultHTTPSPort)) ||
227
+ (scheme == "http" && a.Port != strconv.Itoa(caddyhttp.DefaultHTTPPort))) {
228
+ s += net.JoinHostPort(a.Host, a.Port)
229
+ } else {
230
+ s += a.Host
231
+ }
232
+ if a.Path != "" {
233
+ s += a.Path
234
+ }
235
+ return s
236
+}
237
+
238
+// VHost returns a sensible concatenation of Host:Port/Path from a.
239
+// It's basically the a.Original but without the scheme.
240
+func (a Address) VHost() string {
241
+ if idx := strings.Index(a.Original, "://"); idx > -1 {
242
+ return a.Original[idx+3:]
243
+ }
244
+ return a.Original
245
+}
246
+
247
+// Normalize normalizes URL: turn scheme and host names into lower case
248
+func (a Address) Normalize() Address {
249
+ path := a.Path
250
+ if !caseSensitivePath {
251
+ path = strings.ToLower(path)
252
+ }
253
+
254
+ // ensure host is normalized if it's an IP address
255
+ host := a.Host
256
+ if ip := net.ParseIP(host); ip != nil {
257
+ host = ip.String()
258
+ }
259
+
260
+ return Address{
261
+ Original: a.Original,
262
+ Scheme: strings.ToLower(a.Scheme),
263
+ Host: strings.ToLower(host),
264
+ Port: a.Port,
265
+ Path: path,
266
+ }
267
+}
268
+
269
+// Key is similar to String, just replaces scheme and host values with modified values.
270
+// Unlike String it doesn't add anything default (scheme, port, etc)
271
+func (a Address) Key() string {
272
+ res := ""
273
+ if a.Scheme != "" {
274
+ res += a.Scheme + "://"
275
+ }
276
+ if a.Host != "" {
277
+ res += a.Host
278
+ }
279
+ if a.Port != "" {
280
+ if strings.HasPrefix(a.Original[len(res):], ":"+a.Port) {
281
+ // insert port only if the original has its own explicit port
282
+ res += ":" + a.Port
283
+ }
284
+ }
285
+ if a.Path != "" {
286
+ res += a.Path
287
+ }
288
+ return res
289
+}
290
+
291
+// standardizeAddress parses an address string into a structured format with separate
292
+// scheme, host, port, and path portions, as well as the original input string.
293
+func standardizeAddress(str string) (Address, error) {
294
+ httpPort, httpsPort := strconv.Itoa(certmagic.HTTPPort), strconv.Itoa(certmagic.HTTPSPort)
295
+
296
+ input := str
297
+
298
+ // Split input into components (prepend with // to assert host by default)
299
+ if !strings.Contains(str, "//") && !strings.HasPrefix(str, "/") {
300
+ str = "//" + str
301
+ }
302
+ u, err := url.Parse(str)
303
+ if err != nil {
304
+ return Address{}, err
305
+ }
306
+
307
+ // separate host and port
308
+ host, port, err := net.SplitHostPort(u.Host)
309
+ if err != nil {
310
+ host, port, err = net.SplitHostPort(u.Host + ":")
311
+ if err != nil {
312
+ host = u.Host
313
+ }
314
+ }
315
+
316
+ // see if we can set port based off scheme
317
+ if port == "" {
318
+ if u.Scheme == "http" {
319
+ port = httpPort
320
+ } else if u.Scheme == "https" {
321
+ port = httpsPort
322
+ }
323
+ }
324
+
325
+ // repeated or conflicting scheme is confusing, so error
326
+ if u.Scheme != "" && (port == "http" || port == "https") {
327
+ return Address{}, fmt.Errorf("[%s] scheme specified twice in address", input)
328
+ }
329
+
330
+ // error if scheme and port combination violate convention
331
+ if (u.Scheme == "http" && port == httpsPort) || (u.Scheme == "https" && port == httpPort) {
332
+ return Address{}, fmt.Errorf("[%s] scheme and port violate convention", input)
333
+ }
334
+
335
+ // standardize http and https ports to their respective port numbers
336
+ if port == "http" {
337
+ u.Scheme = "http"
338
+ port = httpPort
339
+ } else if port == "https" {
340
+ u.Scheme = "https"
341
+ port = httpsPort
342
+ }
343
+
344
+ return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err
345
+}
346
+
347
+const (
348
+ defaultPort = "2015"
349
+ caseSensitivePath = false
350
+)
caddyconfig/httpcaddyfile/addresses_test.go
new
+129
@@ -0,0 +1,129 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
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
+
15
+package httpcaddyfile
16
+
17
+import "testing"
18
+
19
+func TestStandardizeAddress(t *testing.T) {
20
+ for i, test := range []struct {
21
+ input string
22
+ scheme, host, port, path string
23
+ shouldErr bool
24
+ }{
25
+ {`localhost`, "", "localhost", "", "", false},
26
+ {`localhost:1234`, "", "localhost", "1234", "", false},
27
+ {`localhost:`, "", "localhost", "", "", false},
28
+ {`0.0.0.0`, "", "0.0.0.0", "", "", false},
29
+ {`127.0.0.1:1234`, "", "127.0.0.1", "1234", "", false},
30
+ {`:1234`, "", "", "1234", "", false},
31
+ {`[::1]`, "", "::1", "", "", false},
32
+ {`[::1]:1234`, "", "::1", "1234", "", false},
33
+ {`:`, "", "", "", "", false},
34
+ {`localhost:http`, "http", "localhost", "80", "", false},
35
+ {`localhost:https`, "https", "localhost", "443", "", false},
36
+ {`:http`, "http", "", "80", "", false},
37
+ {`:https`, "https", "", "443", "", false},
38
+ {`http://localhost:https`, "", "", "", "", true}, // conflict
39
+ {`http://localhost:http`, "", "", "", "", true}, // repeated scheme
40
+ {`http://localhost:443`, "", "", "", "", true}, // not conventional
41
+ {`https://localhost:80`, "", "", "", "", true}, // not conventional
42
+ {`http://localhost`, "http", "localhost", "80", "", false},
43
+ {`https://localhost`, "https", "localhost", "443", "", false},
44
+ {`http://127.0.0.1`, "http", "127.0.0.1", "80", "", false},
45
+ {`https://127.0.0.1`, "https", "127.0.0.1", "443", "", false},
46
+ {`http://[::1]`, "http", "::1", "80", "", false},
47
+ {`http://localhost:1234`, "http", "localhost", "1234", "", false},
48
+ {`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", "", false},
49
+ {`http://[::1]:1234`, "http", "::1", "1234", "", false},
50
+ {``, "", "", "", "", false},
51
+ {`::1`, "", "::1", "", "", true},
52
+ {`localhost::`, "", "localhost::", "", "", true},
53
+ {`#$%@`, "", "", "", "", true},
54
+ {`host/path`, "", "host", "", "/path", false},
55
+ {`http://host/`, "http", "host", "80", "/", false},
56
+ {`//asdf`, "", "asdf", "", "", false},
57
+ {`:1234/asdf`, "", "", "1234", "/asdf", false},
58
+ {`http://host/path`, "http", "host", "80", "/path", false},
59
+ {`https://host:443/path/foo`, "https", "host", "443", "/path/foo", false},
60
+ {`host:80/path`, "", "host", "80", "/path", false},
61
+ {`host:https/path`, "https", "host", "443", "/path", false},
62
+ {`/path`, "", "", "", "/path", false},
63
+ } {
64
+ actual, err := standardizeAddress(test.input)
65
+
66
+ if err != nil && !test.shouldErr {
67
+ t.Errorf("Test %d (%s): Expected no error, but had error: %v", i, test.input, err)
68
+ }
69
+ if err == nil && test.shouldErr {
70
+ t.Errorf("Test %d (%s): Expected error, but had none", i, test.input)
71
+ }
72
+
73
+ if !test.shouldErr && actual.Original != test.input {
74
+ t.Errorf("Test %d (%s): Expected original '%s', got '%s'", i, test.input, test.input, actual.Original)
75
+ }
76
+ if actual.Scheme != test.scheme {
77
+ t.Errorf("Test %d (%s): Expected scheme '%s', got '%s'", i, test.input, test.scheme, actual.Scheme)
78
+ }
79
+ if actual.Host != test.host {
80
+ t.Errorf("Test %d (%s): Expected host '%s', got '%s'", i, test.input, test.host, actual.Host)
81
+ }
82
+ if actual.Port != test.port {
83
+ t.Errorf("Test %d (%s): Expected port '%s', got '%s'", i, test.input, test.port, actual.Port)
84
+ }
85
+ if actual.Path != test.path {
86
+ t.Errorf("Test %d (%s): Expected path '%s', got '%s'", i, test.input, test.path, actual.Path)
87
+ }
88
+ }
89
+}
90
+
91
+func TestAddressVHost(t *testing.T) {
92
+ for i, test := range []struct {
93
+ addr Address
94
+ expected string
95
+ }{
96
+ {Address{Original: "host:1234"}, "host:1234"},
97
+ {Address{Original: "host:1234/foo"}, "host:1234/foo"},
98
+ {Address{Original: "host/foo"}, "host/foo"},
99
+ {Address{Original: "http://host/foo"}, "host/foo"},
100
+ {Address{Original: "https://host/foo"}, "host/foo"},
101
+ } {
102
+ actual := test.addr.VHost()
103
+ if actual != test.expected {
104
+ t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expected, actual)
105
+ }
106
+ }
107
+}
108
+
109
+func TestAddressString(t *testing.T) {
110
+ for i, test := range []struct {
111
+ addr Address
112
+ expected string
113
+ }{
114
+ {Address{Scheme: "http", Host: "host", Port: "1234", Path: "/path"}, "http://host:1234/path"},
115
+ {Address{Scheme: "", Host: "host", Port: "", Path: ""}, "http://host"},
116
+ {Address{Scheme: "", Host: "host", Port: "80", Path: ""}, "http://host"},
117
+ {Address{Scheme: "", Host: "host", Port: "443", Path: ""}, "https://host"},
118
+ {Address{Scheme: "https", Host: "host", Port: "443", Path: ""}, "https://host"},
119
+ {Address{Scheme: "https", Host: "host", Port: "", Path: ""}, "https://host"},
120
+ {Address{Scheme: "", Host: "host", Port: "80", Path: "/path"}, "http://host/path"},
121
+ {Address{Scheme: "http", Host: "", Port: "1234", Path: ""}, "http://:1234"},
122
+ {Address{Scheme: "", Host: "", Port: "", Path: ""}, ""},
123
+ } {
124
+ actual := test.addr.String()
125
+ if actual != test.expected {
126
+ t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expected, actual)
127
+ }
128
+ }
129
+}
caddyconfig/httpcaddyfile/builtins.go
new
+257
@@ -0,0 +1,257 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
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
+
15
+package httpcaddyfile
16
+
17
+import (
18
+ "encoding/json"
19
+ "fmt"
20
+ "html"
21
+ "net/http"
22
+
23
+ "github.com/caddyserver/caddy/v2/caddyconfig"
24
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
25
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
26
+ "github.com/caddyserver/caddy/v2/modules/caddytls"
27
+)
28
+
29
+func (st *ServerType) parseRoot(
30
+ tkns []caddyfile.Token,
31
+ matcherDefs map[string]map[string]json.RawMessage,
32
+ warnings *[]caddyconfig.Warning,
33
+) ([]caddyhttp.Route, error) {
34
+ var routes []caddyhttp.Route
35
+
36
+ matchersAndTokens, err := st.tokensToMatcherSets(tkns, matcherDefs, warnings)
37
+ if err != nil {
38
+ return nil, err
39
+ }
40
+
41
+ for _, mst := range matchersAndTokens {
42
+ d := caddyfile.NewDispenser("Caddyfile", mst.tokens)
43
+
44
+ var root string
45
+ for d.Next() {
46
+ if !d.NextArg() {
47
+ return nil, d.ArgErr()
48
+ }
49
+ root = d.Val()
50
+ if d.NextArg() {
51
+ return nil, d.ArgErr()
52
+ }
53
+ }
54
+
55
+ varsHandler := caddyhttp.VarsMiddleware{"root": root}
56
+ route := caddyhttp.Route{
57
+ Handle: []json.RawMessage{
58
+ caddyconfig.JSONModuleObject(varsHandler, "handler", "vars", warnings),
59
+ },
60
+ }
61
+ if mst.matcherSet != nil {
62
+ route.MatcherSets = []map[string]json.RawMessage{mst.matcherSet}
63
+ }
64
+
65
+ routes = append(routes, route)
66
+ }
67
+
68
+ return routes, nil
69
+}
70
+
71
+func (st *ServerType) parseRedir(
72
+ tkns []caddyfile.Token,
73
+ matcherDefs map[string]map[string]json.RawMessage,
74
+ warnings *[]caddyconfig.Warning,
75
+) ([]caddyhttp.Route, error) {
76
+ var routes []caddyhttp.Route
77
+
78
+ matchersAndTokens, err := st.tokensToMatcherSets(tkns, matcherDefs, warnings)
79
+ if err != nil {
80
+ return nil, err
81
+ }
82
+
83
+ for _, mst := range matchersAndTokens {
84
+ var route caddyhttp.Route
85
+
86
+ d := caddyfile.NewDispenser("Caddyfile", mst.tokens)
87
+
88
+ for d.Next() {
89
+ if !d.NextArg() {
90
+ return nil, d.ArgErr()
91
+ }
92
+ to := d.Val()
93
+
94
+ var code string
95
+ if d.NextArg() {
96
+ code = d.Val()
97
+ }
98
+ if code == "permanent" {
99
+ code = "301"
100
+ }
101
+ if code == "temporary" || code == "" {
102
+ code = "307"
103
+ }
104
+ var body string
105
+ if code == "meta" {
106
+ // Script tag comes first since that will better imitate a redirect in the browser's
107
+ // history, but the meta tag is a fallback for most non-JS clients.
108
+ const metaRedir = `<!DOCTYPE html>
109
+<html>
110
+ <head>
111
+ <title>Redirecting...</title>
112
+ <script>window.location.replace("%s");</script>
113
+ <meta http-equiv="refresh" content="0; URL='%s'">
114
+ </head>
115
+ <body>Redirecting to <a href="%s">%s</a>...</body>
116
+</html>
117
+`
118
+ safeTo := html.EscapeString(to)
119
+ body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo)
120
+ }
121
+
122
+ handler := caddyhttp.StaticResponse{
123
+ StatusCode: caddyhttp.WeakString(code),
124
+ Headers: http.Header{"Location": []string{to}},
125
+ Body: body,
126
+ }
127
+
128
+ route.Handle = append(route.Handle,
129
+ caddyconfig.JSONModuleObject(handler, "handler", "static_response", warnings))
130
+ }
131
+
132
+ if mst.matcherSet != nil {
133
+ route.MatcherSets = []map[string]json.RawMessage{mst.matcherSet}
134
+ }
135
+
136
+ routes = append(routes, route)
137
+ }
138
+
139
+ return routes, nil
140
+}
141
+
142
+func (st *ServerType) parseTLSAutomationManager(d *caddyfile.Dispenser) (caddytls.ACMEManagerMaker, error) {
143
+ var m caddytls.ACMEManagerMaker
144
+
145
+ for d.Next() {
146
+ firstLine := d.RemainingArgs()
147
+ if len(firstLine) == 1 && firstLine[0] != "off" {
148
+ m.Email = firstLine[0]
149
+ }
150
+
151
+ var hasBlock bool
152
+ for d.NextBlock() {
153
+ hasBlock = true
154
+ switch d.Val() {
155
+ case "ca":
156
+ arg := d.RemainingArgs()
157
+ if len(arg) != 1 {
158
+ return m, d.ArgErr()
159
+ }
160
+ m.CA = arg[0]
161
+ // TODO: other properties
162
+ }
163
+ }
164
+
165
+ // a naked tls directive is not allowed
166
+ if len(firstLine) == 0 && !hasBlock {
167
+ return m, d.ArgErr()
168
+ }
169
+ }
170
+
171
+ return m, nil
172
+}
173
+
174
+func (st *ServerType) parseTLSCerts(d *caddyfile.Dispenser) (map[string]caddytls.CertificateLoader, error) {
175
+ var fileLoader caddytls.FileLoader
176
+ var folderLoader caddytls.FolderLoader
177
+
178
+ for d.Next() {
179
+ // file loader
180
+ firstLine := d.RemainingArgs()
181
+ if len(firstLine) == 2 {
182
+ fileLoader = append(fileLoader, caddytls.CertKeyFilePair{
183
+ Certificate: firstLine[0],
184
+ Key: firstLine[1],
185
+ // TODO: tags, for enterprise module's certificate selection
186
+ })
187
+ }
188
+
189
+ // folder loader
190
+ for d.NextBlock() {
191
+ if d.Val() == "load" {
192
+ folderLoader = append(folderLoader, d.RemainingArgs()...)
193
+ }
194
+ }
195
+ }
196
+
197
+ // put configured loaders into the map
198
+ loaders := make(map[string]caddytls.CertificateLoader)
199
+ if len(fileLoader) > 0 {
200
+ loaders["load_files"] = fileLoader
201
+ }
202
+ if len(folderLoader) > 0 {
203
+ loaders["load_folders"] = folderLoader
204
+ }
205
+
206
+ return loaders, nil
207
+}
208
+
209
+func (st *ServerType) parseTLSConnPolicy(d *caddyfile.Dispenser) (*caddytls.ConnectionPolicy, error) {
210
+ cp := new(caddytls.ConnectionPolicy)
211
+
212
+ for d.Next() {
213
+ for d.NextBlock() {
214
+ switch d.Val() {
215
+ case "protocols":
216
+ args := d.RemainingArgs()
217
+ if len(args) == 0 {
218
+ return nil, d.SyntaxErr("one or two protocols")
219
+ }
220
+ if len(args) > 0 {
221
+ if _, ok := caddytls.SupportedProtocols[args[0]]; !ok {
222
+ return nil, d.Errf("Wrong protocol name or protocol not supported: '%s'", args[0])
223
+ }
224
+ cp.ProtocolMin = args[0]
225
+ }
226
+ if len(args) > 1 {
227
+ if _, ok := caddytls.SupportedProtocols[args[1]]; !ok {
228
+ return nil, d.Errf("Wrong protocol name or protocol not supported: '%s'", args[1])
229
+ }
230
+ cp.ProtocolMax = args[1]
231
+ }
232
+ case "ciphers":
233
+ for d.NextArg() {
234
+ if _, ok := caddytls.SupportedCipherSuites[d.Val()]; !ok {
235
+ return nil, d.Errf("Wrong cipher suite name or cipher suite not supported: '%s'", d.Val())
236
+ }
237
+ cp.CipherSuites = append(cp.CipherSuites, d.Val())
238
+ }
239
+ case "curves":
240
+ for d.NextArg() {
241
+ if _, ok := caddytls.SupportedCurves[d.Val()]; !ok {
242
+ return nil, d.Errf("Wrong curve name or curve not supported: '%s'", d.Val())
243
+ }
244
+ cp.Curves = append(cp.Curves, d.Val())
245
+ }
246
+ case "alpn":
247
+ args := d.RemainingArgs()
248
+ if len(args) == 0 {
249
+ return nil, d.ArgErr()
250
+ }
251
+ cp.ALPN = args
252
+ }
253
+ }
254
+ }
255
+
256
+ return cp, nil
257
+}
caddyconfig/httpcaddyfile/handlers.go
new
+92
@@ -0,0 +1,92 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
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
+
15
+package httpcaddyfile
16
+
17
+import (
18
+ "encoding/json"
19
+ "fmt"
20
+ "log"
21
+
22
+ "github.com/caddyserver/caddy/v2"
23
+ "github.com/caddyserver/caddy/v2/caddyconfig"
24
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
25
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
26
+)
27
+
28
+func (st *ServerType) parseMatcherDefinitions(d *caddyfile.Dispenser) (map[string]map[string]json.RawMessage, error) {
29
+ matchers := make(map[string]map[string]json.RawMessage)
30
+ for d.Next() {
31
+ definitionName := d.Val()
32
+ for d.NextBlock() {
33
+ matcherName := d.Val()
34
+ mod, err := caddy.GetModule("http.matchers." + matcherName)
35
+ if err != nil {
36
+ return nil, fmt.Errorf("getting matcher module '%s': %v", matcherName, err)
37
+ }
38
+ unm, ok := mod.New().(caddyfile.Unmarshaler)
39
+ if !ok {
40
+ return nil, fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName)
41
+ }
42
+ err = unm.UnmarshalCaddyfile(d.NewFromNextTokens())
43
+ if err != nil {
44
+ return nil, err
45
+ }
46
+ rm, ok := unm.(caddyhttp.RequestMatcher)
47
+ if !ok {
48
+ return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
49
+ }
50
+ if _, ok := matchers[definitionName]; !ok {
51
+ matchers[definitionName] = make(map[string]json.RawMessage)
52
+ }
53
+ matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
54
+ }
55
+ }
56
+ return matchers, nil
57
+}
58
+
59
+// directiveBuckets returns a list of middleware/handler directives.
60
+// Buckets are ordered, and directives should be evaluated in their
61
+// bucket order. Within a bucket, directives are not ordered. Hence,
62
+// the return value has a slice of buckets, where each bucket is a
63
+// map, which is a strongly-typed reminder that directives within a
64
+// bucket are not ordered.
65
+func directiveBuckets() []map[string]struct{} {
66
+ directiveBuckets := []map[string]struct{}{
67
+ // prefer odd-numbered buckets; evens are there for contingencies
68
+ {}, // 0
69
+ {}, // 1 - keep empty unless necessary
70
+ {}, // 2
71
+ {}, // 3 - first handlers, last responders
72
+ {}, // 4
73
+ {}, // 5 - middle of chain
74
+ {}, // 6
75
+ {}, // 7 - last handlers, first responders
76
+ {}, // 8
77
+ {}, // 9 - keep empty unless necessary
78
+ {}, // 10
79
+ }
80
+ for _, mod := range caddy.GetModules("http.handlers") {
81
+ if hd, ok := mod.New().(HandlerDirective); ok {
82
+ bucket := hd.Bucket()
83
+ if bucket < 0 || bucket >= len(directiveBuckets) {
84
+ log.Printf("[ERROR] directive %s: bucket out of range [0-%d): %d; skipping",
85
+ mod.Name, len(directiveBuckets), bucket)
86
+ continue
87
+ }
88
+ directiveBuckets[bucket][mod.ID()] = struct{}{}
89
+ }
90
+ }
91
+ return directiveBuckets
92
+}
caddyconfig/httpcaddyfile/httptype.go
new
+542
@@ -0,0 +1,542 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
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
+
15
+package httpcaddyfile
16
+
17
+import (
18
+ "encoding/json"
19
+ "fmt"
20
+ "reflect"
21
+ "strconv"
22
+ "strings"
23
+
24
+ "github.com/mholt/certmagic"
25
+
26
+ "github.com/caddyserver/caddy/v2"
27
+ "github.com/caddyserver/caddy/v2/caddyconfig"
28
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
29
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
30
+ "github.com/caddyserver/caddy/v2/modules/caddytls"
31
+)
32
+
33
+func init() {
34
+ caddyconfig.RegisterAdapter("caddyfile", caddyfile.Adapter{ServerType: ServerType{}})
35
+}
36
+
37
+// ServerType can set up a config from an HTTP Caddyfile.
38
+type ServerType struct {
39
+}
40
+
41
+// ValidDirectives returns the list of known directives.
42
+func (ServerType) ValidDirectives() []string {
43
+ dirs := []string{"matcher", "root", "tls", "redir"} // TODO: put special-case (hard-coded, or non-handler) directives here
44
+ for _, mod := range caddy.GetModules("http.handlers") {
45
+ if _, ok := mod.New().(HandlerDirective); ok {
46
+ dirs = append(dirs, mod.ID())
47
+ }
48
+ }
49
+ return dirs
50
+}
51
+
52
+// Setup makes a config from the tokens.
53
+func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
54
+ options map[string]string) (*caddy.Config, []caddyconfig.Warning, error) {
55
+ var warnings []caddyconfig.Warning
56
+
57
+ // map
58
+ sbmap, err := st.mapAddressToServerBlocks(originalServerBlocks)
59
+ if err != nil {
60
+ return nil, warnings, err
61
+ }
62
+
63
+ // reduce
64
+ pairings := st.consolidateAddrMappings(sbmap)
65
+
66
+ // each pairing of listener addresses to list of server
67
+ // blocks is basically a server definition
68
+ servers, err := st.serversFromPairings(pairings, &warnings)
69
+ if err != nil {
70
+ return nil, warnings, err
71
+ }
72
+
73
+ // now that each server is configured, make the HTTP app
74
+ httpApp := caddyhttp.App{
75
+ HTTPPort: tryInt(options["http-port"], &warnings),
76
+ HTTPSPort: tryInt(options["https-port"], &warnings),
77
+ Servers: servers,
78
+ }
79
+
80
+ // now for the TLS app! (TODO: refactor into own func)
81
+ tlsApp := caddytls.TLS{Certificates: make(map[string]json.RawMessage)}
82
+ for _, p := range pairings {
83
+ for _, sblock := range p.serverBlocks {
84
+ if tkns, ok := sblock.Tokens["tls"]; ok {
85
+ // extract all unique hostnames from the server block
86
+ // keys, then convert to a slice for use in the TLS app
87
+ hostMap := make(map[string]struct{})
88
+ for _, sblockKey := range sblock.Keys {
89
+ addr, err := standardizeAddress(sblockKey)
90
+ if err != nil {
91
+ return nil, warnings, fmt.Errorf("parsing server block key: %v", err)
92
+ }
93
+ hostMap[addr.Host] = struct{}{}
94
+ }
95
+ sblockHosts := make([]string, 0, len(hostMap))
96
+ for host := range hostMap {
97
+ sblockHosts = append(sblockHosts, host)
98
+ }
99
+
100
+ // parse tokens to get ACME manager config
101
+ acmeMgr, err := st.parseTLSAutomationManager(caddyfile.NewDispenser("Caddyfile", tkns))
102
+ if err != nil {
103
+ return nil, warnings, err
104
+ }
105
+
106
+ tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, caddytls.AutomationPolicy{
107
+ Hosts: sblockHosts,
108
+ ManagementRaw: caddyconfig.JSONModuleObject(acmeMgr, "module", "acme", &warnings),
109
+ })
110
+
111
+ // parse tokens to get certificates to be loaded manually
112
+ certLoaders, err := st.parseTLSCerts(caddyfile.NewDispenser("Caddyfile", tkns))
113
+ if err != nil {
114
+ return nil, nil, err
115
+ }
116
+ for loaderName, loader := range certLoaders {
117
+ tlsApp.Certificates[loaderName] = caddyconfig.JSON(loader, &warnings)
118
+ }
119
+
120
+ }
121
+ }
122
+ }
123
+
124
+ // annnd the top-level config, then we're done!
125
+ cfg := &caddy.Config{AppsRaw: make(map[string]json.RawMessage)}
126
+ if !reflect.DeepEqual(httpApp, caddyhttp.App{}) {
127
+ cfg.AppsRaw["http"] = caddyconfig.JSON(httpApp, &warnings)
128
+ }
129
+ if !reflect.DeepEqual(tlsApp, caddytls.TLS{}) {
130
+ cfg.AppsRaw["tls"] = caddyconfig.JSON(tlsApp, &warnings)
131
+ }
132
+
133
+ return cfg, warnings, nil
134
+}
135
+
136
+// hostsFromServerBlockKeys returns a list of all the
137
+// hostnames found in the keys of the server block sb.
138
+// The list may not be in a consistent order.
139
+func (st *ServerType) hostsFromServerBlockKeys(sb caddyfile.ServerBlock) ([]string, error) {
140
+ // first get each unique hostname
141
+ hostMap := make(map[string]struct{})
142
+ for _, sblockKey := range sb.Keys {
143
+ addr, err := standardizeAddress(sblockKey)
144
+ if err != nil {
145
+ return nil, fmt.Errorf("parsing server block key: %v", err)
146
+ }
147
+ hostMap[addr.Host] = struct{}{}
148
+ }
149
+
150
+ // convert map to slice
151
+ sblockHosts := make([]string, 0, len(hostMap))
152
+ for host := range hostMap {
153
+ sblockHosts = append(sblockHosts, host)
154
+ }
155
+
156
+ return sblockHosts, nil
157
+}
158
+
159
+// serversFromPairings creates the servers for each pairing of addresses
160
+// to server blocks. Each pairing is essentially a server definition.
161
+func (st *ServerType) serversFromPairings(pairings []sbAddrAssociation, warnings *[]caddyconfig.Warning) (map[string]*caddyhttp.Server, error) {
162
+ servers := make(map[string]*caddyhttp.Server)
163
+
164
+ for i, p := range pairings {
165
+ srv := &caddyhttp.Server{
166
+ Listen: p.addresses,
167
+ }
168
+
169
+ for _, sblock := range p.serverBlocks {
170
+ matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock)
171
+ if err != nil {
172
+ return nil, fmt.Errorf("server block %v: compiling matcher sets: %v", sblock.Keys, err)
173
+ }
174
+
175
+ // extract matcher definitions
176
+ d := caddyfile.NewDispenser("Caddyfile", sblock.Tokens["matcher"])
177
+ matcherDefs, err := st.parseMatcherDefinitions(d)
178
+ if err != nil {
179
+ return nil, err
180
+ }
181
+
182
+ siteVarSubroute, handlerSubroute := new(caddyhttp.Subroute), new(caddyhttp.Subroute)
183
+
184
+ // built-in directives
185
+
186
+ // root: path to root of site
187
+ if tkns, ok := sblock.Tokens["root"]; ok {
188
+ routes, err := st.parseRoot(tkns, matcherDefs, warnings)
189
+ if err != nil {
190
+ return nil, err
191
+ }
192
+ siteVarSubroute.Routes = append(siteVarSubroute.Routes, routes...)
193
+ }
194
+
195
+ // tls: off and conn policies
196
+ if tkns, ok := sblock.Tokens["tls"]; ok {
197
+ // get the hosts for this server block...
198
+ hosts, err := st.hostsFromServerBlockKeys(sblock)
199
+ if err != nil {
200
+ return nil, err
201
+ }
202
+
203
+ // ...and of those, which ones qualify for auto HTTPS
204
+ var autoHTTPSQualifiedHosts []string
205
+ for _, h := range hosts {
206
+ if certmagic.HostQualifies(h) {
207
+ autoHTTPSQualifiedHosts = append(autoHTTPSQualifiedHosts, h)
208
+ }
209
+ }
210
+
211
+ if len(tkns) == 2 && tkns[1].Text == "off" {
212
+ // tls off: disable TLS (and automatic HTTPS) for server block's names
213
+ if srv.AutoHTTPS == nil {
214
+ srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
215
+ }
216
+ srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, autoHTTPSQualifiedHosts...)
217
+ } else {
218
+ // tls connection policies
219
+ cp, err := st.parseTLSConnPolicy(caddyfile.NewDispenser("Caddyfile", tkns))
220
+ if err != nil {
221
+ return nil, err
222
+ }
223
+ // TODO: are matchers needed if every hostname of the config is matched?
224
+ cp.Matchers = map[string]json.RawMessage{
225
+ "sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones
226
+ }
227
+ srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp)
228
+ }
229
+ }
230
+
231
+ // set up each handler directive
232
+ for _, dirBucket := range directiveBuckets() {
233
+ for dir := range dirBucket {
234
+ // keep in mind that multiple occurrences of the directive may appear here
235
+ tkns, ok := sblock.Tokens[dir]
236
+ if !ok {
237
+ continue
238
+ }
239
+
240
+ // extract matcher sets from matcher tokens, if any
241
+ matcherSetsMap, err := st.tokensToMatcherSets(tkns, matcherDefs, warnings)
242
+
243
+ mod, err := caddy.GetModule("http.handlers." + dir)
244
+ if err != nil {
245
+ return nil, fmt.Errorf("getting handler module '%s': %v", mod.Name, err)
246
+ }
247
+
248
+ // the tokens have been divided by matcher set for us,
249
+ // so iterate each one and set them up
250
+ for _, mst := range matcherSetsMap {
251
+ unm, ok := mod.New().(caddyfile.Unmarshaler)
252
+ if !ok {
253
+ return nil, fmt.Errorf("handler module '%s' is not a Caddyfile unmarshaler", mod.Name)
254
+ }
255
+ err = unm.UnmarshalCaddyfile(caddyfile.NewDispenser(d.File(), mst.tokens))
256
+ if err != nil {
257
+ return nil, err
258
+ }
259
+ handler, ok := unm.(caddyhttp.MiddlewareHandler)
260
+ if !ok {
261
+ return nil, fmt.Errorf("handler module '%s' does not implement caddyhttp.MiddlewareHandler interface", mod.Name)
262
+ }
263
+
264
+ route := caddyhttp.Route{
265
+ Handle: []json.RawMessage{
266
+ caddyconfig.JSONModuleObject(handler, "handler", dir, warnings),
267
+ },
268
+ }
269
+ if mst.matcherSet != nil {
270
+ route.MatcherSets = []map[string]json.RawMessage{mst.matcherSet}
271
+ }
272
+ handlerSubroute.Routes = append(handlerSubroute.Routes, route)
273
+ }
274
+
275
+ }
276
+ }
277
+
278
+ // redir: static responses that redirect
279
+ if tkns, ok := sblock.Tokens["redir"]; ok {
280
+ routes, err := st.parseRedir(tkns, matcherDefs, warnings)
281
+ if err != nil {
282
+ return nil, err
283
+ }
284
+ handlerSubroute.Routes = append(handlerSubroute.Routes, routes...)
285
+ }
286
+
287
+ // the route that contains the site's handlers will
288
+ // be assumed to be the sub-route for this site...
289
+ siteSubroute := handlerSubroute
290
+
291
+ // ... unless, of course, there are variables that might
292
+ // be used by the site's matchers or handlers, in which
293
+ // case we need to nest the handlers in a sub-sub-route,
294
+ // and the variables go in the sub-route so the variables
295
+ // get evaluated first
296
+ if len(siteVarSubroute.Routes) > 0 {
297
+ subSubRoute := caddyhttp.Subroute{Routes: siteSubroute.Routes}
298
+ siteSubroute.Routes = append(
299
+ siteVarSubroute.Routes,
300
+ caddyhttp.Route{
301
+ Handle: []json.RawMessage{
302
+ caddyconfig.JSONModuleObject(subSubRoute, "handler", "subroute", warnings),
303
+ },
304
+ },
305
+ )
306
+ }
307
+
308
+ siteSubroute.Routes = consolidateRoutes(siteSubroute.Routes)
309
+
310
+ srv.Routes = append(srv.Routes, caddyhttp.Route{
311
+ MatcherSets: matcherSetsEnc,
312
+ Handle: []json.RawMessage{
313
+ caddyconfig.JSONModuleObject(siteSubroute, "handler", "subroute", warnings),
314
+ },
315
+ })
316
+ }
317
+
318
+ srv.Routes = consolidateRoutes(srv.Routes)
319
+
320
+ servers[fmt.Sprintf("srv%d", i)] = srv
321
+ }
322
+
323
+ return servers, nil
324
+}
325
+
326
+// consolidateRoutes combines routes with the same properties
327
+// (same matchers, same Terminal and Group settings) for a
328
+// cleaner overall output.
329
+func consolidateRoutes(routes caddyhttp.RouteList) caddyhttp.RouteList {
330
+ for i := 0; i < len(routes)-1; i++ {
331
+ if reflect.DeepEqual(routes[i].MatcherSets, routes[i+1].MatcherSets) &&
332
+ routes[i].Terminal == routes[i+1].Terminal &&
333
+ routes[i].Group == routes[i+1].Group {
334
+ // keep the handlers in the same order, then splice out repetitive route
335
+ routes[i].Handle = append(routes[i].Handle, routes[i+1].Handle...)
336
+ routes = append(routes[:i+1], routes[i+2:]...)
337
+ i--
338
+ }
339
+ }
340
+ return routes
341
+}
342
+
343
+func (st *ServerType) tokensToMatcherSets(
344
+ tkns []caddyfile.Token,
345
+ matcherDefs map[string]map[string]json.RawMessage,
346
+ warnings *[]caddyconfig.Warning,
347
+) (map[string]matcherSetAndTokens, error) {
348
+ m := make(map[string]matcherSetAndTokens)
349
+
350
+ for len(tkns) > 0 {
351
+ d := caddyfile.NewDispenser("Caddyfile", tkns)
352
+ d.Next() // consume directive token
353
+
354
+ // look for matcher; it should be the next argument
355
+ var matcherToken caddyfile.Token
356
+ var matcherSet map[string]json.RawMessage
357
+ if d.NextArg() {
358
+ var ok bool
359
+ var err error
360
+ matcherSet, ok, err = st.matcherSetFromMatcherToken(d.Token(), matcherDefs, warnings)
361
+ if err != nil {
362
+ return nil, err
363
+ }
364
+ if ok {
365
+ // found a matcher; save it, then splice it out
366
+ // since we don't want to parse it again
367
+ matcherToken = d.Token()
368
+ tkns = d.Delete()
369
+ }
370
+ d.RemainingArgs() // advance to end of line
371
+ }
372
+ for d.NextBlock() {
373
+ // skip entire block including any nested blocks; all
374
+ // we care about is accessing next directive occurrence
375
+ for d.Nested() {
376
+ d.NextBlock()
377
+ }
378
+ }
379
+ end := d.Cursor() + 1
380
+ m[matcherToken.Text] = matcherSetAndTokens{
381
+ matcherSet: matcherSet,
382
+ tokens: append(m[matcherToken.Text].tokens, tkns[:end]...),
383
+ }
384
+ tkns = tkns[end:]
385
+ }
386
+ return m, nil
387
+}
388
+
389
+func (st *ServerType) matcherSetFromMatcherToken(
390
+ tkn caddyfile.Token,
391
+ matcherDefs map[string]map[string]json.RawMessage,
392
+ warnings *[]caddyconfig.Warning,
393
+) (map[string]json.RawMessage, bool, error) {
394
+ // matcher tokens can be wildcards, simple path matchers,
395
+ // or refer to a pre-defined matcher by some name
396
+ if tkn.Text == "*" {
397
+ // match all requests == no matchers, so nothing to do
398
+ return nil, true, nil
399
+ } else if strings.HasPrefix(tkn.Text, "/") {
400
+ // convenient way to specify a single path match
401
+ return map[string]json.RawMessage{
402
+ "path": caddyconfig.JSON(caddyhttp.MatchPath{tkn.Text}, warnings),
403
+ }, true, nil
404
+ } else if strings.HasPrefix(tkn.Text, "match:") {
405
+ // pre-defined matcher
406
+ matcherName := strings.TrimPrefix(tkn.Text, "match:")
407
+ m, ok := matcherDefs[matcherName]
408
+ if !ok {
409
+ return nil, false, fmt.Errorf("unrecognized matcher name: %+v", matcherName)
410
+ }
411
+ return m, true, nil
412
+ }
413
+
414
+ return nil, false, nil
415
+}
416
+
417
+func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([]map[string]json.RawMessage, error) {
418
+ type hostPathPair struct {
419
+ hostm caddyhttp.MatchHost
420
+ pathm caddyhttp.MatchPath
421
+ }
422
+
423
+ // keep routes with common host and path matchers together
424
+ var matcherPairs []*hostPathPair
425
+
426
+ for _, key := range sblock.Keys {
427
+ addr, err := standardizeAddress(key)
428
+ if err != nil {
429
+ return nil, fmt.Errorf("server block %v: parsing and standardizing address '%s': %v", sblock.Keys, key, err)
430
+ }
431
+
432
+ // choose a matcher pair that should be shared by this
433
+ // server block; if none exists yet, create one
434
+ var chosenMatcherPair *hostPathPair
435
+ for _, mp := range matcherPairs {
436
+ if (len(mp.pathm) == 0 && addr.Path == "") ||
437
+ (len(mp.pathm) == 1 && mp.pathm[0] == addr.Path) {
438
+ chosenMatcherPair = mp
439
+ break
440
+ }
441
+ }
442
+ if chosenMatcherPair == nil {
443
+ chosenMatcherPair = new(hostPathPair)
444
+ if addr.Path != "" {
445
+ chosenMatcherPair.pathm = []string{addr.Path}
446
+ }
447
+ matcherPairs = append(matcherPairs, chosenMatcherPair)
448
+ }
449
+
450
+ // add this server block's keys to the matcher
451
+ // pair if it doesn't already exist
452
+ if addr.Host != "" {
453
+ var found bool
454
+ for _, h := range chosenMatcherPair.hostm {
455
+ if h == addr.Host {
456
+ found = true
457
+ break
458
+ }
459
+ }
460
+ if !found {
461
+ chosenMatcherPair.hostm = append(chosenMatcherPair.hostm, addr.Host)
462
+ }
463
+ }
464
+ }
465
+
466
+ // iterate each pairing of host and path matchers and
467
+ // put them into a map for JSON encoding
468
+ var matcherSets []map[string]caddyhttp.RequestMatcher
469
+ for _, mp := range matcherPairs {
470
+ matcherSet := make(map[string]caddyhttp.RequestMatcher)
471
+ if len(mp.hostm) > 0 {
472
+ matcherSet["host"] = mp.hostm
473
+ }
474
+ if len(mp.pathm) > 0 {
475
+ matcherSet["path"] = mp.pathm
476
+ }
477
+ if len(matcherSet) > 0 {
478
+ matcherSets = append(matcherSets, matcherSet)
479
+ }
480
+ }
481
+
482
+ // finally, encode each of the matcher sets
483
+ var matcherSetsEnc []map[string]json.RawMessage
484
+ for _, ms := range matcherSets {
485
+ msEncoded, err := encodeMatcherSet(ms)
486
+ if err != nil {
487
+ return nil, fmt.Errorf("server block %v: %v", sblock.Keys, err)
488
+ }
489
+ matcherSetsEnc = append(matcherSetsEnc, msEncoded)
490
+ }
491
+
492
+ return matcherSetsEnc, nil
493
+}
494
+
495
+func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (map[string]json.RawMessage, error) {
496
+ msEncoded := make(map[string]json.RawMessage)
497
+ for matcherName, val := range matchers {
498
+ jsonBytes, err := json.Marshal(val)
499
+ if err != nil {
500
+ return nil, fmt.Errorf("marshaling matcher set %#v: %v", matchers, err)
501
+ }
502
+ msEncoded[matcherName] = jsonBytes
503
+ }
504
+ return msEncoded, nil
505
+}
506
+
507
+// HandlerDirective implements a directive for an HTTP handler,
508
+// in that it can unmarshal its own configuration from Caddyfile
509
+// tokens and also specify which directive bucket it belongs in.
510
+type HandlerDirective interface {
511
+ caddyfile.Unmarshaler
512
+ Bucket() int
513
+}
514
+
515
+// tryInt tries to convert str to an integer. If it fails, it downgrades
516
+// the error to a warning and returns 0.
517
+func tryInt(str string, warnings *[]caddyconfig.Warning) int {
518
+ if str == "" {
519
+ return 0
520
+ }
521
+ val, err := strconv.Atoi(str)
522
+ if err != nil && warnings != nil {
523
+ *warnings = append(*warnings, caddyconfig.Warning{Message: err.Error()})
524
+ }
525
+ return val
526
+}
527
+
528
+type matcherSetAndTokens struct {
529
+ matcherSet map[string]json.RawMessage
530
+ tokens []caddyfile.Token
531
+}
532
+
533
+// sbAddrAssocation is a mapping from a list of
534
+// addresses to a list of server blocks that are
535
+// served on those addresses.
536
+type sbAddrAssociation struct {
537
+ addresses []string
538
+ serverBlocks []caddyfile.ServerBlock
539
+}
540
+
541
+// Interface guard
542
+var _ caddyfile.ServerType = (*ServerType)(nil)
cmd/caddy/main.go
+1
@@ -18,6 +18,7 @@ import (
18
caddycmd "github.com/caddyserver/caddy/v2/cmd"
19
20
// this is where modules get plugged in
21
+ _ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
22
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
23
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
24
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/brotli"
cmd/commands.go
+65
-16
@@ -31,6 +31,7 @@ import (
31
"strings"
32
33
"github.com/caddyserver/caddy/v2"
34
+ "github.com/caddyserver/caddy/v2/caddyconfig"
35
"github.com/mholt/certmagic"
36
"github.com/mitchellh/go-ps"
37
)
@@ -38,6 +39,7 @@ import (
39
func cmdStart() (int, error) {
40
startCmd := flag.NewFlagSet("start", flag.ExitOnError)
41
startCmdConfigFlag := startCmd.String("config", "", "Configuration file")
42
+ startCmdConfigAdapterFlag := startCmd.String("config-adapter", "", "Name of config adapter to apply")
43
startCmd.Parse(os.Args[2:])
44
45
// open a listener to which the child process will connect when
@@ -62,6 +64,9 @@ func cmdStart() (int, error) {
64
if *startCmdConfigFlag != "" {
65
cmd.Args = append(cmd.Args, "--config", *startCmdConfigFlag)
66
}
67
+ if *startCmdConfigAdapterFlag != "" {
68
+ cmd.Args = append(cmd.Args, "--config-adapter", *startCmdConfigAdapterFlag)
69
+ }
70
stdinpipe, err := cmd.StdinPipe()
71
if err != nil {
72
return caddy.ExitCodeFailedStartup,
@@ -137,7 +142,8 @@ func cmdStart() (int, error) {
142
func cmdRun() (int, error) {
143
runCmd := flag.NewFlagSet("run", flag.ExitOnError)
144
runCmdConfigFlag := runCmd.String("config", "", "Configuration file")
140
- runCmdPrintEnvFlag := runCmd.Bool("print-env", false, "Print environment (useful for debugging)")
145
+ runCmdConfigAdapterFlag := runCmd.String("config-adapter", "", "Name of config adapter to apply")
146
+ runCmdPrintEnvFlag := runCmd.Bool("print-env", false, "Print environment")
147
runCmdPingbackFlag := runCmd.String("pingback", "", "Echo confirmation bytes to this address on success")
148
runCmd.Parse(os.Args[2:])
149
@@ -149,16 +155,10 @@ func cmdRun() (int, error) {
155
}
156
}
157
152
- // if a config file was specified for bootstrapping
153
- // the server instance, load it now
154
- var config []byte
155
- if *runCmdConfigFlag != "" {
156
- var err error
157
- config, err = ioutil.ReadFile(*runCmdConfigFlag)
158
- if err != nil {
159
- return caddy.ExitCodeFailedStartup,
160
- fmt.Errorf("reading config file: %v", err)
161
- }
158
+ // get the config in caddy's native format
159
+ config, err := loadConfig(*runCmdConfigFlag, *runCmdConfigAdapterFlag)
160
+ if err != nil {
161
+ return caddy.ExitCodeFailedStartup, err
162
}
163
164
// set a fitting User-Agent for ACME requests
@@ -167,7 +167,7 @@ func cmdRun() (int, error) {
167
certmagic.UserAgent = "Caddy/" + cleanModVersion
168
169
// start the admin endpoint along with any initial config
170
- err := caddy.StartAdmin(config)
170
+ err = caddy.StartAdmin(config)
171
if err != nil {
172
return caddy.ExitCodeFailedStartup,
173
fmt.Errorf("starting caddy administration endpoint: %v", err)
@@ -226,6 +226,7 @@ func cmdStop() (int, error) {
226
func cmdReload() (int, error) {
227
reloadCmd := flag.NewFlagSet("load", flag.ExitOnError)
228
reloadCmdConfigFlag := reloadCmd.String("config", "", "Configuration file")
229
+ reloadCmdConfigAdapterFlag := reloadCmd.String("config-adapter", "", "Name of config adapter to apply")
230
reloadCmdAddrFlag := reloadCmd.String("address", "", "Address of the administration listener, if different from config")
231
reloadCmd.Parse(os.Args[2:])
232
@@ -235,11 +236,10 @@ func cmdReload() (int, error) {
236
fmt.Errorf("no configuration to load (use --config)")
237
}
238
238
- // load the configuration file
239
- config, err := ioutil.ReadFile(*reloadCmdConfigFlag)
239
+ // get the config in caddy's native format
240
+ config, err := loadConfig(*reloadCmdConfigFlag, *reloadCmdConfigAdapterFlag)
241
if err != nil {
241
- return caddy.ExitCodeFailedStartup,
242
- fmt.Errorf("reading config file: %v", err)
242
+ return caddy.ExitCodeFailedStartup, err
243
}
244
245
// get the address of the admin listener and craft endpoint URL
@@ -306,3 +306,52 @@ func cmdEnviron() (int, error) {
306
}
307
return caddy.ExitCodeSuccess, nil
308
}
309
+
310
+func cmdAdaptConfig() (int, error) {
311
+ adaptCmd := flag.NewFlagSet("adapt", flag.ExitOnError)
312
+ adaptCmdAdapterFlag := adaptCmd.String("adapter", "", "Name of config adapter")
313
+ adaptCmdInputFlag := adaptCmd.String("input", "", "Configuration file to adapt")
314
+ adaptCmdPrettyFlag := adaptCmd.Bool("pretty", false, "Format the output for human readability")
315
+ adaptCmd.Parse(os.Args[2:])
316
+
317
+ if *adaptCmdAdapterFlag == "" || *adaptCmdInputFlag == "" {
318
+ return caddy.ExitCodeFailedStartup,
319
+ fmt.Errorf("usage: caddy adapt-config --adapter <name> --input <file>")
320
+ }
321
+
322
+ cfgAdapter := caddyconfig.GetAdapter(*adaptCmdAdapterFlag)
323
+ if cfgAdapter == nil {
324
+ return caddy.ExitCodeFailedStartup,
325
+ fmt.Errorf("unrecognized config adapter: %s", *adaptCmdAdapterFlag)
326
+ }
327
+
328
+ input, err := ioutil.ReadFile(*adaptCmdInputFlag)
329
+ if err != nil {
330
+ return caddy.ExitCodeFailedStartup,
331
+ fmt.Errorf("reading input file: %v", err)
332
+ }
333
+
334
+ opts := make(map[string]string)
335
+ if *adaptCmdPrettyFlag {
336
+ opts["pretty"] = "true"
337
+ }
338
+
339
+ adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
340
+ if err != nil {
341
+ return caddy.ExitCodeFailedStartup, err
342
+ }
343
+
344
+ // print warnings to stderr
345
+ for _, warn := range warnings {
346
+ msg := warn.Message
347
+ if warn.Directive != "" {
348
+ msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
349
+ }
350
+ log.Printf("[WARNING][%s] %s:%d: %s", *adaptCmdAdapterFlag, warn.File, warn.Line, msg)
351
+ }
352
+
353
+ // print result to stdout
354
+ fmt.Println(string(adaptedConfig))
355
+
356
+ return caddy.ExitCodeSuccess, nil
357
+}
cmd/main.go
+65
@@ -25,6 +25,7 @@ import (
25
"os"
26
27
"github.com/caddyserver/caddy/v2"
28
+ "github.com/caddyserver/caddy/v2/caddyconfig"
29
)
30
31
// Main implements the main function of the caddy command.
@@ -62,6 +63,7 @@ var commands = map[string]commandFunc{
63
"version": cmdVersion,
64
"list-modules": cmdListModules,
65
"environ": cmdEnviron,
66
+ "adapt-config": cmdAdaptConfig,
67
}
68
69
func usageString() string {
@@ -85,3 +87,66 @@ func handlePingbackConn(conn net.Conn, expect []byte) error {
87
}
88
return nil
89
}
90
+
91
+// loadConfig loads the config from configFile and adapts it
92
+// using adapterName. If adapterName is specified, configFile
93
+// must be also. It prints any warnings to stderr, and returns
94
+// the resulting JSON config bytes.
95
+func loadConfig(configFile, adapterName string) ([]byte, error) {
96
+ // specifying an adapter without a config file is ambiguous
97
+ if configFile == "" && adapterName != "" {
98
+ return nil, fmt.Errorf("cannot adapt config without config file (use --config)")
99
+ }
100
+
101
+ // load initial config and adapter
102
+ var config []byte
103
+ var cfgAdapter caddyconfig.Adapter
104
+ var err error
105
+ if configFile != "" {
106
+ config, err = ioutil.ReadFile(configFile)
107
+ if err != nil {
108
+ return nil, fmt.Errorf("reading config file: %v", err)
109
+ }
110
+ } else if adapterName == "" {
111
+ // as a special case when no config file or adapter
112
+ // is specified, see if the Caddyfile adapter is
113
+ // plugged in, and if so, try using a default Caddyfile
114
+ cfgAdapter = caddyconfig.GetAdapter("caddyfile")
115
+ if cfgAdapter != nil {
116
+ config, err = ioutil.ReadFile("Caddyfile")
117
+ if err != nil && !os.IsNotExist(err) {
118
+ return nil, fmt.Errorf("reading default Caddyfile: %v", err)
119
+ }
120
+ configFile = "Caddyfile"
121
+ }
122
+ }
123
+
124
+ // load config adapter
125
+ if adapterName != "" {
126
+ cfgAdapter = caddyconfig.GetAdapter(adapterName)
127
+ if cfgAdapter == nil {
128
+ return nil, fmt.Errorf("unrecognized config adapter: %s", adapterName)
129
+ }
130
+ }
131
+
132
+ // adapt config
133
+ if cfgAdapter != nil {
134
+ adaptedConfig, warnings, err := cfgAdapter.Adapt(config, map[string]string{
135
+ "filename": configFile,
136
+ // TODO: all other options... (http-port, etc...)
137
+ })
138
+ if err != nil {
139
+ return nil, fmt.Errorf("adapting config using %s: %v", adapterName, err)
140
+ }
141
+ for _, warn := range warnings {
142
+ msg := warn.Message
143
+ if warn.Directive != "" {
144
+ msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
145
+ }
146
+ fmt.Printf("[WARNING][%s] %s:%d: %s", adapterName, warn.File, warn.Line, msg)
147
+ }
148
+ config = adaptedConfig
149
+ }
150
+
151
+ return config, nil
152
+}
modules.go
+20
@@ -38,6 +38,26 @@ type Module struct {
38
New func() interface{}
39
}
40
41
+// ID returns a module's ID, which is the
42
+// last element of its name.
43
+func (m Module) ID() string {
44
+ if m.Name == "" {
45
+ return ""
46
+ }
47
+ parts := strings.Split(m.Name, ".")
48
+ return parts[len(parts)-1]
49
+}
50
+
51
+// Namespace returns the module's namespace (scope)
52
+// which is all but the last element of its name.
53
+func (m Module) Namespace() string {
54
+ lastDot := strings.LastIndex(m.Name, ".")
55
+ if lastDot < 0 {
56
+ return ""
57
+ }
58
+ return m.Name[:lastDot]
59
+}
60
+
61
func (m Module) String() string { return m.Name }
62
63
// RegisterModule registers a module. Modules must call
modules/caddyhttp/caddyhttp.go
+84
-2
@@ -15,9 +15,12 @@
15
package caddyhttp
16
17
import (
18
+ "bytes"
19
"context"
20
"crypto/tls"
21
+ "encoding/json"
22
"fmt"
23
+ "io"
24
"log"
25
weakrand "math/rand"
26
"net"
@@ -244,6 +247,14 @@ func (app *App) automaticHTTPS() error {
247
for d := range domainSet {
248
domains = append(domains, d)
249
if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) {
250
+ // if a certificate for this name is already loaded,
251
+ // don't obtain another one for it, unless we are
252
+ // supposed to ignore loaded certificates
253
+ if !srv.AutoHTTPS.IgnoreLoadedCerts &&
254
+ len(tlsApp.CertificatesWithSAN(d)) > 0 {
255
+ log.Printf("[INFO][%s] Skipping automatic certificate management because a certificate with that SAN is already loaded", d)
256
+ continue
257
+ }
258
domainsForCerts = append(domainsForCerts, d)
259
}
260
}
@@ -319,7 +330,7 @@ func (app *App) automaticHTTPS() error {
330
}
331
redirTo += "{http.request.uri}"
332
322
- redirRoutes = append(redirRoutes, ServerRoute{
333
+ redirRoutes = append(redirRoutes, Route{
334
matcherSets: []MatcherSet{
335
{
336
MatchProtocol("http"),
@@ -328,7 +339,7 @@ func (app *App) automaticHTTPS() error {
339
},
340
handlers: []MiddlewareHandler{
341
StaticResponse{
331
- StatusCode: weakString(strconv.Itoa(http.StatusTemporaryRedirect)), // TODO: use permanent redirect instead
342
+ StatusCode: WeakString(strconv.Itoa(http.StatusTemporaryRedirect)), // TODO: use permanent redirect instead
343
Headers: http.Header{
344
"Location": []string{redirTo},
345
"Connection": []string{"close"},
@@ -431,6 +442,77 @@ type MiddlewareHandler interface {
442
// emptyHandler is used as a no-op handler.
443
var emptyHandler HandlerFunc = func(http.ResponseWriter, *http.Request) error { return nil }
444
445
+// WeakString is a type that unmarshals any JSON value
446
+// as a string literal, with the following exceptions:
447
+// 1) actual string values are decoded as strings, and
448
+// 2) null is decoded as empty string
449
+// and provides methods for getting the value as various
450
+// primitive types. However, using this type removes any
451
+// type safety as far as deserializing JSON is concerned.
452
+type WeakString string
453
+
454
+// UnmarshalJSON satisfies json.Unmarshaler according to
455
+// this type's documentation.
456
+func (ws *WeakString) UnmarshalJSON(b []byte) error {
457
+ if len(b) == 0 {
458
+ return io.EOF
459
+ }
460
+ if b[0] == byte('"') && b[len(b)-1] == byte('"') {
461
+ var s string
462
+ err := json.Unmarshal(b, &s)
463
+ if err != nil {
464
+ return err
465
+ }
466
+ *ws = WeakString(s)
467
+ return nil
468
+ }
469
+ if bytes.Equal(b, []byte("null")) {
470
+ return nil
471
+ }
472
+ *ws = WeakString(b)
473
+ return nil
474
+}
475
+
476
+// MarshalJSON marshals was a boolean if true or false,
477
+// a number if an integer, or a string otherwise.
478
+func (ws WeakString) MarshalJSON() ([]byte, error) {
479
+ if ws == "true" {
480
+ return []byte("true"), nil
481
+ }
482
+ if ws == "false" {
483
+ return []byte("false"), nil
484
+ }
485
+ if num, err := strconv.Atoi(string(ws)); err == nil {
486
+ return json.Marshal(num)
487
+ }
488
+ return json.Marshal(string(ws))
489
+}
490
+
491
+// Int returns ws as an integer. If ws is not an
492
+// integer, 0 is returned.
493
+func (ws WeakString) Int() int {
494
+ num, _ := strconv.Atoi(string(ws))
495
+ return num
496
+}
497
+
498
+// Float64 returns ws as a float64. If ws is not a
499
+// float value, the zero value is returned.
500
+func (ws WeakString) Float64() float64 {
501
+ num, _ := strconv.ParseFloat(string(ws), 64)
502
+ return num
503
+}
504
+
505
+// Bool returns ws as a boolean. If ws is not a
506
+// boolean, false is returned.
507
+func (ws WeakString) Bool() bool {
508
+ return string(ws) == "true"
509
+}
510
+
511
+// String returns ws as a string.
512
+func (ws WeakString) String() string {
513
+ return string(ws)
514
+}
515
+
516
const (
517
// DefaultHTTPPort is the default port for HTTP.
518
DefaultHTTPPort = 80
modules/caddyhttp/encode/brotli/brotli.go
+21
-2
@@ -16,8 +16,10 @@ package caddybrotli
16
17
import (
18
"fmt"
19
+ "strconv"
20
21
"github.com/andybalholm/brotli"
22
+ "github.com/caddyserver/caddy/caddyconfig/caddyfile"
23
"github.com/caddyserver/caddy/v2"
24
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
25
)
@@ -35,6 +37,22 @@ type Brotli struct {
37
Quality *int `json:"quality,omitempty"`
38
}
39
40
+// UnmarshalCaddyfile sets up the handler from Caddyfile tokens.
41
+func (b *Brotli) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
42
+ for d.Next() {
43
+ if !d.NextArg() {
44
+ continue
45
+ }
46
+ qualityStr := d.Val()
47
+ quality, err := strconv.Atoi(qualityStr)
48
+ if err != nil {
49
+ return err
50
+ }
51
+ b.Quality = &quality
52
+ }
53
+ return nil
54
+}
55
+
56
// Validate validates b's configuration.
57
func (b Brotli) Validate() error {
58
if b.Quality != nil {
@@ -64,6 +82,7 @@ func (b Brotli) NewEncoder() encode.Encoder {
82
83
// Interface guards
84
var (
67
- _ encode.Encoding = (*Brotli)(nil)
68
- _ caddy.Validator = (*Brotli)(nil)
85
+ _ encode.Encoding = (*Brotli)(nil)
86
+ _ caddy.Validator = (*Brotli)(nil)
87
+ _ caddyfile.Unmarshaler = (*Brotli)(nil)
88
)
modules/caddyhttp/encode/caddyfile.go
new
+85
@@ -0,0 +1,85 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
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
+
15
+package encode
16
+
17
+import (
18
+ "encoding/json"
19
+ "fmt"
20
+
21
+ "github.com/caddyserver/caddy"
22
+ "github.com/caddyserver/caddy/caddyconfig"
23
+ "github.com/caddyserver/caddy/caddyconfig/caddyfile"
24
+ "github.com/caddyserver/caddy/caddyconfig/httpcaddyfile"
25
+)
26
+
27
+// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
28
+//
29
+// encode [<matcher>] <formats...> {
30
+// gzip [<level>]
31
+// zstd
32
+// brotli [<quality>]
33
+// }
34
+//
35
+// Specifying the formats on the first line will use those formats' defaults.
36
+func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
37
+ for d.Next() {
38
+ for _, arg := range d.RemainingArgs() {
39
+ mod, err := caddy.GetModule("http.encoders." + arg)
40
+ if err != nil {
41
+ return fmt.Errorf("finding encoder module '%s': %v", mod.Name, err)
42
+ }
43
+ encoding, ok := mod.New().(Encoding)
44
+ if !ok {
45
+ return fmt.Errorf("module %s is not an HTTP encoding", mod.Name)
46
+ }
47
+ if enc.EncodingsRaw == nil {
48
+ enc.EncodingsRaw = make(map[string]json.RawMessage)
49
+ }
50
+ enc.EncodingsRaw[arg] = caddyconfig.JSON(encoding, nil)
51
+ }
52
+
53
+ for d.NextBlock() {
54
+ name := d.Val()
55
+ mod, err := caddy.GetModule("http.encoders." + name)
56
+ if err != nil {
57
+ return fmt.Errorf("getting encoder module '%s': %v", mod.Name, err)
58
+ }
59
+ unm, ok := mod.New().(caddyfile.Unmarshaler)
60
+ if !ok {
61
+ return fmt.Errorf("encoder module '%s' is not a Caddyfile unmarshaler", mod.Name)
62
+ }
63
+ err = unm.UnmarshalCaddyfile(d.NewFromNextTokens())
64
+ if err != nil {
65
+ return err
66
+ }
67
+ encoding, ok := unm.(Encoding)
68
+ if !ok {
69
+ return fmt.Errorf("module %s is not an HTTP encoding", mod.Name)
70
+ }
71
+ if enc.EncodingsRaw == nil {
72
+ enc.EncodingsRaw = make(map[string]json.RawMessage)
73
+ }
74
+ enc.EncodingsRaw[name] = caddyconfig.JSON(encoding, nil)
75
+ }
76
+ }
77
+
78
+ return nil
79
+}
80
+
81
+// Bucket returns the HTTP Caddyfile handler bucket number.
82
+func (enc Encode) Bucket() int { return 3 }
83
+
84
+// Interface guard
85
+var _ httpcaddyfile.HandlerDirective = (*Encode)(nil)
modules/caddyhttp/encode/encode.go
+23
-9
@@ -52,19 +52,15 @@ type Encode struct {
52
53
// Provision provisions enc.
54
func (enc *Encode) Provision(ctx caddy.Context) error {
55
- enc.writerPools = make(map[string]*sync.Pool)
56
-
55
for modName, rawMsg := range enc.EncodingsRaw {
56
val, err := ctx.LoadModule("http.encoders."+modName, rawMsg)
57
if err != nil {
58
return fmt.Errorf("loading encoder module '%s': %v", modName, err)
59
}
62
- encoder := val.(Encoding)
63
-
64
- enc.writerPools[encoder.AcceptEncoding()] = &sync.Pool{
65
- New: func() interface{} {
66
- return encoder.NewEncoder()
67
- },
60
+ encoding := val.(Encoding)
61
+ err = enc.addEncoding(encoding)
62
+ if err != nil {
63
+ return err
64
}
65
}
66
enc.EncodingsRaw = nil // allow GC to deallocate - TODO: Does this help?
@@ -85,10 +81,28 @@ func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
81
defer w.(*responseWriter).Close()
82
break
83
}
88
-
84
return next.ServeHTTP(w, r)
85
}
86
87
+func (enc *Encode) addEncoding(e Encoding) error {
88
+ ae := e.AcceptEncoding()
89
+ if ae == "" {
90
+ return fmt.Errorf("encoder does not specify an Accept-Encoding value")
91
+ }
92
+ if _, ok := enc.writerPools[ae]; ok {
93
+ return fmt.Errorf("encoder already added: %s", ae)
94
+ }
95
+ if enc.writerPools == nil {
96
+ enc.writerPools = make(map[string]*sync.Pool)
97
+ }
98
+ enc.writerPools[ae] = &sync.Pool{
99
+ New: func() interface{} {
100
+ return e.NewEncoder()
101
+ },
102
+ }
103
+ return nil
104
+}
105
+
106
// openResponseWriter creates a new response writer that may (or may not)
107
// encode the response with encodingName. The returned response writer MUST
108
// be closed after the handler completes.
modules/caddyhttp/encode/gzip/gzip.go
+22
-3
@@ -18,7 +18,9 @@ import (
18
"compress/flate"
19
"compress/gzip" // TODO: consider using https://github.com/klauspost/compress/gzip
20
"fmt"
21
+ "strconv"
22
23
+ "github.com/caddyserver/caddy/caddyconfig/caddyfile"
24
"github.com/caddyserver/caddy/v2"
25
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
26
)
@@ -35,6 +37,22 @@ type Gzip struct {
37
Level int `json:"level,omitempty"`
38
}
39
40
+// UnmarshalCaddyfile sets up the handler from Caddyfile tokens.
41
+func (g *Gzip) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
42
+ for d.Next() {
43
+ if !d.NextArg() {
44
+ continue
45
+ }
46
+ levelStr := d.Val()
47
+ level, err := strconv.Atoi(levelStr)
48
+ if err != nil {
49
+ return err
50
+ }
51
+ g.Level = level
52
+ }
53
+ return nil
54
+}
55
+
56
// Provision provisions g's configuration.
57
func (g *Gzip) Provision(ctx caddy.Context) error {
58
if g.Level == 0 {
@@ -69,7 +87,8 @@ var defaultGzipLevel = 5
87
88
// Interface guards
89
var (
72
- _ encode.Encoding = (*Gzip)(nil)
73
- _ caddy.Provisioner = (*Gzip)(nil)
74
- _ caddy.Validator = (*Gzip)(nil)
90
+ _ encode.Encoding = (*Gzip)(nil)
91
+ _ caddy.Provisioner = (*Gzip)(nil)
92
+ _ caddy.Validator = (*Gzip)(nil)
93
+ _ caddyfile.Unmarshaler = (*Gzip)(nil)
94
)
modules/caddyhttp/encode/zstd/zstd.go
+11
-2
@@ -15,6 +15,7 @@
15
package caddyzstd
16
17
import (
18
+ "github.com/caddyserver/caddy/caddyconfig/caddyfile"
19
"github.com/caddyserver/caddy/v2"
20
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
21
"github.com/klauspost/compress/zstd"
@@ -30,6 +31,11 @@ func init() {
31
// Zstd can create Zstandard encoders.
32
type Zstd struct{}
33
34
+// UnmarshalCaddyfile sets up the handler from Caddyfile tokens.
35
+func (z *Zstd) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
36
+ return nil
37
+}
38
+
39
// AcceptEncoding returns the name of the encoding as
40
// used in the Accept-Encoding request headers.
41
func (Zstd) AcceptEncoding() string { return "zstd" }
@@ -40,5 +46,8 @@ func (z Zstd) NewEncoder() encode.Encoder {
46
return writer
47
}
48
43
-// Interface guard
44
-var _ encode.Encoding = (*Zstd)(nil)
49
+// Interface guards
50
+var (
51
+ _ encode.Encoding = (*Zstd)(nil)
52
+ _ caddyfile.Unmarshaler = (*Zstd)(nil)
53
+)
modules/caddyhttp/fileserver/caddyfile.go
new
+87
@@ -0,0 +1,87 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
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
+
15
+package fileserver
16
+
17
+import (
18
+ "github.com/caddyserver/caddy/caddyconfig/caddyfile"
19
+ "github.com/caddyserver/caddy/caddyconfig/httpcaddyfile"
20
+)
21
+
22
+// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
23
+//
24
+// file_server [<matcher>] [browse] {
25
+// hide <files...>
26
+// index <files...>
27
+// browse [<template_file>]
28
+// root <path>
29
+// }
30
+//
31
+// If browse is given on the first line, it can't be used in the block also.
32
+// The default root is the one given by the root directive.
33
+func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
34
+ for d.Next() {
35
+ args := d.RemainingArgs()
36
+ switch len(args) {
37
+ case 0:
38
+ case 1:
39
+ if args[0] != "browse" {
40
+ return d.ArgErr()
41
+ }
42
+ fsrv.Browse = new(Browse)
43
+ default:
44
+ return d.ArgErr()
45
+ }
46
+
47
+ for d.NextBlock() {
48
+ switch d.Val() {
49
+ case "hide":
50
+ fsrv.Hide = d.RemainingArgs()
51
+ if len(fsrv.Hide) == 0 {
52
+ return d.ArgErr()
53
+ }
54
+ case "index":
55
+ fsrv.IndexNames = d.RemainingArgs()
56
+ if len(fsrv.Hide) == 0 {
57
+ return d.ArgErr()
58
+ }
59
+ case "root":
60
+ if !d.Args(&fsrv.Root) {
61
+ return d.ArgErr()
62
+ }
63
+ case "browse":
64
+ if fsrv.Browse != nil {
65
+ return d.Err("browsing is already configured")
66
+ }
67
+ fsrv.Browse = new(Browse)
68
+ d.Args(&fsrv.Browse.TemplateFile)
69
+ default:
70
+ return d.Errf("unknown subdirective '%s'", d.Val())
71
+ }
72
+ }
73
+ }
74
+
75
+ // if no root was configured explicitly, use site root
76
+ if fsrv.Root == "" {
77
+ fsrv.Root = "{http.var.root}"
78
+ }
79
+
80
+ return nil
81
+}
82
+
83
+// Bucket returns the HTTP Caddyfile handler bucket number.
84
+func (fsrv FileServer) Bucket() int { return 7 }
85
+
86
+// Interface guard
87
+var _ httpcaddyfile.HandlerDirective = (*FileServer)(nil)
modules/caddyhttp/fileserver/matcher.go
+34
@@ -20,6 +20,7 @@ import (
20
"os"
21
"time"
22
23
+ "github.com/caddyserver/caddy/caddyconfig/caddyfile"
24
"github.com/caddyserver/caddy/v2"
25
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
26
)
@@ -51,6 +52,39 @@ type MatchFile struct {
52
TryPolicy string `json:"try_policy,omitempty"`
53
}
54
55
+// UnmarshalCaddyfile sets up the matcher from Caddyfile tokens. Syntax:
56
+//
57
+// file {
58
+// root <path>
59
+// try_files <files...>
60
+// try_policy <first_exist|smallest_size|largest_size|most_recent_modified>
61
+// }
62
+//
63
+func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
64
+ for d.Next() {
65
+ for d.NextBlock() {
66
+ switch d.Val() {
67
+ case "root":
68
+ if !d.NextArg() {
69
+ return d.ArgErr()
70
+ }
71
+ m.Root = d.Val()
72
+ case "try_files":
73
+ m.TryFiles = d.RemainingArgs()
74
+ if len(m.TryFiles) == 0 {
75
+ return d.ArgErr()
76
+ }
77
+ case "try_policy":
78
+ if !d.NextArg() {
79
+ return d.ArgErr()
80
+ }
81
+ m.TryPolicy = d.Val()
82
+ }
83
+ }
84
+ }
85
+ return nil
86
+}
87
+
88
// Validate ensures m has a valid configuration.
89
func (m MatchFile) Validate() error {
90
switch m.TryPolicy {
modules/caddyhttp/fileserver/staticfiles.go
+2
-4
@@ -48,8 +48,6 @@ type FileServer struct {
48
Hide []string `json:"hide,omitempty"`
49
IndexNames []string `json:"index_names,omitempty"`
50
Browse *Browse `json:"browse,omitempty"`
51
-
52
- // TODO: Content negotiation
51
}
52
53
// Provision sets up the static files responder.
@@ -83,7 +81,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, _ cadd
81
82
filesToHide := fsrv.transformHidePaths(repl)
83
86
- root := repl.ReplaceAll(fsrv.Root, "")
84
+ root := repl.ReplaceAll(fsrv.Root, ".")
85
suffix := repl.ReplaceAll(r.URL.Path, "")
86
filename := sanitizedPathJoin(root, suffix)
87
@@ -302,7 +300,7 @@ func calculateEtag(d os.FileInfo) string {
300
return `"` + t + s + `"`
301
}
302
305
-var defaultIndexNames = []string{"index.html"}
303
+var defaultIndexNames = []string{"index.html", "index.txt"}
304
305
var bufPool = sync.Pool{
306
New: func() interface{} {
modules/caddyhttp/headers/caddyfile.go
new
+92
@@ -0,0 +1,92 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
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
+
15
+package headers
16
+
17
+import (
18
+ "net/http"
19
+ "strings"
20
+
21
+ "github.com/caddyserver/caddy/caddyconfig/caddyfile"
22
+ "github.com/caddyserver/caddy/caddyconfig/httpcaddyfile"
23
+)
24
+
25
+// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
26
+//
27
+// headers [<matcher>] [[+|-]<field> <value>] {
28
+// [+][<field>] [<value>]
29
+// [-<field>]
30
+// }
31
+//
32
+// Either a block can be opened or a single header field can be configured
33
+// in the first line, but not both in the same directive.
34
+func (h *Headers) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
35
+ for d.Next() {
36
+ // first see if headers are in the initial line
37
+ var hasArgs bool
38
+ if d.NextArg() {
39
+ hasArgs = true
40
+ field := d.Val()
41
+ d.NextArg()
42
+ value := d.Val()
43
+ h.processCaddyfileLine(field, value)
44
+ }
45
+
46
+ // if not, they should be in a block
47
+ for d.NextBlock() {
48
+ if hasArgs {
49
+ return d.Err("cannot specify headers in both arguments and block")
50
+ }
51
+ field := d.Val()
52
+ var value string
53
+ if d.NextArg() {
54
+ value = d.Val()
55
+ }
56
+ h.processCaddyfileLine(field, value)
57
+ }
58
+ }
59
+ return nil
60
+}
61
+
62
+func (h *Headers) processCaddyfileLine(field, value string) {
63
+ if strings.HasPrefix(field, "+") {
64
+ if h.Response == nil {
65
+ h.Response = &RespHeaderOps{HeaderOps: new(HeaderOps)}
66
+ }
67
+ if h.Response.Add == nil {
68
+ h.Response.Add = make(http.Header)
69
+ }
70
+ h.Response.Add.Set(field[1:], value)
71
+ } else if strings.HasPrefix(field, "-") {
72
+ if h.Response == nil {
73
+ h.Response = &RespHeaderOps{HeaderOps: new(HeaderOps)}
74
+ }
75
+ h.Response.Delete = append(h.Response.Delete, field[1:])
76
+ h.Response.Deferred = true
77
+ } else {
78
+ if h.Response == nil {
79
+ h.Response = &RespHeaderOps{HeaderOps: new(HeaderOps)}
80
+ }
81
+ if h.Response.Set == nil {
82
+ h.Response.Set = make(http.Header)
83
+ }
84
+ h.Response.Set.Set(field, value)
85
+ }
86
+}
87
+
88
+// Bucket returns the HTTP Caddyfile handler bucket number.
89
+func (h Headers) Bucket() int { return 3 }
90
+
91
+// Interface guard
92
+var _ httpcaddyfile.HandlerDirective = (*Headers)(nil)
modules/caddyhttp/matchers.go
+99
-1
@@ -28,6 +28,7 @@ import (
28
"strings"
29
30
"github.com/caddyserver/caddy/v2"
31
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
32
"github.com/caddyserver/caddy/v2/pkg/caddyscript"
33
"go.starlark.net/starlark"
34
)
@@ -125,6 +126,12 @@ func init() {
126
})
127
}
128
129
+// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
130
+func (m *MatchHost) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
131
+ *m = d.RemainingArgs()
132
+ return nil
133
+}
134
+
135
// Match returns true if r matches m.
136
func (m MatchHost) Match(r *http.Request) bool {
137
reqHost, _, err := net.SplitHostPort(r.Host)
@@ -177,12 +184,24 @@ func (m MatchPath) Match(r *http.Request) bool {
184
return false
185
}
186
187
+// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
188
+func (m *MatchPath) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
189
+ *m = d.RemainingArgs()
190
+ return nil
191
+}
192
+
193
// Match returns true if r matches m.
194
func (m MatchPathRE) Match(r *http.Request) bool {
195
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
196
return m.MatchRegexp.Match(r.URL.Path, repl, "path_regexp")
197
}
198
199
+// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
200
+func (m *MatchMethod) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
201
+ *m = d.RemainingArgs()
202
+ return nil
203
+}
204
+
205
// Match returns true if r matches m.
206
func (m MatchMethod) Match(r *http.Request) bool {
207
for _, method := range m {
@@ -193,6 +212,18 @@ func (m MatchMethod) Match(r *http.Request) bool {
212
return false
213
}
214
215
+// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
216
+func (m *MatchQuery) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
217
+ for d.Next() {
218
+ parts := strings.SplitN(d.Val(), "=", 2)
219
+ if len(parts) != 2 {
220
+ return d.Errf("malformed query matcher token: %s; must be in param=val format", d.Val())
221
+ }
222
+ url.Values(*m).Set(parts[0], parts[1])
223
+ }
224
+ return nil
225
+}
226
+
227
// Match returns true if r matches m.
228
func (m MatchQuery) Match(r *http.Request) bool {
229
for param, vals := range m {
@@ -206,6 +237,18 @@ func (m MatchQuery) Match(r *http.Request) bool {
237
return false
238
}
239
240
+// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
241
+func (m *MatchHeader) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
242
+ for d.Next() {
243
+ var field, val string
244
+ if !d.Args(&field, &val) {
245
+ return d.Errf("expected both field and value")
246
+ }
247
+ http.Header(*m).Set(field, val)
248
+ }
249
+ return nil
250
+}
251
+
252
// Match returns true if r matches m.
253
func (m MatchHeader) Match(r *http.Request) bool {
254
for field, allowedFieldVals := range m {
@@ -227,6 +270,21 @@ func (m MatchHeader) Match(r *http.Request) bool {
270
return true
271
}
272
273
+// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
274
+func (m *MatchHeaderRE) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
275
+ if *m == nil {
276
+ *m = make(map[string]*MatchRegexp)
277
+ }
278
+ for d.Next() {
279
+ var field, val string
280
+ if !d.Args(&field, &val) {
281
+ return d.ArgErr()
282
+ }
283
+ (*m)[field] = &MatchRegexp{Pattern: val}
284
+ }
285
+ return nil
286
+}
287
+
288
// Match returns true if r matches m.
289
func (m MatchHeaderRE) Match(r *http.Request) bool {
290
for field, rm := range m {
@@ -274,6 +332,16 @@ func (m MatchProtocol) Match(r *http.Request) bool {
332
return false
333
}
334
335
+// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
336
+func (m *MatchProtocol) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
337
+ var proto string
338
+ if !d.Args(&proto) {
339
+ return d.Err("expected exactly one protocol")
340
+ }
341
+ *m = MatchProtocol(proto)
342
+ return nil
343
+}
344
+
345
// UnmarshalJSON unmarshals data into m's unexported map field.
346
// This is done because we cannot embed the map directly into
347
// the struct, but we need a struct because we need another
@@ -282,6 +350,12 @@ func (m *MatchNegate) UnmarshalJSON(data []byte) error {
350
return json.Unmarshal(data, &m.matchersRaw)
351
}
352
353
+// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
354
+func (m *MatchNegate) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
355
+ // TODO: figure out how this will work
356
+ return nil
357
+}
358
+
359
// Provision loads the matcher modules to be negated.
360
func (m *MatchNegate) Provision(ctx caddy.Context) error {
361
for modName, rawMsg := range m.matchersRaw {
@@ -301,6 +375,12 @@ func (m MatchNegate) Match(r *http.Request) bool {
375
return !m.matchers.Match(r)
376
}
377
378
+// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
379
+func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
380
+ m.Ranges = d.RemainingArgs()
381
+ return nil
382
+}
383
+
384
// Provision parses m's IP ranges, either from IP or CIDR expressions.
385
func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
386
for _, str := range m.Ranges {
@@ -379,7 +459,7 @@ func (m MatchStarlarkExpr) Match(r *http.Request) bool {
459
// MatchRegexp is an embeddable type for matching
460
// using regular expressions.
461
type MatchRegexp struct {
382
- Name string `json:"name"`
462
+ Name string `json:"name,omitempty"`
463
Pattern string `json:"pattern"`
464
compiled *regexp.Regexp
465
}
@@ -431,6 +511,14 @@ func (mre *MatchRegexp) Match(input string, repl caddy.Replacer, scope string) b
511
return true
512
}
513
514
+// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
515
+func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
516
+ if !d.Args(&mre.Name, &mre.Pattern) {
517
+ return fmt.Errorf("missing arguments")
518
+ }
519
+ return nil
520
+}
521
+
522
// ResponseMatcher is a type which can determine if a given response
523
// status code and its headers match some criteria.
524
type ResponseMatcher struct {
@@ -506,4 +594,14 @@ var (
594
_ caddy.Provisioner = (*MatchNegate)(nil)
595
_ RequestMatcher = (*MatchStarlarkExpr)(nil)
596
_ caddy.Provisioner = (*MatchRegexp)(nil)
597
+
598
+ _ caddyfile.Unmarshaler = (*MatchHost)(nil)
599
+ _ caddyfile.Unmarshaler = (*MatchPath)(nil)
600
+ _ caddyfile.Unmarshaler = (*MatchPathRE)(nil)
601
+ _ caddyfile.Unmarshaler = (*MatchMethod)(nil)
602
+ _ caddyfile.Unmarshaler = (*MatchQuery)(nil)
603
+ _ caddyfile.Unmarshaler = (*MatchHeader)(nil)
604
+ _ caddyfile.Unmarshaler = (*MatchHeaderRE)(nil)
605
+ _ caddyfile.Unmarshaler = (*MatchProtocol)(nil)
606
+ _ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
607
)
modules/caddyhttp/replacer.go
+29
-8
@@ -15,6 +15,7 @@
15
package caddyhttp
16
17
import (
18
+ "fmt"
19
"net"
20
"net/http"
21
"net/textproto"
@@ -28,6 +29,7 @@ import (
29
func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.ResponseWriter) {
30
httpVars := func(key string) (string, bool) {
31
if req != nil {
32
+ // query string parameters
33
if strings.HasPrefix(key, queryReplPrefix) {
34
vals := req.URL.Query()[key[len(queryReplPrefix):]]
35
// always return true, since the query param might
@@ -35,6 +37,7 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon
37
return strings.Join(vals, ","), true
38
}
39
40
+ // request header fields
41
if strings.HasPrefix(key, reqHeaderReplPrefix) {
42
field := key[len(reqHeaderReplPrefix):]
43
vals := req.Header[textproto.CanonicalMIMEHeaderKey(field)]
@@ -43,6 +46,7 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon
46
return strings.Join(vals, ","), true
47
}
48
49
+ // cookies
50
if strings.HasPrefix(key, cookieReplPrefix) {
51
name := key[len(cookieReplPrefix):]
52
for _, cookie := range req.Cookies() {
@@ -87,14 +91,7 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon
91
return req.URL.RawQuery, true
92
}
93
90
- if strings.HasPrefix(key, respHeaderReplPrefix) {
91
- field := key[len(respHeaderReplPrefix):]
92
- vals := w.Header()[textproto.CanonicalMIMEHeaderKey(field)]
93
- // always return true, since the header field might
94
- // be present only in some requests
95
- return strings.Join(vals, ","), true
96
- }
97
-
94
+ // hostname labels
95
if strings.HasPrefix(key, hostLabelReplPrefix) {
96
idxStr := key[len(hostLabelReplPrefix):]
97
idx, err := strconv.Atoi(idxStr)
@@ -111,6 +108,7 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon
108
return hostLabels[idx], true
109
}
110
111
+ // path parts
112
if strings.HasPrefix(key, pathPartsReplPrefix) {
113
idxStr := key[len(pathPartsReplPrefix):]
114
idx, err := strconv.Atoi(idxStr)
@@ -129,9 +127,31 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon
127
}
128
return pathParts[idx], true
129
}
130
+
131
+ // middleware variables
132
+ if strings.HasPrefix(key, varsReplPrefix) {
133
+ varName := key[len(varsReplPrefix):]
134
+ tbl := req.Context().Value(VarCtxKey).(map[string]interface{})
135
+ raw, ok := tbl[varName]
136
+ if !ok {
137
+ // variables can be dynamic, so always return true
138
+ // even when it may not be set; treat as empty
139
+ return "", true
140
+ }
141
+ // do our best to convert it to a string efficiently
142
+ switch val := raw.(type) {
143
+ case string:
144
+ return val, true
145
+ case fmt.Stringer:
146
+ return val.String(), true
147
+ default:
148
+ return fmt.Sprintf("%s", val), true
149
+ }
150
+ }
151
}
152
153
if w != nil {
154
+ // response header fields
155
if strings.HasPrefix(key, respHeaderReplPrefix) {
156
field := key[len(respHeaderReplPrefix):]
157
vals := w.Header()[textproto.CanonicalMIMEHeaderKey(field)]
@@ -153,5 +173,6 @@ const (
173
cookieReplPrefix = "http.request.cookie."
174
hostLabelReplPrefix = "http.request.host.labels."
175
pathPartsReplPrefix = "http.request.uri.path."
176
+ varsReplPrefix = "http.var."
177
respHeaderReplPrefix = "http.response.header."
178
)
modules/caddyhttp/reverseproxy/module.go
+26
@@ -15,6 +15,8 @@
15
package reverseproxy
16
17
import (
18
+ "github.com/caddyserver/caddy/caddyconfig/caddyfile"
19
+ "github.com/caddyserver/caddy/caddyconfig/httpcaddyfile"
20
"github.com/caddyserver/caddy/v2"
21
)
22
@@ -25,3 +27,27 @@ func init() {
27
New: func() interface{} { return new(LoadBalanced) },
28
})
29
}
30
+
31
+// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
32
+//
33
+// proxy [<matcher>] <to>
34
+//
35
+// TODO: This needs to be finished. It definitely needs to be able to open a block...
36
+func (lb *LoadBalanced) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
37
+ for d.Next() {
38
+ allTo := d.RemainingArgs()
39
+ if len(allTo) == 0 {
40
+ return d.ArgErr()
41
+ }
42
+ for _, to := range allTo {
43
+ lb.Upstreams = append(lb.Upstreams, &UpstreamConfig{Host: to})
44
+ }
45
+ }
46
+ return nil
47
+}
48
+
49
+// Bucket returns the HTTP Caddyfile handler bucket number.
50
+func (*LoadBalanced) Bucket() int { return 7 }
51
+
52
+// Interface guard
53
+var _ httpcaddyfile.HandlerDirective = (*LoadBalanced)(nil)
modules/caddyhttp/reverseproxy/upstream.go
+11
-11
@@ -179,21 +179,21 @@ type LoadBalanced struct {
179
180
// The following struct fields are set by caddy configuration.
181
// TryInterval is the max duration for which request retrys will be performed for a request.
182
- TryInterval string `json:"try_interval"`
182
+ TryInterval string `json:"try_interval,omitempty"`
183
184
// Upstreams are the configs for upstream hosts
185
- Upstreams []*UpstreamConfig `json:"upstreams"`
185
+ Upstreams []*UpstreamConfig `json:"upstreams,omitempty"`
186
187
// LoadBalanceType is the string representation of what loadbalancing algorithm to use. i.e. "random" or "round_robin".
188
- LoadBalanceType string `json:"load_balance_type"`
188
+ LoadBalanceType string `json:"load_balance_type,omitempty"`
189
190
// NoHealthyUpstreamsMessage is returned as a response when there are no healthy upstreams to loadbalance to.
191
- NoHealthyUpstreamsMessage string `json:"no_healthy_upstreams_message"`
191
+ NoHealthyUpstreamsMessage string `json:"no_healthy_upstreams_message,omitempty"`
192
193
// TODO :- store healthcheckers as package level state where each upstream gets a single healthchecker
194
// currently a healthchecker is created for each upstream defined, even if a healthchecker was previously created
195
// for that upstream
196
- HealthCheckers []*HealthChecker
196
+ HealthCheckers []*HealthChecker `json:"health_checkers,omitempty"`
197
}
198
199
// Cleanup stops all health checkers on a loadbalanced reverse proxy.
@@ -320,22 +320,22 @@ func (lb *LoadBalanced) random() *upstream {
320
// UpstreamConfig represents the config of an upstream.
321
type UpstreamConfig struct {
322
// Host is the host name of the upstream server.
323
- Host string `json:"host"`
323
+ Host string `json:"host,omitempty"`
324
325
// FastHealthCheckDuration is the duration for which a health check is performed when a node is considered unhealthy.
326
- FastHealthCheckDuration string `json:"fast_health_check_duration"`
326
+ FastHealthCheckDuration string `json:"fast_health_check_duration,omitempty"`
327
328
- CircuitBreaker json.RawMessage `json:"circuit_breaker"`
328
+ CircuitBreaker json.RawMessage `json:"circuit_breaker,omitempty"`
329
330
// // CircuitBreakerConfig is the config passed to setup a circuit breaker.
331
- // CircuitBreakerConfig *circuitbreaker.Config `json:"circuit_breaker"`
331
+ // CircuitBreakerConfig *circuitbreaker.Config `json:"circuit_breaker,omitempty"`
332
circuitbreaker CircuitBreaker
333
334
// HealthCheckDuration is the default duration for which a health check is performed.
335
- HealthCheckDuration string `json:"health_check_duration"`
335
+ HealthCheckDuration string `json:"health_check_duration,omitempty"`
336
337
// HealthCheckPath is the path at the upstream host to use for healthchecks.
338
- HealthCheckPath string `json:"health_check_path"`
338
+ HealthCheckPath string `json:"health_check_path,omitempty"`
339
}
340
341
// upstream represents an upstream host.
modules/caddyhttp/rewrite/caddyfile.go
new
+38
@@ -0,0 +1,38 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
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
+
15
+package rewrite
16
+
17
+import (
18
+ "github.com/caddyserver/caddy/caddyconfig/caddyfile"
19
+ "github.com/caddyserver/caddy/caddyconfig/httpcaddyfile"
20
+)
21
+
22
+// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
23
+//
24
+// rewrite [<matcher>] <to>
25
+//
26
+// The <to> parameter becomes the new URI.
27
+func (rewr *Rewrite) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
28
+ for d.Next() {
29
+ rewr.URI = d.Val()
30
+ }
31
+ return nil
32
+}
33
+
34
+// Bucket returns the HTTP Caddyfile handler bucket number.
35
+func (rewr Rewrite) Bucket() int { return 1 }
36
+
37
+// Interface guard
38
+var _ httpcaddyfile.HandlerDirective = (*Rewrite)(nil)
modules/caddyhttp/routes.go
+13
-13
@@ -22,10 +22,10 @@ import (
22
"github.com/caddyserver/caddy/v2"
23
)
24
25
-// ServerRoute represents a set of matching rules,
25
+// Route represents a set of matching rules,
26
// middlewares, and a responder for handling HTTP
27
// requests.
28
-type ServerRoute struct {
28
+type Route struct {
29
Group string `json:"group,omitempty"`
30
MatcherSets []map[string]json.RawMessage `json:"match,omitempty"`
31
Handle []json.RawMessage `json:"handle,omitempty"`
@@ -37,22 +37,22 @@ type ServerRoute struct {
37
}
38
39
// Empty returns true if the route has all zero/default values.
40
-func (sr ServerRoute) Empty() bool {
41
- return len(sr.MatcherSets) == 0 &&
42
- len(sr.Handle) == 0 &&
43
- len(sr.handlers) == 0 &&
44
- !sr.Terminal &&
45
- sr.Group == ""
40
+func (r Route) Empty() bool {
41
+ return len(r.MatcherSets) == 0 &&
42
+ len(r.Handle) == 0 &&
43
+ len(r.handlers) == 0 &&
44
+ !r.Terminal &&
45
+ r.Group == ""
46
}
47
48
-func (sr ServerRoute) anyMatcherSetMatches(r *http.Request) bool {
49
- for _, ms := range sr.matcherSets {
50
- if ms.Match(r) {
48
+func (r Route) anyMatcherSetMatches(req *http.Request) bool {
49
+ for _, ms := range r.matcherSets {
50
+ if ms.Match(req) {
51
return true
52
}
53
}
54
// if no matchers, always match
55
- return len(sr.matcherSets) == 0
55
+ return len(r.matcherSets) == 0
56
}
57
58
// MatcherSet is a set of matchers which
@@ -73,7 +73,7 @@ func (mset MatcherSet) Match(r *http.Request) bool {
73
74
// RouteList is a list of server routes that can
75
// create a middleware chain.
76
-type RouteList []ServerRoute
76
+type RouteList []Route
77
78
// Provision sets up all the routes by loading the modules.
79
func (routes RouteList) Provision(ctx caddy.Context) error {
modules/caddyhttp/server.go
+11
-3
@@ -57,7 +57,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
57
repl := caddy.NewReplacer()
58
ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
59
ctx = context.WithValue(ctx, ServerCtxKey, s)
60
- ctx = context.WithValue(ctx, TableCtxKey, make(map[string]interface{})) // TODO: Implement this
60
+ ctx = context.WithValue(ctx, VarCtxKey, make(map[string]interface{}))
61
r = r.WithContext(ctx)
62
63
// once the pointer to the request won't change
@@ -201,6 +201,14 @@ type AutoHTTPSConfig struct {
201
// that certificates will not be provisioned and managed
202
// for these names.
203
SkipCerts []string `json:"skip_certificates,omitempty"`
204
+
205
+ // By default, automatic HTTPS will obtain and renew
206
+ // certificates for qualifying hostnames. However, if
207
+ // a certificate with a matching SAN is already loaded
208
+ // into the cache, certificate management will not be
209
+ // enabled. To force automated certificate management
210
+ // regardless of loaded certificates, set this to true.
211
+ IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"`
212
}
213
214
// Skipped returns true if name is in skipSlice, which
@@ -225,6 +233,6 @@ const (
233
// For referencing the server instance
234
ServerCtxKey caddy.CtxKey = "server"
235
228
- // For the request's variable table (TODO: implement this)
229
- TableCtxKey caddy.CtxKey = "table"
236
+ // For the request's variable table
237
+ VarCtxKey caddy.CtxKey = "vars"
238
)
modules/caddyhttp/staticerror.go
+1
-42
@@ -18,7 +18,6 @@ import (
18
"fmt"
19
"net/http"
20
"strconv"
21
- "strings"
21
22
"github.com/caddyserver/caddy/v2"
23
)
@@ -33,7 +32,7 @@ func init() {
32
// StaticError implements a simple handler that returns an error.
33
type StaticError struct {
34
Error string `json:"error,omitempty"`
36
- StatusCode weakString `json:"status_code,omitempty"`
35
+ StatusCode WeakString `json:"status_code,omitempty"`
36
}
37
38
func (e StaticError) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error {
@@ -53,43 +52,3 @@ func (e StaticError) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler
52
53
// Interface guard
54
var _ MiddlewareHandler = (*StaticError)(nil)
56
-
57
-// weakString is a type that unmarshals any JSON value
58
-// as a string literal, and provides methods for
59
-// getting the value as different primitive types.
60
-// However, using this type removes any type safety
61
-// as far as deserializing JSON is concerned.
62
-type weakString string
63
-
64
-// UnmarshalJSON satisfies json.Unmarshaler. It
65
-// unmarshals b by always interpreting it as a
66
-// string literal.
67
-func (ws *weakString) UnmarshalJSON(b []byte) error {
68
- *ws = weakString(strings.Trim(string(b), `"`))
69
- return nil
70
-}
71
-
72
-// Int returns ws as an integer. If ws is not an
73
-// integer, 0 is returned.
74
-func (ws weakString) Int() int {
75
- num, _ := strconv.Atoi(string(ws))
76
- return num
77
-}
78
-
79
-// Float64 returns ws as a float64. If ws is not a
80
-// float value, the zero value is returned.
81
-func (ws weakString) Float64() float64 {
82
- num, _ := strconv.ParseFloat(string(ws), 64)
83
- return num
84
-}
85
-
86
-// Bool returns ws as a boolean. If ws is not a
87
-// boolean, false is returned.
88
-func (ws weakString) Bool() bool {
89
- return string(ws) == "true"
90
-}
91
-
92
-// String returns ws as a string.
93
-func (ws weakString) String() string {
94
- return string(ws)
95
-}
modules/caddyhttp/staticresp.go
+41
-4
@@ -20,6 +20,7 @@ import (
20
"strconv"
21
22
"github.com/caddyserver/caddy/v2"
23
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
24
)
25
26
func init() {
@@ -31,12 +32,48 @@ func init() {
32
33
// StaticResponse implements a simple responder for static responses.
34
type StaticResponse struct {
34
- StatusCode weakString `json:"status_code"`
35
- Headers http.Header `json:"headers"`
36
- Body string `json:"body"`
37
- Close bool `json:"close"`
35
+ StatusCode WeakString `json:"status_code,omitempty"`
36
+ Headers http.Header `json:"headers,omitempty"`
37
+ Body string `json:"body,omitempty"`
38
+ Close bool `json:"close,omitempty"`
39
}
40
41
+// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
42
+//
43
+// static_response [<matcher>] <status> {
44
+// body <text>
45
+// close
46
+// }
47
+//
48
+func (s *StaticResponse) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
49
+ for d.Next() {
50
+ var statusCodeStr string
51
+ if d.Args(&statusCodeStr) {
52
+ s.StatusCode = WeakString(statusCodeStr)
53
+ }
54
+ for d.NextBlock() {
55
+ switch d.Val() {
56
+ case "body":
57
+ if s.Body != "" {
58
+ return d.Err("body already specified")
59
+ }
60
+ if !d.Args(&s.Body) {
61
+ return d.ArgErr()
62
+ }
63
+ case "close":
64
+ if s.Close {
65
+ return d.Err("close already specified")
66
+ }
67
+ s.Close = true
68
+ }
69
+ }
70
+ }
71
+ return nil
72
+}
73
+
74
+// Bucket returns the HTTP Caddyfile handler bucket number.
75
+func (StaticResponse) Bucket() int { return 7 }
76
+
77
func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error {
78
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
79
modules/caddyhttp/staticresp_test.go
+1
-1
@@ -30,7 +30,7 @@ func TestStaticResponseHandler(t *testing.T) {
30
w := httptest.NewRecorder()
31
32
s := StaticResponse{
33
- StatusCode: weakString(strconv.Itoa(http.StatusNotFound)),
33
+ StatusCode: WeakString(strconv.Itoa(http.StatusNotFound)),
34
Headers: http.Header{
35
"X-Test": []string{"Testing"},
36
},
modules/caddyhttp/table.go
deleted
-55
@@ -1,55 +0,0 @@
1
-// Copyright 2015 Matthew Holt and The Caddy Authors
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
-
15
-package caddyhttp
16
-
17
-import (
18
- "net/http"
19
-
20
- "github.com/caddyserver/caddy/v2"
21
-)
22
-
23
-func init() {
24
- caddy.RegisterModule(caddy.Module{
25
- Name: "http.handlers.table",
26
- New: func() interface{} { return new(tableMiddleware) },
27
- })
28
-
29
- caddy.RegisterModule(caddy.Module{
30
- Name: "http.matchers.table",
31
- New: func() interface{} { return new(tableMatcher) },
32
- })
33
-}
34
-
35
-type tableMiddleware struct {
36
-}
37
-
38
-func (t tableMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error {
39
- // tbl := r.Context().Value(TableCtxKey).(map[string]interface{})
40
-
41
- // TODO: implement this...
42
-
43
- return nil
44
-}
45
-
46
-type tableMatcher struct {
47
-}
48
-
49
-func (m tableMatcher) Match(r *http.Request) bool {
50
- return false // TODO: implement
51
-}
52
-
53
-// Interface guards
54
-var _ MiddlewareHandler = (*tableMiddleware)(nil)
55
-var _ RequestMatcher = (*tableMatcher)(nil)
modules/caddyhttp/templates/caddyfile.go
new
+63
@@ -0,0 +1,63 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
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
+
15
+package templates
16
+
17
+import (
18
+ "github.com/caddyserver/caddy/caddyconfig/caddyfile"
19
+ "github.com/caddyserver/caddy/caddyconfig/httpcaddyfile"
20
+)
21
+
22
+// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
23
+//
24
+// templates [<matcher>] {
25
+// mime <types...>
26
+// between <open_delim> <close_delim>
27
+// root <path>
28
+// }
29
+//
30
+func (t *Templates) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
31
+ for d.Next() {
32
+ for d.NextBlock() {
33
+ switch d.Val() {
34
+ case "mime":
35
+ t.MIMETypes = d.RemainingArgs()
36
+ if len(t.MIMETypes) == 0 {
37
+ return d.ArgErr()
38
+ }
39
+ case "between":
40
+ t.Delimiters = d.RemainingArgs()
41
+ if len(t.Delimiters) != 2 {
42
+ return d.ArgErr()
43
+ }
44
+ case "root":
45
+ if !d.Args(&t.IncludeRoot) {
46
+ return d.ArgErr()
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ if t.IncludeRoot == "" {
53
+ t.IncludeRoot = "{http.var.root}"
54
+ }
55
+
56
+ return nil
57
+}
58
+
59
+// Bucket returns the HTTP Caddyfile handler bucket number.
60
+func (t Templates) Bucket() int { return 5 }
61
+
62
+// Interface guard
63
+var _ httpcaddyfile.HandlerDirective = (*Templates)(nil)
modules/caddyhttp/templates/templates.go
+2
-1
@@ -108,7 +108,8 @@ func (t *Templates) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
108
func (t *Templates) executeTemplate(rr caddyhttp.ResponseRecorder, r *http.Request) error {
109
var fs http.FileSystem
110
if t.IncludeRoot != "" {
111
- fs = http.Dir(t.IncludeRoot)
111
+ repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
112
+ fs = http.Dir(repl.ReplaceAll(t.IncludeRoot, "."))
113
}
114
115
ctx := &templateContext{
modules/caddyhttp/templates/tplcontext.go
-13
@@ -136,19 +136,6 @@ func (c templateContext) Cookie(name string) string {
136
return ""
137
}
138
139
-// Hostname gets the (remote) hostname of the client making the request.
140
-// Performance warning: This involves a DNS lookup.
141
-func (c templateContext) Hostname() string {
142
- ip := c.RemoteIP()
143
-
144
- hostnameList, err := net.LookupAddr(ip)
145
- if err != nil || len(hostnameList) == 0 {
146
- return c.Req.RemoteAddr
147
- }
148
-
149
- return hostnameList[0]
150
-}
151
-
139
// RemoteIP gets the IP address of the client making the request.
140
func (c templateContext) RemoteIP() string {
141
ip, _, err := net.SplitHostPort(c.Req.RemoteAddr)
modules/caddyhttp/vars.go
new
+71
@@ -0,0 +1,71 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
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
+
15
+package caddyhttp
16
+
17
+import (
18
+ "net/http"
19
+
20
+ "github.com/caddyserver/caddy/v2"
21
+)
22
+
23
+func init() {
24
+ caddy.RegisterModule(caddy.Module{
25
+ Name: "http.handlers.vars",
26
+ New: func() interface{} { return new(VarsMiddleware) },
27
+ })
28
+ caddy.RegisterModule(caddy.Module{
29
+ Name: "http.matchers.vars",
30
+ New: func() interface{} { return new(VarsMiddleware) },
31
+ })
32
+}
33
+
34
+// VarsMiddleware is an HTTP middleware which sets variables
35
+// in the context, mainly for use by placeholders.
36
+type VarsMiddleware map[string]string
37
+
38
+func (t VarsMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error {
39
+ vars := r.Context().Value(VarCtxKey).(map[string]interface{})
40
+ repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
41
+ for k, v := range t {
42
+ keyExpanded := repl.ReplaceAll(k, "")
43
+ valExpanded := repl.ReplaceAll(v, "")
44
+ vars[keyExpanded] = valExpanded
45
+ }
46
+ return next.ServeHTTP(w, r)
47
+}
48
+
49
+// VarsMatcher is an HTTP request matcher which can match
50
+// requests based on variables in the context.
51
+type VarsMatcher map[string]string
52
+
53
+// Match matches a request based on variables in the context.
54
+func (m VarsMatcher) Match(r *http.Request) bool {
55
+ vars := r.Context().Value(VarCtxKey).(map[string]string)
56
+ repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
57
+ for k, v := range m {
58
+ keyExpanded := repl.ReplaceAll(k, "")
59
+ valExpanded := repl.ReplaceAll(v, "")
60
+ if vars[keyExpanded] != valExpanded {
61
+ return false
62
+ }
63
+ }
64
+ return true
65
+}
66
+
67
+// Interface guards
68
+var (
69
+ _ MiddlewareHandler = (*VarsMiddleware)(nil)
70
+ _ RequestMatcher = (*VarsMatcher)(nil)
71
+)
modules/caddytls/connpolicy.go
+4
-4
@@ -172,7 +172,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
172
// add all the cipher suites in order, without duplicates
173
cipherSuitesAdded := make(map[uint16]struct{})
174
for _, csName := range p.CipherSuites {
175
- csID := supportedCipherSuites[csName]
175
+ csID := SupportedCipherSuites[csName]
176
if _, ok := cipherSuitesAdded[csID]; !ok {
177
cipherSuitesAdded[csID] = struct{}{}
178
cfg.CipherSuites = append(cfg.CipherSuites, csID)
@@ -182,7 +182,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
182
// add all the curve preferences in order, without duplicates
183
curvesAdded := make(map[tls.CurveID]struct{})
184
for _, curveName := range p.Curves {
185
- curveID := supportedCurves[curveName]
185
+ curveID := SupportedCurves[curveName]
186
if _, ok := curvesAdded[curveID]; !ok {
187
curvesAdded[curveID] = struct{}{}
188
cfg.CurvePreferences = append(cfg.CurvePreferences, curveID)
@@ -203,10 +203,10 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
203
204
// min and max protocol versions
205
if p.ProtocolMin != "" {
206
- cfg.MinVersion = supportedProtocols[p.ProtocolMin]
206
+ cfg.MinVersion = SupportedProtocols[p.ProtocolMin]
207
}
208
if p.ProtocolMax != "" {
209
- cfg.MaxVersion = supportedProtocols[p.ProtocolMax]
209
+ cfg.MaxVersion = SupportedProtocols[p.ProtocolMax]
210
}
211
if p.ProtocolMin > p.ProtocolMax {
212
return fmt.Errorf("protocol min (%x) cannot be greater than protocol max (%x)", p.ProtocolMin, p.ProtocolMax)
modules/caddytls/fileloader.go
+5
-5
@@ -25,12 +25,12 @@ import (
25
func init() {
26
caddy.RegisterModule(caddy.Module{
27
Name: "tls.certificates.load_files",
28
- New: func() interface{} { return fileLoader{} },
28
+ New: func() interface{} { return FileLoader{} },
29
})
30
}
31
32
-// fileLoader loads certificates and their associated keys from disk.
33
-type fileLoader []CertKeyFilePair
32
+// FileLoader loads certificates and their associated keys from disk.
33
+type FileLoader []CertKeyFilePair
34
35
// CertKeyFilePair pairs certificate and key file names along with their
36
// encoding format so that they can be loaded from disk.
@@ -42,7 +42,7 @@ type CertKeyFilePair struct {
42
}
43
44
// LoadCertificates returns the certificates to be loaded by fl.
45
-func (fl fileLoader) LoadCertificates() ([]Certificate, error) {
45
+func (fl FileLoader) LoadCertificates() ([]Certificate, error) {
46
var certs []Certificate
47
for _, pair := range fl {
48
certData, err := ioutil.ReadFile(pair.Certificate)
@@ -73,4 +73,4 @@ func (fl fileLoader) LoadCertificates() ([]Certificate, error) {
73
}
74
75
// Interface guard
76
-var _ CertificateLoader = (fileLoader)(nil)
76
+var _ CertificateLoader = (FileLoader)(nil)
modules/caddytls/folderloader.go
+5
-5
@@ -30,20 +30,20 @@ import (
30
func init() {
31
caddy.RegisterModule(caddy.Module{
32
Name: "tls.certificates.load_folders",
33
- New: func() interface{} { return folderLoader{} },
33
+ New: func() interface{} { return FolderLoader{} },
34
})
35
}
36
37
-// folderLoader loads certificates and their associated keys from disk
37
+// FolderLoader loads certificates and their associated keys from disk
38
// by recursively walking the specified directories, looking for PEM
39
// files which contain both a certificate and a key.
40
-type folderLoader []string
40
+type FolderLoader []string
41
42
// LoadCertificates loads all the certificates+keys in the directories
43
// listed in fl from all files ending with .pem. This method of loading
44
// certificates expects the certificate and key to be bundled into the
45
// same file.
46
-func (fl folderLoader) LoadCertificates() ([]Certificate, error) {
46
+func (fl FolderLoader) LoadCertificates() ([]Certificate, error) {
47
var certs []Certificate
48
for _, dir := range fl {
49
err := filepath.Walk(dir, func(fpath string, info os.FileInfo, err error) error {
@@ -135,4 +135,4 @@ func x509CertFromCertAndKeyPEMFile(fpath string) (tls.Certificate, error) {
135
return cert, nil
136
}
137
138
-var _ CertificateLoader = (folderLoader)(nil)
138
+var _ CertificateLoader = (FolderLoader)(nil)
modules/caddytls/sessiontickets.go
+1
-1
@@ -29,7 +29,7 @@ import (
29
// SessionTicketService configures and manages TLS session tickets.
30
type SessionTicketService struct {
31
KeySource json.RawMessage `json:"key_source,omitempty"`
32
- RotationInterval caddy.Duration `json:"rotation_interval,omitempty"`
32
+ RotationInterval caddy.Duration `json:"rotation_interval,omitempty"`
33
MaxKeys int `json:"max_keys,omitempty"`
34
DisableRotation bool `json:"disable_rotation,omitempty"`
35
Disabled bool `json:"disabled,omitempty"`
modules/caddytls/tls.go
+17
-10
@@ -45,8 +45,8 @@ func init() {
45
// TLS represents a process-wide TLS configuration.
46
type TLS struct {
47
Certificates map[string]json.RawMessage `json:"certificates,omitempty"`
48
- Automation AutomationConfig `json:"automation,omitempty"`
49
- SessionTickets SessionTicketService `json:"session_tickets,omitempty"`
48
+ Automation AutomationConfig `json:"automation"`
49
+ SessionTickets SessionTicketService `json:"session_tickets"`
50
51
certificateLoaders []CertificateLoader
52
certCache *certmagic.Cache
@@ -105,16 +105,12 @@ func (t *TLS) Provision(ctx caddy.Context) error {
105
onDemandRateLimiter.SetLimit(0)
106
}
107
108
- return nil
109
-}
110
-
111
-// Start activates the TLS module.
112
-func (t *TLS) Start() error {
108
+ // load manual/static (unmanaged) certificates - we do this in
109
+ // provision so that other apps (such as http) can know which
110
+ // certificates have been manually loaded
111
magic := certmagic.New(t.certCache, certmagic.Config{
114
- Storage: t.ctx.Storage(),
112
+ Storage: ctx.Storage(),
113
})
116
-
117
- // load manual/static (unmanaged) certificates
114
for _, loader := range t.certificateLoaders {
115
certs, err := loader.LoadCertificates()
116
if err != nil {
@@ -128,6 +124,11 @@ func (t *TLS) Start() error {
124
}
125
}
126
127
+ return nil
128
+}
129
+
130
+// Start activates the TLS module.
131
+func (t *TLS) Start() error {
132
// load automated (managed) certificates
133
if automatedRawMsg, ok := t.Certificates[automateKey]; ok {
134
var names []string
@@ -204,6 +205,12 @@ func (t *TLS) getAutomationPolicyForName(name string) AutomationPolicy {
205
return AutomationPolicy{Management: mgmt}
206
}
207
208
+// CertificatesWithSAN returns the list of all certificates
209
+// in the cache the match the given SAN value.
210
+func (t *TLS) CertificatesWithSAN(san string) []certmagic.Certificate {
211
+ return t.certCache.CertificatesWithSAN(san)
212
+}
213
+
214
// CertificateLoader is a type that can load certificates.
215
// Certificates can optionally be associated with tags.
216
type CertificateLoader interface {
modules/caddytls/values.go
+21
-15
@@ -22,12 +22,16 @@ import (
22
"github.com/klauspost/cpuid"
23
)
24
25
-// supportedCipherSuites is the unordered map of cipher suite
25
+// SupportedCipherSuites is the unordered map of cipher suite
26
// string names to their definition in crypto/tls. All values
27
// should be IANA-reserved names. See
28
// https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml
29
+// Two of the cipher suite constants in the standard lib do not use the
30
+// full IANA name, but we do; see:
31
+// https://github.com/golang/go/issues/32061 and
32
+// https://github.com/golang/go/issues/30325#issuecomment-512862374.
33
// TODO: might not be needed much longer: https://github.com/golang/go/issues/30325
30
-var supportedCipherSuites = map[string]uint16{
34
+var SupportedCipherSuites = map[string]uint16{
35
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
36
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
37
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
@@ -84,22 +88,24 @@ func getOptimalDefaultCipherSuites() []uint16 {
88
return defaultCipherSuitesWithoutAESNI
89
}
90
87
-// supportedCurves is the unordered map of supported curves.
91
+// SupportedCurves is the unordered map of supported curves.
92
// https://golang.org/pkg/crypto/tls/#CurveID
89
-var supportedCurves = map[string]tls.CurveID{
90
- "X25519": tls.X25519,
91
- "P256": tls.CurveP256,
92
- "P384": tls.CurveP384,
93
- "P521": tls.CurveP521,
93
+var SupportedCurves = map[string]tls.CurveID{
94
+ // TODO: Use IANA names, probably? see https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8
95
+ // All named crypto/elliptic curves have secpXXXr1 IANA names.
96
+ "x25519": tls.X25519, // x25519, 29
97
+ "p256": tls.CurveP256, // secp256r1, 23
98
+ "p384": tls.CurveP384, // secp384r1, 24
99
+ "p521": tls.CurveP521, // secp521r1, 25
100
}
101
102
// supportedCertKeyTypes is all the key types that are supported
103
// for certificates that are obtained through ACME.
104
var supportedCertKeyTypes = map[string]certcrypto.KeyType{
99
- "RSA2048": certcrypto.RSA2048,
100
- "RSA4096": certcrypto.RSA4096,
101
- "P256": certcrypto.EC256,
102
- "P384": certcrypto.EC384,
105
+ "rsa_2048": certcrypto.RSA2048,
106
+ "rsa_4096": certcrypto.RSA4096,
107
+ "ec_p256": certcrypto.EC256,
108
+ "ec_p384": certcrypto.EC384,
109
}
110
111
// defaultCurves is the list of only the curves we want to use
@@ -115,9 +121,9 @@ var defaultCurves = []tls.CurveID{
121
tls.CurveP256,
122
}
123
118
-// supportedProtocols is a map of supported protocols.
119
-// HTTP/2 only supports TLS 1.2 and higher.
120
-var supportedProtocols = map[string]uint16{
124
+// SupportedProtocols is a map of supported protocols.
125
+// Note that HTTP/2 only supports TLS 1.2 and higher.
126
+var SupportedProtocols = map[string]uint16{
127
"tls1.0": tls.VersionTLS10,
128
"tls1.1": tls.VersionTLS11,
129
"tls1.2": tls.VersionTLS12,