diff options
69 files changed, 5501 insertions, 435 deletions
@@ -95,9 +95,11 @@ func StartAdmin(initialConfigJSON []byte) error { mux.HandleFunc("/debug/pprof/trace", pprof.Trace) ///// END PPROF STUFF ////// - for _, m := range GetModules("admin") { - route := m.New().(AdminRoute) - mux.Handle(route.Pattern, route) + for _, m := range GetModules("admin.routers") { + adminrtr := m.New().(AdminRouter) + for _, route := range adminrtr.Routes() { + mux.Handle(route.Pattern, route) + } } handler := cors.Default().Handler(mux) @@ -144,6 +146,11 @@ func StopAdmin() error { return nil } +// AdminRouter is a type which can return routes for the admin API. +type AdminRouter interface { + Routes() []AdminRoute +} + // AdminRoute represents a route for the admin endpoint. type AdminRoute struct { http.Handler diff --git a/caddyconfig/caddyfile/adapter.go b/caddyconfig/caddyfile/adapter.go new file mode 100644 index 0000000..377f77b --- /dev/null +++ b/caddyconfig/caddyfile/adapter.go @@ -0,0 +1,87 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyfile + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" +) + +// Adapter adapts Caddyfile to Caddy JSON. +type Adapter struct { + ServerType ServerType +} + +// Adapt converts the Caddyfile config in body to Caddy JSON. +func (a Adapter) Adapt(body []byte, options map[string]string) ([]byte, []caddyconfig.Warning, error) { + if a.ServerType == nil { + return nil, nil, fmt.Errorf("no server type") + } + if options == nil { + options = make(map[string]string) + } + + filename := options["filename"] + if filename == "" { + filename = "Caddyfile" + } + + serverBlocks, err := Parse(filename, bytes.NewReader(body)) + if err != nil { + return nil, nil, err + } + + cfg, warnings, err := a.ServerType.Setup(serverBlocks, options) + if err != nil { + return nil, warnings, err + } + + marshalFunc := json.Marshal + if options["pretty"] == "true" { + marshalFunc = caddyconfig.JSONIndent + } + result, err := marshalFunc(cfg) + + return result, warnings, err +} + +// Unmarshaler is a type that can unmarshal +// Caddyfile tokens to set itself up for a +// JSON encoding. The goal of an unmarshaler +// is not to set itself up for actual use, +// but to set itself up for being marshaled +// into JSON. Caddyfile-unmarshaled values +// will not be used directly; they will be +// encoded as JSON and then used from that. +type Unmarshaler interface { + UnmarshalCaddyfile(d *Dispenser) error +} + +// ServerType is a type that can evaluate a Caddyfile and set up a caddy config. +type ServerType interface { + // Setup takes the server blocks which + // contain tokens, as well as options + // (e.g. CLI flags) and creates a Caddy + // config, along with any warnings or + // an error. + Setup([]ServerBlock, map[string]string) (*caddy.Config, []caddyconfig.Warning, error) +} + +// Interface guard +var _ caddyconfig.Adapter = (*Adapter)(nil) diff --git a/caddyconfig/caddyfile/dispenser.go b/caddyconfig/caddyfile/dispenser.go new file mode 100755 index 0000000..0d2c789 --- /dev/null +++ b/caddyconfig/caddyfile/dispenser.go @@ -0,0 +1,341 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyfile + +import ( + "errors" + "fmt" + "strings" +) + +// Dispenser is a type that dispenses tokens, similarly to a lexer, +// except that it can do so with some notion of structure. An empty +// Dispenser is invalid; call NewDispenser to make a proper instance. +type Dispenser struct { + filename string + tokens []Token + cursor int + nesting int +} + +// NewDispenser returns a Dispenser filled with the given tokens. +// TODO: Get rid of the filename argument; it seems pointless here +func NewDispenser(filename string, tokens []Token) *Dispenser { + return &Dispenser{ + filename: filename, + tokens: tokens, + cursor: -1, + } +} + +// Next loads the next token. Returns true if a token +// was loaded; false otherwise. If false, all tokens +// have been consumed. +func (d *Dispenser) Next() bool { + if d.cursor < len(d.tokens)-1 { + d.cursor++ + return true + } + return false +} + +// Prev moves to the previous token. It does the inverse +// of Next(), except this function may decrement the cursor +// to -1 so that the next call to Next() points to the +// first token; this allows dispensing to "start over". This +// method returns true if the cursor ends up pointing to a +// valid token. +func (d *Dispenser) Prev() bool { + if d.cursor > -1 { + d.cursor-- + return d.cursor > -1 + } + return false +} + +// NextArg loads the next token if it is on the same +// line and if it is not a block opening (open curly +// brace). Returns true if an argument token was +// loaded; false otherwise. If false, all tokens on +// the line have been consumed except for potentially +// a block opening. It handles imported tokens +// correctly. +func (d *Dispenser) NextArg() bool { + if !d.nextOnSameLine() { + return false + } + if d.Val() == "{" { + // roll back; a block opening is not an argument + d.cursor-- + return false + } + return true +} + +// nextOnSameLine advances the cursor if the next +// token is on the same line of the same file. +func (d *Dispenser) nextOnSameLine() bool { + if d.cursor < 0 { + d.cursor++ + return true + } + if d.cursor >= len(d.tokens) { + return false + } + if d.cursor < len(d.tokens)-1 && + d.tokens[d.cursor].File == d.tokens[d.cursor+1].File && + d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line { + d.cursor++ + return true + } + return false +} + +// NextLine loads the next token only if it is not on the same +// line as the current token, and returns true if a token was +// loaded; false otherwise. If false, there is not another token +// or it is on the same line. It handles imported tokens correctly. +func (d *Dispenser) NextLine() bool { + if d.cursor < 0 { + d.cursor++ + return true + } + if d.cursor >= len(d.tokens) { + return false + } + if d.cursor < len(d.tokens)-1 && + (d.tokens[d.cursor].File != d.tokens[d.cursor+1].File || + d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) { + d.cursor++ + return true + } + return false +} + +// NextBlock can be used as the condition of a for loop +// to load the next token as long as it opens a block or +// is already in a block. It returns true if a token was +// loaded, or false when the block's closing curly brace +// was loaded and thus the block ended. Nested blocks are +// not supported. +func (d *Dispenser) NextBlock() bool { + if d.nesting > 0 { + d.Next() + if d.Val() == "}" { + d.nesting-- + return false + } + return true + } + if !d.nextOnSameLine() { // block must open on same line + return false + } + if d.Val() != "{" { + d.cursor-- // roll back if not opening brace + return false + } + d.Next() + if d.Val() == "}" { + // open and then closed right away + return false + } + d.nesting++ + return true +} + +// Nested returns true if the token is currently nested +// inside a block (i.e. an open curly brace was consumed). +func (d *Dispenser) Nested() bool { + return d.nesting > 0 +} + +// Val gets the text of the current token. If there is no token +// loaded, it returns empty string. +func (d *Dispenser) Val() string { + if d.cursor < 0 || d.cursor >= len(d.tokens) { + return "" + } + return d.tokens[d.cursor].Text +} + +// Line gets the line number of the current token. If there is no token +// loaded, it returns 0. +func (d *Dispenser) Line() int { + if d.cursor < 0 || d.cursor >= len(d.tokens) { + return 0 + } + return d.tokens[d.cursor].Line +} + +// File gets the filename of the current token. If there is no token loaded, +// it returns the filename originally given when parsing started. +func (d *Dispenser) File() string { + if d.cursor < 0 || d.cursor >= len(d.tokens) { + return d.filename + } + if tokenFilename := d.tokens[d.cursor].File; tokenFilename != "" { + return tokenFilename + } + return d.filename +} + +// Args is a convenience function that loads the next arguments +// (tokens on the same line) into an arbitrary number of strings +// pointed to in targets. If there are fewer tokens available +// than string pointers, the remaining strings will not be changed +// and false will be returned. If there were enough tokens available +// to fill the arguments, then true will be returned. +func (d *Dispenser) Args(targets ...*string) bool { + for i := 0; i < len(targets); i++ { + if !d.NextArg() { + return false + } + *targets[i] = d.Val() + } + return true +} + +// RemainingArgs loads any more arguments (tokens on the same line) +// into a slice and returns them. Open curly brace tokens also indicate +// the end of arguments, and the curly brace is not included in +// the return value nor is it loaded. +func (d *Dispenser) RemainingArgs() []string { + var args []string + for d.NextArg() { + args = append(args, d.Val()) + } + return args +} + +// NewFromNextTokens returns a new dispenser with a copy of +// the tokens from the current token until the end of the +// "directive" whether that be to the end of the line or +// the end of a block that starts at the end of the line. +func (d *Dispenser) NewFromNextTokens() *Dispenser { + tkns := []Token{d.Token()} + for d.NextArg() { + tkns = append(tkns, d.Token()) + } + if d.Next() && d.Val() == "{" { + tkns = append(tkns, d.Token()) + for d.NextBlock() { + for d.Nested() { + tkns = append(tkns, d.Token()) + d.NextBlock() + } + } + tkns = append(tkns, d.Token()) + } else { + d.cursor-- + } + return NewDispenser(d.filename, tkns) +} + +// Token returns the current token. +func (d *Dispenser) Token() Token { + return d.TokenAt(d.cursor) +} + +func (d *Dispenser) TokenAt(cursor int) Token { + if cursor < 0 || cursor >= len(d.tokens) { + return Token{} + } + return d.tokens[cursor] +} + +// Cursor returns the current cursor (token index). +func (d *Dispenser) Cursor() int { + return d.cursor +} + +func (d *Dispenser) Reset() { + d.cursor = -1 +} + +// ArgErr returns an argument error, meaning that another +// argument was expected but not found. In other words, +// a line break or open curly brace was encountered instead of +// an argument. +func (d *Dispenser) ArgErr() error { + if d.Val() == "{" { + return d.Err("Unexpected token '{', expecting argument") + } + return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val()) +} + +// SyntaxErr creates a generic syntax error which explains what was +// found and what was expected. +func (d *Dispenser) SyntaxErr(expected string) error { + msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected) + return errors.New(msg) +} + +// EOFErr returns an error indicating that the dispenser reached +// the end of the input when searching for the next token. +func (d *Dispenser) EOFErr() error { + return d.Errf("Unexpected EOF") +} + +// Err generates a custom parse-time error with a message of msg. +func (d *Dispenser) Err(msg string) error { + msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg) + return errors.New(msg) +} + +// Errf is like Err, but for formatted error messages +func (d *Dispenser) Errf(format string, args ...interface{}) error { + return d.Err(fmt.Sprintf(format, args...)) +} + +// Delete deletes the current token and returns the updated slice +// of tokens. The cursor is not advanced to the next token. +// Because deletion modifies the underlying slice, this method +// should only be called if you have access to the original slice +// of tokens and/or are using the slice of tokens outside this +// Dispenser instance. If you do not re-assign the slice with the +// return value of this method, inconsistencies in the token +// array will become apparent (or worse, hide from you like they +// did me for 3 and a half freaking hours late one night). +func (d *Dispenser) Delete() []Token { + if d.cursor >= 0 && d.cursor < len(d.tokens)-1 { + d.tokens = append(d.tokens[:d.cursor], d.tokens[d.cursor+1:]...) + d.cursor-- + } + return d.tokens +} + +// numLineBreaks counts how many line breaks are in the token +// value given by the token index tknIdx. It returns 0 if the +// token does not exist or there are no line breaks. +func (d *Dispenser) numLineBreaks(tknIdx int) int { + if tknIdx < 0 || tknIdx >= len(d.tokens) { + return 0 + } + return strings.Count(d.tokens[tknIdx].Text, "\n") +} + +// isNewLine determines whether the current token is on a different +// line (higher line number) than the previous token. It handles imported +// tokens correctly. If there isn't a previous token, it returns true. +func (d *Dispenser) isNewLine() bool { + if d.cursor < 1 { + return true + } + if d.cursor > len(d.tokens)-1 { + return false + } + return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File || + d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line +} diff --git a/caddyconfig/caddyfile/dispenser_test.go b/caddyconfig/caddyfile/dispenser_test.go new file mode 100755 index 0000000..9860bed --- /dev/null +++ b/caddyconfig/caddyfile/dispenser_test.go @@ -0,0 +1,316 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyfile + +import ( + "io" + "log" + "reflect" + "strings" + "testing" +) + +func TestDispenser_Val_Next(t *testing.T) { + input := `host:port + dir1 arg1 + dir2 arg2 arg3 + dir3` + d := newTestDispenser(input) + + if val := d.Val(); val != "" { + t.Fatalf("Val(): Should return empty string when no token loaded; got '%s'", val) + } + + assertNext := func(shouldLoad bool, expectedCursor int, expectedVal string) { + if loaded := d.Next(); loaded != shouldLoad { + t.Errorf("Next(): Expected %v but got %v instead (val '%s')", shouldLoad, loaded, d.Val()) + } + if d.cursor != expectedCursor { + t.Errorf("Expected cursor to be %d, but was %d", expectedCursor, d.cursor) + } + if d.nesting != 0 { + t.Errorf("Nesting should be 0, was %d instead", d.nesting) + } + if val := d.Val(); val != expectedVal { + t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val) + } + } + + assertNext(true, 0, "host:port") + assertNext(true, 1, "dir1") + assertNext(true, 2, "arg1") + assertNext(true, 3, "dir2") + assertNext(true, 4, "arg2") + assertNext(true, 5, "arg3") + assertNext(true, 6, "dir3") + // Note: This next test simply asserts existing behavior. + // If desired, we may wish to empty the token value after + // reading past the EOF. Open an issue if you want this change. + assertNext(false, 6, "dir3") +} + +func TestDispenser_NextArg(t *testing.T) { + input := `dir1 arg1 + dir2 arg2 arg3 + dir3` + d := newTestDispenser(input) + + assertNext := func(shouldLoad bool, expectedVal string, expectedCursor int) { + if d.Next() != shouldLoad { + t.Errorf("Next(): Should load token but got false instead (val: '%s')", d.Val()) + } + if d.cursor != expectedCursor { + t.Errorf("Next(): Expected cursor to be at %d, but it was %d", expectedCursor, d.cursor) + } + if val := d.Val(); val != expectedVal { + t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val) + } + } + + assertNextArg := func(expectedVal string, loadAnother bool, expectedCursor int) { + if !d.NextArg() { + t.Error("NextArg(): Should load next argument but got false instead") + } + if d.cursor != expectedCursor { + t.Errorf("NextArg(): Expected cursor to be at %d, but it was %d", expectedCursor, d.cursor) + } + if val := d.Val(); val != expectedVal { + t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val) + } + if !loadAnother { + if d.NextArg() { + t.Fatalf("NextArg(): Should NOT load another argument, but got true instead (val: '%s')", d.Val()) + } + if d.cursor != expectedCursor { + t.Errorf("NextArg(): Expected cursor to remain at %d, but it was %d", expectedCursor, d.cursor) + } + } + } + + assertNext(true, "dir1", 0) + assertNextArg("arg1", false, 1) + assertNext(true, "dir2", 2) + assertNextArg("arg2", true, 3) + assertNextArg("arg3", false, 4) + assertNext(true, "dir3", 5) + assertNext(false, "dir3", 5) +} + +func TestDispenser_NextLine(t *testing.T) { + input := `host:port + dir1 arg1 + dir2 arg2 arg3` + d := newTestDispenser(input) + + assertNextLine := func(shouldLoad bool, expectedVal string, expectedCursor int) { + if d.NextLine() != shouldLoad { + t.Errorf("NextLine(): Should load token but got false instead (val: '%s')", d.Val()) + } + if d.cursor != expectedCursor { + t.Errorf("NextLine(): Expected cursor to be %d, instead was %d", expectedCursor, d.cursor) + } + if val := d.Val(); val != expectedVal { + t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val) + } + } + + assertNextLine(true, "host:port", 0) + assertNextLine(true, "dir1", 1) + assertNextLine(false, "dir1", 1) + d.Next() // arg1 + assertNextLine(true, "dir2", 3) + assertNextLine(false, "dir2", 3) + d.Next() // arg2 + assertNextLine(false, "arg2", 4) + d.Next() // arg3 + assertNextLine(false, "arg3", 5) +} + +func TestDispenser_NextBlock(t *testing.T) { + input := `foobar1 { + sub1 arg1 + sub2 + } + foobar2 { + }` + d := newTestDispenser(input) + + assertNextBlock := func(shouldLoad bool, expectedCursor, expectedNesting int) { + if loaded := d.NextBlock(); loaded != shouldLoad { + t.Errorf("NextBlock(): Should return %v but got %v", shouldLoad, loaded) + } + if d.cursor != expectedCursor { + t.Errorf("NextBlock(): Expected cursor to be %d, was %d", expectedCursor, d.cursor) + } + if d.nesting != expectedNesting { + t.Errorf("NextBlock(): Nesting should be %d, not %d", expectedNesting, d.nesting) + } + } + + assertNextBlock(false, -1, 0) + d.Next() // foobar1 + assertNextBlock(true, 2, 1) + assertNextBlock(true, 3, 1) + assertNextBlock(true, 4, 1) + assertNextBlock(false, 5, 0) + d.Next() // foobar2 + assertNextBlock(false, 8, 0) // empty block is as if it didn't exist +} + +func TestDispenser_Args(t *testing.T) { + var s1, s2, s3 string + input := `dir1 arg1 arg2 arg3 + dir2 arg4 arg5 + dir3 arg6 arg7 + dir4` + d := newTestDispenser(input) + + d.Next() // dir1 + + // As many strings as arguments + if all := d.Args(&s1, &s2, &s3); !all { + t.Error("Args(): Expected true, got false") + } + if s1 != "arg1" { + t.Errorf("Args(): Expected s1 to be 'arg1', got '%s'", s1) + } + if s2 != "arg2" { + t.Errorf("Args(): Expected s2 to be 'arg2', got '%s'", s2) + } + if s3 != "arg3" { + t.Errorf("Args(): Expected s3 to be 'arg3', got '%s'", s3) + } + + d.Next() // dir2 + + // More strings than arguments + if all := d.Args(&s1, &s2, &s3); all { + t.Error("Args(): Expected false, got true") + } + if s1 != "arg4" { + t.Errorf("Args(): Expected s1 to be 'arg4', got '%s'", s1) + } + if s2 != "arg5" { + t.Errorf("Args(): Expected s2 to be 'arg5', got '%s'", s2) + } + if s3 != "arg3" { + t.Errorf("Args(): Expected s3 to be unchanged ('arg3'), instead got '%s'", s3) + } + + // (quick cursor check just for kicks and giggles) + if d.cursor != 6 { + t.Errorf("Cursor should be 6, but is %d", d.cursor) + } + + d.Next() // dir3 + + // More arguments than strings + if all := d.Args(&s1); !all { + t.Error("Args(): Expected true, got false") + } + if s1 != "arg6" { + t.Errorf("Args(): Expected s1 to be 'arg6', got '%s'", s1) + } + + d.Next() // dir4 + + // No arguments or strings + if all := d.Args(); !all { + t.Error("Args(): Expected true, got false") + } + + // No arguments but at least one string + if all := d.Args(&s1); all { + t.Error("Args(): Expected false, got true") + } +} + +func TestDispenser_RemainingArgs(t *testing.T) { + input := `dir1 arg1 arg2 arg3 + dir2 arg4 arg5 + dir3 arg6 { arg7 + dir4` + d := newTestDispenser(input) + + d.Next() // dir1 + + args := d.RemainingArgs() + if expected := []string{"arg1", "arg2", "arg3"}; !reflect.DeepEqual(args, expected) { + t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args) + } + + d.Next() // dir2 + + args = d.RemainingArgs() + if expected := []string{"arg4", "arg5"}; !reflect.DeepEqual(args, expected) { + t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args) + } + + d.Next() // dir3 + + args = d.RemainingArgs() + if expected := []string{"arg6"}; !reflect.DeepEqual(args, expected) { + t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args) + } + + d.Next() // { + d.Next() // arg7 + d.Next() // dir4 + + args = d.RemainingArgs() + if len(args) != 0 { + t.Errorf("RemainingArgs(): Expected %v, got %v", []string{}, args) + } +} + +func TestDispenser_ArgErr_Err(t *testing.T) { + input := `dir1 { + } + dir2 arg1 arg2` + d := newTestDispenser(input) + + d.cursor = 1 // { + + if err := d.ArgErr(); err == nil || !strings.Contains(err.Error(), "{") { + t.Errorf("ArgErr(): Expected an error message with { in it, but got '%v'", err) + } + + d.cursor = 5 // arg2 + + if err := d.ArgErr(); err == nil || !strings.Contains(err.Error(), "arg2") { + t.Errorf("ArgErr(): Expected an error message with 'arg2' in it; got '%v'", err) + } + + err := d.Err("foobar") + if err == nil { + t.Fatalf("Err(): Expected an error, got nil") + } + + if !strings.Contains(err.Error(), "Testfile:3") { + t.Errorf("Expected error message with filename:line in it; got '%v'", err) + } + + if !strings.Contains(err.Error(), "foobar") { + t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err) + } +} + +func newTestDispenser(input string) *Dispenser { + tokens, err := allTokens(strings.NewReader(input)) + if err != nil && err != io.EOF { + log.Fatalf("getting all tokens from input: %v", err) + } + return NewDispenser("Testfile", tokens) +} diff --git a/caddyconfig/caddyfile/lexer.go b/caddyconfig/caddyfile/lexer.go new file mode 100755 index 0000000..efe648d --- /dev/null +++ b/caddyconfig/caddyfile/lexer.go @@ -0,0 +1,150 @@ +// Copyright 2015 Light Code Labs, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyfile + +import ( + "bufio" + "io" + "unicode" +) + +type ( + // lexer is a utility which can get values, token by + // token, from a Reader. A token is a word, and tokens + // are separated by whitespace. A word can be enclosed + // in quotes if it contains whitespace. + lexer struct { + reader *bufio.Reader + token Token + line int + } + + // Token represents a single parsable unit. + Token struct { + File string + Line int + Text string + } +) + +// load prepares the lexer to scan an input for tokens. +// It discards any leading byte order mark. +func (l *lexer) load(input io.Reader) error { + l.reader = bufio.NewReader(input) + l.line = 1 + + // discard byte order mark, if present + firstCh, _, err := l.reader.ReadRune() + if err != nil { + return err + } + if firstCh != 0xFEFF { + err := l.reader.UnreadRune() + if err != nil { + return err + } + } + + return nil +} + +// next loads the next token into the lexer. +// A token is delimited by whitespace, unless +// the token starts with a quotes character (") +// in which case the token goes until the closing +// quotes (the enclosing quotes are not included). +// Inside quoted strings, quotes may be escaped +// with a preceding \ character. No other chars +// may be escaped. The rest of the line is skipped +// if a "#" character is read in. Returns true if +// a token was loaded; false otherwise. +func (l *lexer) next() bool { + var val []rune + var comment, quoted, escaped bool + + makeToken := func() bool { + l.token.Text = string(val) + return true + } + + for { + ch, _, err := l.reader.ReadRune() + if err != nil { + if len(val) > 0 { + return makeToken() + } + if err == io.EOF { + return false + } + panic(err) + } + + if quoted { + if !escaped { + if ch == '\\' { + escaped = true + continue + } else if ch == '"' { + quoted = false + return makeToken() + } + } + if ch == '\n' { + l.line++ + } + if escaped { + // only escape quotes and newlines + if ch != '"' && ch != '\n' { + val = append(val, '\\') + } + } + val = append(val, ch) + escaped = false + continue + } + + if unicode.IsSpace(ch) { + if ch == '\r' { + continue + } + if ch == '\n' { + l.line++ + comment = false + } + if len(val) > 0 { + return makeToken() + } + continue + } + + if ch == '#' { + comment = true + } + + if comment { + continue + } + + if len(val) == 0 { + l.token = Token{Line: l.line} + if ch == '"' { + quoted = true + continue + } + } + + val = append(val, ch) + } +} diff --git a/caddyconfig/caddyfile/lexer_test.go b/caddyconfig/caddyfile/lexer_test.go new file mode 100755 index 0000000..f9a843c --- /dev/null +++ b/caddyconfig/caddyfile/lexer_test.go @@ -0,0 +1,196 @@ +// Copyright 2015 Light Code Labs, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyfile + +import ( + "log" + "strings" + "testing" +) + +type lexerTestCase struct { + input string + expected []Token +} + +func TestLexer(t *testing.T) { + testCases := []lexerTestCase{ + { + input: `host:123`, + expected: []Token{ + {Line: 1, Text: "host:123"}, + }, + }, + { + input: `host:123 + + directive`, + expected: []Token{ + {Line: 1, Text: "host:123"}, + {Line: 3, Text: "directive"}, + }, + }, + { + input: `host:123 { + directive + }`, + expected: []Token{ + {Line: 1, Text: "host:123"}, + {Line: 1, Text: "{"}, + {Line: 2, Text: "directive"}, + {Line: 3, Text: "}"}, + }, + }, + { + input: `host:123 { directive }`, + expected: []Token{ + {Line: 1, Text: "host:123"}, + {Line: 1, Text: "{"}, + {Line: 1, Text: "directive"}, + {Line: 1, Text: "}"}, + }, + }, + { + input: `host:123 { + #comment + directive + # comment + foobar # another comment + }`, + expected: []Token{ + {Line: 1, Text: "host:123"}, + {Line: 1, Text: "{"}, + {Line: 3, Text: "directive"}, + {Line: 5, Text: "foobar"}, + {Line: 6, Text: "}"}, + }, + }, + { + input: `a "quoted value" b + foobar`, + expected: []Token{ + {Line: 1, Text: "a"}, + {Line: 1, Text: "quoted value"}, + {Line: 1, Text: "b"}, + {Line: 2, Text: "foobar"}, + }, + }, + { + input: `A "quoted \"value\" inside" B`, + expected: []Token{ + {Line: 1, Text: "A"}, + {Line: 1, Text: `quoted "value" inside`}, + {Line: 1, Text: "B"}, + }, + }, + { + input: "A \"newline \\\ninside\" quotes", + expected: []Token{ + {Line: 1, Text: "A"}, + {Line: 1, Text: "newline \ninside"}, + {Line: 2, Text: "quotes"}, + }, + }, + { + input: `"don't\escape"`, + expected: []Token{ + {Line: 1, Text: `don't\escape`}, + }, + }, + { + input: `"don't\\escape"`, + expected: []Token{ + {Line: 1, Text: `don't\\escape`}, + }, + }, + { + input: `A "quoted value with line + break inside" { + foobar + }`, + expected: []Token{ + {Line: 1, Text: "A"}, + {Line: 1, Text: "quoted value with line\n\t\t\t\t\tbreak inside"}, + {Line: 2, Text: "{"}, + {Line: 3, Text: "foobar"}, + {Line: 4, Text: "}"}, + }, + }, + { + input: `"C:\php\php-cgi.exe"`, + expected: []Token{ + {Line: 1, Text: `C:\php\php-cgi.exe`}, + }, + }, + { + input: `empty "" string`, + expected: []Token{ + {Line: 1, Text: `empty`}, + {Line: 1, Text: ``}, + {Line: 1, Text: `string`}, + }, + }, + { + input: "skip those\r\nCR characters", + expected: []Token{ + {Line: 1, Text: "skip"}, + {Line: 1, Text: "those"}, + {Line: 2, Text: "CR"}, + {Line: 2, Text: "characters"}, + }, + }, + { + input: "\xEF\xBB\xBF:8080", // test with leading byte order mark + expected: []Token{ + {Line: 1, Text: ":8080"}, + }, + }, + } + + for i, testCase := range testCases { + actual := tokenize(testCase.input) + lexerCompare(t, i, testCase.expected, actual) + } +} + +func tokenize(input string) (tokens []Token) { + l := lexer{} + if err := l.load(strings.NewReader(input)); err != nil { + log.Printf("[ERROR] load failed: %v", err) + } + for l.next() { + tokens = append(tokens, l.token) + } + return +} + +func lexerCompare(t *testing.T, n int, expected, actual []Token) { + if len(expected) != len(actual) { + t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual)) + } + + for i := 0; i < len(actual) && i < len(expected); i++ { + if actual[i].Line != expected[i].Line { + t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d", + n, i, expected[i].Text, expected[i].Line, actual[i].Line) + break + } + if actual[i].Text != expected[i].Text { + t.Errorf("Test case %d token %d: expected text '%s' but was '%s'", + n, i, expected[i].Text, actual[i].Text) + break + } + } +} diff --git a/caddyconfig/caddyfile/parse.go b/caddyconfig/caddyfile/parse.go new file mode 100755 index 0000000..cc7ed25 --- /dev/null +++ b/caddyconfig/caddyfile/parse.go @@ -0,0 +1,521 @@ +// Copyright 2015 Light Code Labs, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyfile + +import ( + "io" + "log" + "os" + "path/filepath" + "strings" +) + +// Parse parses the input just enough to group tokens, in +// order, by server block. No further parsing is performed. +// Server blocks are returned in the order in which they appear. +// Directives that do not appear in validDirectives will cause +// an error. If you do not want to check for valid directives, +// pass in nil instead. +func Parse(filename string, input io.Reader) ([]ServerBlock, error) { + tokens, err := allTokens(input) + if err != nil { + return nil, err + } + p := parser{Dispenser: NewDispenser(filename, tokens)} + return p.parseAll() +} + +// allTokens lexes the entire input, but does not parse it. +// It returns all the tokens from the input, unstructured +// and in order. +func allTokens(input io.Reader) ([]Token, error) { + l := new(lexer) + err := l.load(input) + if err != nil { + return nil, err + } + var tokens []Token + for l.next() { + tokens = append(tokens, l.token) + } + return tokens, nil +} + +type parser struct { + *Dispenser + block ServerBlock // current server block being parsed + eof bool // if we encounter a valid EOF in a hard place + definedSnippets map[string][]Token + nesting int +} + +func (p *parser) parseAll() ([]ServerBlock, error) { + var blocks []ServerBlock + + for p.Next() { + err := p.parseOne() + if err != nil { + return blocks, err + } + if len(p.block.Keys) > 0 { + blocks = append(blocks, p.block) + } + if p.nesting > 0 { + return blocks, p.EOFErr() + } + } + + return blocks, nil +} + +func (p *parser) parseOne() error { + p.block = ServerBlock{} + return p.begin() +} + +func (p *parser) begin() error { + if len(p.tokens) == 0 { + return nil + } + + err := p.addresses() + + if err != nil { + return err + } + + if p.eof { + // this happens if the Caddyfile consists of only + // a line of addresses and nothing else + return nil + } + + if ok, name := p.isSnippet(); ok { + if p.definedSnippets == nil { + p.definedSnippets = map[string][]Token{} + } + if _, found := p.definedSnippets[name]; found { + return p.Errf("redeclaration of previously declared snippet %s", name) + } + // consume all tokens til matched close brace + tokens, err := p.snippetTokens() + if err != nil { + return err + } + p.definedSnippets[name] = tokens + // empty block keys so we don't save this block as a real server. + p.block.Keys = nil + return nil + } + + return p.blockContents() +} + +func (p *parser) addresses() error { + var expectingAnother bool + + for { + tkn := replaceEnvVars(p.Val()) + + // special case: import directive replaces tokens during parse-time + if tkn == "import" && p.isNewLine() { + err := p.doImport() + if err != nil { + return err + } + continue + } + + // Open brace definitely indicates end of addresses + if tkn == "{" { + if expectingAnother { + return p.Errf("Expected another address but had '%s' - check for extra comma", tkn) + } + break + } + + if tkn != "" { // empty token possible if user typed "" + // Trailing comma indicates another address will follow, which + // may possibly be on the next line + if tkn[len(tkn)-1] == ',' { + tkn = tkn[:len(tkn)-1] + expectingAnother = true + } else { + expectingAnother = false // but we may still see another one on this line + } + + p.block.Keys = append(p.block.Keys, tkn) + } + + // Advance token and possibly break out of loop or return error + hasNext := p.Next() + if expectingAnother && !hasNext { + return p.EOFErr() + } + if !hasNext { + p.eof = true + break // EOF + } + if !expectingAnother && p.isNewLine() { + break + } + } + + return nil +} + +func (p *parser) blockContents() error { + errOpenCurlyBrace := p.openCurlyBrace() + if errOpenCurlyBrace != nil { + // single-server configs don't need curly braces + p.cursor-- + } + + err := p.directives() + if err != nil { + return err + } + + // only look for close curly brace if there was an opening + if errOpenCurlyBrace == nil { + err = p.closeCurlyBrace() + if err != nil { + return err + } + } + + return nil +} + +// directives parses through all the lines for directives +// and it expects the next token to be the first +// directive. It goes until EOF or closing curly brace +// which ends the server block. +func (p *parser) directives() error { + for p.Next() { + // end of server block + if p.Val() == "}" { + // p.nesting has already been decremented + break + } + + // special case: import directive replaces tokens during parse-time + if p.Val() == "import" { + err := p.doImport() + if err != nil { + return err + } + p.cursor-- // cursor is advanced when we continue, so roll back one more + continue + } + + // normal case: parse a directive as a new segment + // (a "segment" is a line which starts with a directive + // and which ends at the end of the line or at the end of + // the block that is opened at the end of the line) + if err := p.directive(); err != nil { + return err + } + } + + return nil +} + +// doImport swaps out the import directive and its argument +// (a total of 2 tokens) with the tokens in the specified file +// or globbing pattern. When the function returns, the cursor +// is on the token before where the import directive was. In +// other words, call Next() to access the first token that was +// imported. +func (p *parser) doImport() error { + // syntax checks + if !p.NextArg() { + return p.ArgErr() + } + importPattern := replaceEnvVars(p.Val()) + if importPattern == "" { + return p.Err("Import requires a non-empty filepath") + } + if p.NextArg() { + return p.Err("Import takes only one argument (glob pattern or file)") + } + // splice out the import directive and its argument (2 tokens total) + tokensBefore := p.tokens[:p.cursor-1] + tokensAfter := p.tokens[p.cursor+1:] + var importedTokens []Token + + // first check snippets. That is a simple, non-recursive replacement + if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil { + importedTokens = p.definedSnippets[importPattern] + } else { + // make path relative to the file of the _token_ being processed rather + // than current working directory (issue #867) and then use glob to get + // list of matching filenames + absFile, err := filepath.Abs(p.Dispenser.File()) + if err != nil { + return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err) + } + + var matches []string + var globPattern string + if !filepath.IsAbs(importPattern) { + globPattern = filepath.Join(filepath.Dir(absFile), importPattern) + } else { + globPattern = importPattern + } + if strings.Count(globPattern, "*") > 1 || strings.Count(globPattern, "?") > 1 || + (strings.Contains(globPattern, "[") && strings.Contains(globPattern, "]")) { + // See issue #2096 - a pattern with many glob expansions can hang for too long + return p.Errf("Glob pattern may only contain one wildcard (*), but has others: %s", globPattern) + } + matches, err = filepath.Glob(globPattern) + + if err != nil { + return p.Errf("Failed to use import pattern %s: %v", importPattern, err) + } + if len(matches) == 0 { + if strings.ContainsAny(globPattern, "*?[]") { + log.Printf("[WARNING] No files matching import glob pattern: %s", importPattern) + } else { + return p.Errf("File to import not found: %s", importPattern) + } + } + + // collect all the imported tokens + + for _, importFile := range matches { + newTokens, err := p.doSingleImport(importFile) + if err != nil { + return err + } + importedTokens = append(importedTokens, newTokens...) + } + } + + // splice the imported tokens in the place of the import statement + // and rewind cursor so Next() will land on first imported token + p.tokens = append(tokensBefore, append(importedTokens, tokensAfter...)...) + p.cursor-- + + return nil +} + +// doSingleImport lexes the individual file at importFile and returns +// its tokens or an error, if any. +func (p *parser) doSingleImport(importFile string) ([]Token, error) { + file, err := os.Open(importFile) + if err != nil { + return nil, p.Errf("Could not import %s: %v", importFile, err) + } + defer file.Close() + + if info, err := file.Stat(); err != nil { + return nil, p.Errf("Could not import %s: %v", importFile, err) + } else if info.IsDir() { + return nil, p.Errf("Could not import %s: is a directory", importFile) + } + + importedTokens, err := allTokens(file) + if err != nil { + return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err) + } + + // Tack the file path onto these tokens so errors show the imported file's name + // (we use full, absolute path to avoid bugs: issue #1892) + filename, err := filepath.Abs(importFile) + if err != nil { + return nil, p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err) + } + for i := 0; i < len(importedTokens); i++ { + importedTokens[i].File = filename + } + + return importedTokens, nil +} + +// directive collects tokens until the directive's scope +// closes (either end of line or end of curly brace block). +// It expects the currently-loaded token to be a directive +// (or } that ends a server block). The collected tokens +// are loaded into the current server block for later use +// by directive setup functions. +func (p *parser) directive() error { + // evaluate any env vars in directive token + p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text) + + // a segment is a list of tokens associated with this directive + var segment Segment + + // the directive itself is appended as a relevant token + segment = append(segment, p.Token()) + + for p.Next() { + if p.Val() == "{" { + p.nesting++ + } else if p.isNewLine() && p.nesting == 0 { + p.cursor-- // read too far + break + } else if p.Val() == "}" && p.nesting > 0 { + p.nesting-- + } else if p.Val() == "}" && p.nesting == 0 { + return p.Err("Unexpected '}' because no matching opening brace") + } else if p.Val() == "import" && p.isNewLine() { + if err := p.doImport(); err != nil { + return err + } + p.cursor-- // cursor is advanced when we continue, so roll back one more + continue + } + p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text) + segment = append(segment, p.Token()) + } + + p.block.Segments = append(p.block.Segments, segment) + + if p.nesting > 0 { + return p.EOFErr() + } + + return nil +} + +// openCurlyBrace expects the current token to be an +// opening curly brace. This acts like an assertion +// because it returns an error if the token is not +// a opening curly brace. It does NOT advance the token. +func (p *parser) openCurlyBrace() error { + if p.Val() != "{" { + return p.SyntaxErr("{") + } + return nil +} + +// closeCurlyBrace expects the current token to be +// a closing curly brace. This acts like an assertion +// because it returns an error if the token is not +// a closing curly brace. It does NOT advance the token. +func (p *parser) closeCurlyBrace() error { + if p.Val() != "}" { + return p.SyntaxErr("}") + } + return nil +} + +// replaceEnvVars replaces environment variables that appear in the token +// and understands both the $UNIX and %WINDOWS% syntaxes. +func replaceEnvVars(s string) string { + s = replaceEnvReferences(s, "{%", "%}") + s = replaceEnvReferences(s, "{$", "}") + return s +} + +// replaceEnvReferences performs the actual replacement of env variables +// in s, given the placeholder start and placeholder end strings. +func replaceEnvReferences(s, refStart, refEnd string) string { + index := strings.Index(s, refStart) + for index != -1 { + endIndex := strings.Index(s[index:], refEnd) + if endIndex == -1 { + break + } + + endIndex += index + if endIndex > index+len(refStart) { + ref := s[index : endIndex+len(refEnd)] + s = strings.Replace(s, ref, os.Getenv(ref[len(refStart):len(ref)-len(refEnd)]), -1) + } else { + return s + } + index = strings.Index(s, refStart) + } + return s +} + +func (p *parser) isSnippet() (bool, string) { + keys := p.block.Keys + // A snippet block is a single key with parens. Nothing else qualifies. + if len(keys) == 1 && strings.HasPrefix(keys[0], "(") && strings.HasSuffix(keys[0], ")") { + return true, strings.TrimSuffix(keys[0][1:], ")") + } + return false, "" +} + +// read and store everything in a block for later replay. +func (p *parser) snippetTokens() ([]Token, error) { + // snippet must have curlies. + err := p.openCurlyBrace() + if err != nil { + return nil, err + } + nesting := 1 // count our own nesting in snippets + tokens := []Token{} + for p.Next() { + if p.Val() == "}" { + nesting-- + if nesting == 0 { + break + } + } + if p.Val() == "{" { + nesting++ + } + tokens = append(tokens, p.tokens[p.cursor]) + } + // make sure we're matched up + if nesting != 0 { + return nil, p.SyntaxErr("}") + } + return tokens, nil +} + +// ServerBlock associates any number of keys from the +// head of the server block with tokens, which are +// grouped by segments. +type ServerBlock struct { + Keys []string + Segments []Segment +} + +// DispenseDirective returns a dispenser that contains +// all the tokens in the server block. +func (sb ServerBlock) DispenseDirective(dir string) *Dispenser { + var tokens []Token + for _, seg := range sb.Segments { + if len(seg) > 0 && seg[0].Text == dir { + tokens = append(tokens, seg...) + } + } + return NewDispenser("", tokens) +} + +// Segment is a list of tokens which begins with a directive +// and ends at the end of the directive (either at the end of +// the line, or at the end of a block it opens). +type Segment []Token + +// Directive returns the directive name for the segment. +// The directive name is the text of the first token. +func (s Segment) Directive() string { + if len(s) > 0 { + return s[0].Text + } + return "" +} + +// NewDispenser returns a dispenser for this +// segment's tokens. +func (s Segment) NewDispenser() *Dispenser { + return NewDispenser("", s) +} diff --git a/caddyconfig/caddyfile/parse_test.go b/caddyconfig/caddyfile/parse_test.go new file mode 100755 index 0000000..19959de --- /dev/null +++ b/caddyconfig/caddyfile/parse_test.go @@ -0,0 +1,681 @@ +// Copyright 2015 Light Code Labs, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyfile + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" +) + +// TODO: re-enable all tests + +func TestAllTokens(t *testing.T) { + input := strings.NewReader("a b c\nd e") + expected := []string{"a", "b", "c", "d", "e"} + tokens, err := allTokens(input) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if len(tokens) != len(expected) { + t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens)) + } + + for i, val := range expected { + if tokens[i].Text != val { + t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].Text) + } + } +} + +func TestParseOneAndImport(t *testing.T) { + testParseOne := func(input string) (ServerBlock, error) { + p := testParser(input) + p.Next() // parseOne doesn't call Next() to start, so we must + err := p.parseOne() + return p.block, err + } + + for i, test := range []struct { + input string + shouldErr bool + keys []string + numTokens []int // number of tokens to expect in each segment + }{ + {`localhost`, false, []string{ + "localhost", + }, []int{}}, + + {`localhost + dir1`, false, []string{ + "localhost", + }, []int{1}}, + + {`localhost:1234 + dir1 foo bar`, false, []string{ + "localhost:1234", + }, []int{3}, + }, + + {`localhost { + dir1 + }`, false, []string{ + "localhost", + }, []int{1}}, + + {`localhost:1234 { + dir1 foo bar + dir2 + }`, false, []string{ + "localhost:1234", + }, []int{3, 1}}, + + {`http://localhost https://localhost + dir1 foo bar`, false, []string{ + "http://localhost", + "https://localhost", + }, []int{3}}, + + {`http://localhost https://localhost { + dir1 foo bar + }`, false, []string{ + "http://localhost", + "https://localhost", + }, []int{3}}, + + {`http://localhost, https://localhost { + dir1 foo bar + }`, false, []string{ + "http://localhost", + "https://localhost", + }, []int{3}}, + + {`http://localhost, { + }`, true, []string{ + "http://localhost", + }, []int{}}, + + {`host1:80, http://host2.com + dir1 foo bar + dir2 baz`, false, []string{ + "host1:80", + "http://host2.com", + }, []int{3, 2}}, + + {`http://host1.com, + http://host2.com, + https://host3.com`, false, []string{ + "http://host1.com", + "http://host2.com", + "https://host3.com", + }, []int{}}, + + {`http://host1.com:1234, https://host2.com + dir1 foo { + bar baz + } + dir2`, false, []string{ + "http://host1.com:1234", + "https://host2.com", + }, []int{6, 1}}, + + {`127.0.0.1 + dir1 { + bar baz + } + dir2 { + foo bar + }`, false, []string{ + "127.0.0.1", + }, []int{5, 5}}, + + {`localhost + dir1 { + foo`, true, []string{ + "localhost", + }, []int{3}}, + + {`localhost + dir1 { + }`, false, []string{ + "localhost", + }, []int{3}}, + + {`localhost + dir1 { + } }`, true, []string{ + "localhost", + }, []int{}}, + + {`localhost + dir1 { + nested { + foo + } + } + dir2 foo bar`, false, []string{ + "localhost", + }, []int{7, 3}}, + + {``, false, []string{}, []int{}}, + + {`localhost + dir1 arg1 + import testdata/import_test1.txt`, false, []string{ + "localhost", + }, []int{2, 3, 1}}, + + {`import testdata/import_test2.txt`, false, []string{ + "host1", + }, []int{1, 2}}, + + {`import testdata/import_test1.txt testdata/import_test2.txt`, true, []string{}, []int{}}, + + {`import testdata/not_found.txt`, true, []string{}, []int{}}, + + {`""`, false, []string{}, []int{}}, + + {``, false, []string{}, []int{}}, + + // test cases found by fuzzing! + {`import }{$"`, true, []string{}, []int{}}, + {`import /*/*.txt`, true, []string{}, []int{}}, + {`import /???/?*?o`, true, []string{}, []int{}}, + {`import /??`, true, []string{}, []int{}}, + {`import /[a-z]`, true, []string{}, []int{}}, + {`import {$}`, true, []string{}, []int{}}, + {`import {%}`, true, []string{}, []int{}}, + {`import {$$}`, true, []string{}, []int{}}, + {`import {%%}`, true, []string{}, []int{}}, + } { + result, err := testParseOne(test.input) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected an error, but didn't get one", i) + } + if !test.shouldErr && err != nil { + t.Errorf("Test %d: Expected no error, but got: %v", i, err) + } + + if len(result.Keys) != len(test.keys) { + t.Errorf("Test %d: Expected %d keys, got %d", + i, len(test.keys), len(result.Keys)) + continue + } + for j, addr := range result.Keys { + if addr != test.keys[j] { + t.Errorf("Test %d, key %d: Expected '%s', but was '%s'", + i, j, test.keys[j], addr) + } + } + + if len(result.Segments) != len(test.numTokens) { + t.Errorf("Test %d: Expected %d segments, had %d", + i, len(test.numTokens), len(result.Segments)) + continue + } + + for j, seg := range result.Segments { + if len(seg) != test.numTokens[j] { + t.Errorf("Test %d, segment %d: Expected %d tokens, counted %d", + i, j, test.numTokens[j], len(seg)) + continue + } + } + } +} + +func TestRecursiveImport(t *testing.T) { + testParseOne := func(input string) (ServerBlock, error) { + p := testParser(input) + p.Next() // parseOne doesn't call Next() to start, so we must + err := p.parseOne() + return p.block, err + } + + isExpected := func(got ServerBlock) bool { + if len(got.Keys) != 1 || got.Keys[0] != "localhost" { + t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys) + return false + } + if len(got.Segments) != 2 { + t.Errorf("got wrong number of segments: expect 2, got %d", len(got.Segments)) + return false + } + if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 2 { + t.Errorf("got unexpect tokens: %v", got.Segments) + return false + } + return true + } + + recursiveFile1, err := filepath.Abs("testdata/recursive_import_test1") + if err != nil { + t.Fatal(err) + } + recursiveFile2, err := filepath.Abs("testdata/recursive_import_test2") + if err != nil { + t.Fatal(err) + } + + // test relative recursive import + err = ioutil.WriteFile(recursiveFile1, []byte( + `localhost + dir1 + import recursive_import_test2`), 0644) + if err != nil { + t.Fatal(err) + } + defer os.Remove(recursiveFile1) + + err = ioutil.WriteFile(recursiveFile2, []byte("dir2 1"), 0644) + if err != nil { + t.Fatal(err) + } + defer os.Remove(recursiveFile2) + + // import absolute path + result, err := testParseOne("import " + recursiveFile1) + if err != nil { + t.Fatal(err) + } + if !isExpected(result) { + t.Error("absolute+relative import failed") + } + + // import relative path + result, err = testParseOne("import testdata/recursive_import_test1") + if err != nil { + t.Fatal(err) + } + if !isExpected(result) { + t.Error("relative+relative import failed") + } + + // test absolute recursive import + err = ioutil.WriteFile(recursiveFile1, []byte( + `localhost + dir1 + import `+recursiveFile2), 0644) + if err != nil { + t.Fatal(err) + } + + // import absolute path + result, err = testParseOne("import " + recursiveFile1) + if err != nil { + t.Fatal(err) + } + if !isExpected(result) { + t.Error("absolute+absolute import failed") + } + + // import relative path + result, err = testParseOne("import testdata/recursive_import_test1") + if err != nil { + t.Fatal(err) + } + if !isExpected(result) { + t.Error("relative+absolute import failed") + } +} + +func TestDirectiveImport(t *testing.T) { + testParseOne := func(input string) (ServerBlock, error) { + p := testParser(input) + p.Next() // parseOne doesn't call Next() to start, so we must + err := p.parseOne() + return p.block, err + } + + isExpected := func(got ServerBlock) bool { + if len(got.Keys) != 1 || got.Keys[0] != "localhost" { + t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys) + return false + } + if len(got.Segments) != 2 { + t.Errorf("got wrong number of segments: expect 2, got %d", len(got.Segments)) + return false + } + if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 8 { + t.Errorf("got unexpect tokens: %v", got.Segments) + return false + } + return true + } + + directiveFile, err := filepath.Abs("testdata/directive_import_test") + if err != nil { + t.Fatal(err) + } + + err = ioutil.WriteFile(directiveFile, []byte(`prop1 1 + prop2 2`), 0644) + if err != nil { + t.Fatal(err) + } + defer os.Remove(directiveFile) + + // import from existing file + result, err := testParseOne(`localhost + dir1 + proxy { + import testdata/directive_import_test + transparent + }`) + if err != nil { + t.Fatal(err) + } + if !isExpected(result) { + t.Error("directive import failed") + } + + // import from nonexistent file + _, err = testParseOne(`localhost + dir1 + proxy { + import testdata/nonexistent_file + transparent + }`) + if err == nil { + t.Fatal("expected error when importing a nonexistent file") + } +} + +func TestParseAll(t *testing.T) { + for i, test := range []struct { + input string + shouldErr bool + keys [][]string // keys per server block, in order + }{ + {`localhost`, false, [][]string{ + {"localhost"}, + }}, + + {`localhost:1234`, false, [][]string{ + {"localhost:1234"}, + }}, + + {`localhost:1234 { + } + localhost:2015 { + }`, false, [][]string{ + {"localhost:1234"}, + {"localhost:2015"}, + }}, + + {`localhost:1234, http://host2`, false, [][]string{ + {"localhost:1234", "http://host2"}, + }}, + + {`localhost:1234, http://host2,`, true, [][]string{}}, + + {`http://host1.com, http://host2.com { + } + https://host3.com, https://host4.com { + }`, false, [][]string{ + {"http://host1.com", "http://host2.com"}, + {"https://host3.com", "https://host4.com"}, + }}, + + {`import testdata/import_glob*.txt`, false, [][]string{ + {"glob0.host0"}, + {"glob0.host1"}, + {"glob1.host0"}, + {"glob2.host0"}, + }}, + + {`import notfound/*`, false, [][]string{}}, // glob needn't error with no matches + {`import notfound/file.conf`, true, [][]string{}}, // but a specific file should + } { + p := testParser(test.input) + blocks, err := p.parseAll() + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected an error, but didn't get one", i) + } + if !test.shouldErr && err != nil { + t.Errorf("Test %d: Expected no error, but got: %v", i, err) + } + + if len(blocks) != len(test.keys) { + t.Errorf("Test %d: Expected %d server blocks, got %d", + i, len(test.keys), len(blocks)) + continue + } + for j, block := range blocks { + if len(block.Keys) != len(test.keys[j]) { + t.Errorf("Test %d: Expected %d keys in block %d, got %d", + i, len(test.keys[j]), j, len(block.Keys)) + continue + } + for k, addr := range block.Keys { + if addr != test.keys[j][k] { + t.Errorf("Test %d, block %d, key %d: Expected '%s', but got '%s'", + i, j, k, test.keys[j][k], addr) + } + } + } + } +} + +func TestEnvironmentReplacement(t *testing.T) { + os.Setenv("PORT", "8080") + os.Setenv("ADDRESS", "servername.com") + os.Setenv("FOOBAR", "foobar") + os.Setenv("PARTIAL_DIR", "r1") + + // basic test; unix-style env vars + p := testParser(`{$ADDRESS}`) + blocks, _ := p.parseAll() + if actual, expected := blocks[0].Keys[0], "servername.com"; expected != actual { + t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) + } + + // basic test; unix-style env vars + p = testParser(`di{$PARTIAL_DIR}`) + blocks, _ = p.parseAll() + if actual, expected := blocks[0].Keys[0], "dir1"; expected != actual { + t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) + } + + // multiple vars per token + p = testParser(`{$ADDRESS}:{$PORT}`) + blocks, _ = p.parseAll() + if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual { + t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) + } + + // windows-style var and unix style in same token + p = testParser(`{%ADDRESS%}:{$PORT}`) + blocks, _ = p.parseAll() + if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual { + t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) + } + + // reverse order + p = testParser(`{$ADDRESS}:{%PORT%}`) + blocks, _ = p.parseAll() + if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual { + t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) + } + + // env var in server block body as argument + p = testParser(":{%PORT%}\ndir1 {$FOOBAR}") + blocks, _ = p.parseAll() + if actual, expected := blocks[0].Keys[0], ":8080"; expected != actual { + t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) + } + if actual, expected := blocks[0].Segments[0][1].Text, "foobar"; expected != actual { + t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) + } + + // combined windows env vars in argument + p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}") + blocks, _ = p.parseAll() + if actual, expected := blocks[0].Segments[0][1].Text, "servername.com/foobar"; expected != actual { + t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) + } + + // malformed env var (windows) + p = testParser(":1234\ndir1 {%ADDRESS}") + blocks, _ = p.parseAll() + if actual, expected := blocks[0].Segments[0][1].Text, "{%ADDRESS}"; expected != actual { + t.Errorf("Expected host to be '%s' but was '%s'", expected, actual) + } + + // malformed (non-existent) env var (unix) + p = testParser(`:{$PORT$}`) + blocks, _ = p.parseAll() + if actual, expected := blocks[0].Keys[0], ":"; expected != actual { + t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) + } + + // in quoted field + p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"") + blocks, _ = p.parseAll() + if actual, expected := blocks[0].Segments[0][1].Text, "Test foobar test"; expected != actual { + t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) + } + + // after end token + p = testParser(":1234\nanswer \"{{ .Name }} {$FOOBAR}\"") + blocks, _ = p.parseAll() + if actual, expected := blocks[0].Segments[0][1].Text, "{{ .Name }} foobar"; expected != actual { + t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) + } +} + +func TestSnippets(t *testing.T) { + p := testParser(` + (common) { + gzip foo + errors stderr + } + http://example.com { + import common + } + `) + blocks, err := p.parseAll() + if err != nil { + t.Fatal(err) + } + for _, b := range blocks { + t.Log(b.Keys) + t.Log(b.Segments) + } + if len(blocks) != 1 { + t.Fatalf("Expect exactly one server block. Got %d.", len(blocks)) + } + if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual { + t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual) + } + if len(blocks[0].Segments) != 2 { + t.Fatalf("Server block should have tokens from import, got: %+v", blocks[0]) + } + if actual, expected := blocks[0].Segments[0][0].Text, "gzip"; expected != actual { + t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) + } + if actual, expected := blocks[0].Segments[1][1].Text, "stderr"; expected != actual { + t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) + } +} + +func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) { + file, err := ioutil.TempFile("", t.Name()) + if err != nil { + panic(err) // get a stack trace so we know where this was called from. + } + if _, err := file.WriteString(str); err != nil { + panic(err) + } + if err := file.Close(); err != nil { + panic(err) + } + return file.Name() +} + +func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) { + fileName := writeStringToTempFileOrDie(t, ` + http://example.com { + # This isn't an import directive, it's just an arg with value 'import' + basicauth / import password + } + `) + // Parse the root file that imports the other one. + p := testParser(`import ` + fileName) + blocks, err := p.parseAll() + if err != nil { + t.Fatal(err) + } + for _, b := range blocks { + t.Log(b.Keys) + t.Log(b.Segments) + } + auth := blocks[0].Segments[0] + line := auth[0].Text + " " + auth[1].Text + " " + auth[2].Text + " " + auth[3].Text + if line != "basicauth / import password" { + // Previously, it would be changed to: + // basicauth / import /path/to/test/dir/password + // referencing a file that (probably) doesn't exist and changing the + // password! + t.Errorf("Expected basicauth tokens to be 'basicauth / import password' but got %#q", line) + } +} + +func TestSnippetAcrossMultipleFiles(t *testing.T) { + // Make the derived Caddyfile that expects (common) to be defined. + fileName := writeStringToTempFileOrDie(t, ` + http://example.com { + import common + } + `) + + // Parse the root file that defines (common) and then imports the other one. + p := testParser(` + (common) { + gzip foo + } + import ` + fileName + ` + `) + + blocks, err := p.parseAll() + if err != nil { + t.Fatal(err) + } + for _, b := range blocks { + t.Log(b.Keys) + t.Log(b.Segments) + } + if len(blocks) != 1 { + t.Fatalf("Expect exactly one server block. Got %d.", len(blocks)) + } + if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual { + t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual) + } + if len(blocks[0].Segments) != 1 { + t.Fatalf("Server block should have tokens from import") + } + if actual, expected := blocks[0].Segments[0][0].Text, "gzip"; expected != actual { + t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) + } +} + +func testParser(input string) parser { + return parser{Dispenser: newTestDispenser(input)} +} diff --git a/caddyconfig/caddyfile/testdata/import_glob0.txt b/caddyconfig/caddyfile/testdata/import_glob0.txt new file mode 100755 index 0000000..e610b5e --- /dev/null +++ b/caddyconfig/caddyfile/testdata/import_glob0.txt @@ -0,0 +1,6 @@ +glob0.host0 { + dir2 arg1 +} + +glob0.host1 { +} diff --git a/caddyconfig/caddyfile/testdata/import_glob1.txt b/caddyconfig/caddyfile/testdata/import_glob1.txt new file mode 100755 index 0000000..111eb04 --- /dev/null +++ b/caddyconfig/caddyfile/testdata/import_glob1.txt @@ -0,0 +1,4 @@ +glob1.host0 { + dir1 + dir2 arg1 +} diff --git a/caddyconfig/caddyfile/testdata/import_glob2.txt b/caddyconfig/caddyfile/testdata/import_glob2.txt new file mode 100755 index 0000000..c09f784 --- /dev/null +++ b/caddyconfig/caddyfile/testdata/import_glob2.txt @@ -0,0 +1,3 @@ +glob2.host0 { + dir2 arg1 +} diff --git a/caddyconfig/caddyfile/testdata/import_test1.txt b/caddyconfig/caddyfile/testdata/import_test1.txt new file mode 100755 index 0000000..dac7b29 --- /dev/null +++ b/caddyconfig/caddyfile/testdata/import_test1.txt @@ -0,0 +1,2 @@ +dir2 arg1 arg2 +dir3
\ No newline at end of file diff --git a/caddyconfig/caddyfile/testdata/import_test2.txt b/caddyconfig/caddyfile/testdata/import_test2.txt new file mode 100755 index 0000000..140c879 --- /dev/null +++ b/caddyconfig/caddyfile/testdata/import_test2.txt @@ -0,0 +1,4 @@ +host1 { + dir1 + dir2 arg1 +}
\ No newline at end of file diff --git a/caddyconfig/configadapters.go b/caddyconfig/configadapters.go new file mode 100644 index 0000000..6e5d530 --- /dev/null +++ b/caddyconfig/configadapters.go @@ -0,0 +1,113 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyconfig + +import ( + "encoding/json" + "fmt" +) + +// Adapter is a type which can adapt a configuration to Caddy JSON. +// It returns the results and any warnings, or an error. +type Adapter interface { + Adapt(body []byte, options map[string]string) ([]byte, []Warning, error) +} + +// Warning represents a warning or notice related to conversion. +type Warning struct { + File string + Line int + Directive string + Message string +} + +// JSON encodes val as JSON, returning it as a json.RawMessage. Any +// marshaling errors (which are highly unlikely with correct code) +// are converted to warnings. This is convenient when filling config +// structs that require a json.RawMessage, without having to worry +// about errors. +func JSON(val interface{}, warnings *[]Warning) json.RawMessage { + b, err := json.Marshal(val) + if err != nil { + if warnings != nil { + *warnings = append(*warnings, Warning{Message: err.Error()}) + } + return nil + } + return b +} + +// JSONModuleObject is like JSON, except it marshals val into a JSON object +// and then adds a key to that object named fieldName with the value fieldVal. +// This is useful for JSON-encoding module values where the module name has to +// be described within the object by a certain key; for example, +// "responder": "file_server" for a file server HTTP responder. The val must +// encode into a map[string]interface{} (i.e. it must be a struct or map), +// and any errors are converted into warnings, so this can be conveniently +// used when filling a struct. For correct code, there should be no errors. +func JSONModuleObject(val interface{}, fieldName, fieldVal string, warnings *[]Warning) json.RawMessage { + // encode to a JSON object first + enc, err := json.Marshal(val) + if err != nil { + if warnings != nil { + *warnings = append(*warnings, Warning{Message: err.Error()}) + } + return nil + } + + // then decode the object + var tmp map[string]interface{} + err = json.Unmarshal(enc, &tmp) + if err != nil { + if warnings != nil { + *warnings = append(*warnings, Warning{Message: err.Error()}) + } + return nil + } + + // so we can easily add the module's field with its appointed value + tmp[fieldName] = fieldVal + + // then re-marshal as JSON + result, err := json.Marshal(tmp) + if err != nil { + if warnings != nil { + *warnings = append(*warnings, Warning{Message: err.Error()}) + } + return nil + } + + return result +} + +// JSONIndent is used to JSON-marshal the final resulting Caddy +// configuration in a consistent, human-readable way. +func JSONIndent(val interface{}) ([]byte, error) { + return json.MarshalIndent(val, "", "\t") +} + +func RegisterAdapter(name string, adapter Adapter) error { + if _, ok := configAdapters[name]; ok { + return fmt.Errorf("%s: already registered", name) + } + configAdapters[name] = adapter + return nil +} + +func GetAdapter(name string) Adapter { + return configAdapters[name] +} + +var configAdapters = make(map[string]Adapter) diff --git a/caddyconfig/httpcaddyfile/addresses.go b/caddyconfig/httpcaddyfile/addresses.go new file mode 100644 index 0000000..2adb818 --- /dev/null +++ b/caddyconfig/httpcaddyfile/addresses.go @@ -0,0 +1,332 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httpcaddyfile + +import ( + "fmt" + "net" + "net/url" + "reflect" + "strconv" + "strings" + + "github.com/caddyserver/caddy/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/mholt/certmagic" +) + +// mapAddressToServerBlocks returns a map of listener address to list of server +// blocks that will be served on that address. To do this, each server block is +// expanded so that each one is considered individually, although keys of a +// server block that share the same address stay grouped together so the config +// isn't repeated unnecessarily. For example, this Caddyfile: +// +// example.com { +// bind 127.0.0.1 +// } +// www.example.com, example.net/path, localhost:9999 { +// bind 127.0.0.1 1.2.3.4 +// } +// +// has two server blocks to start with. But expressed in this Caddyfile are +// actually 4 listener addresses: 127.0.0.1:443, 1.2.3.4:443, 127.0.0.1:9999, +// and 127.0.0.1:9999. This is because the bind directive is applied to each +// key of its server block (specifying the host part), and each key may have +// a different port. And we definitely need to be sure that a site which is +// bound to be served on a specific interface is not served on others just +// beceause that is more convenient: it would be a potential security risk +// if the difference between interfaces means private vs. public. +// +// So what this function does for the example above is iterate each server +// block, and for each server block, iterate its keys. For the first, it +// finds one key (example.com) and determines its listener address +// (127.0.0.1:443 - because of 'bind' and automatic HTTPS). It then adds +// the listener address to the map value returned by this function, with +// the first server block as one of its associations. +// +// It then iterates each key on the second server block and associates them +// with one or more listener addresses. Indeed, each key in this block has +// two listener addresses because of the 'bind' directive. Once we know +// which addresses serve which keys, we can create a new server block for +// each address containing the contents of the server block and only those +// specific keys of the server block which use that address. +// +// It is possible and even likely that some keys in the returned map have +// the exact same list of server blocks (i.e. they are identical). This +// happens when multiple hosts are declared with a 'bind' directive and +// the resulting listener addresses are not shared by any other server +// block (or the other server blocks are exactly identical in their token +// contents). This happens with our example above because 1.2.3.4:443 +// and 1.2.3.4:9999 are used exclusively with the second server block. This +// repetition may be undesirable, so call consolidateAddrMappings() to map +// multiple addresses to the same lists of server blocks (a many:many mapping). +// (Doing this is essentially a map-reduce technique.) +func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock) (map[string][]serverBlock, error) { + sbmap := make(map[string][]serverBlock) + + for i, sblock := range originalServerBlocks { + // within a server block, we need to map all the listener addresses + // implied by the server block to the keys of the server block which + // will be served by them; this has the effect of treating each + // key of a server block as its own, but without having to repeat its + // contents in cases where multiple keys really can be served together + addrToKeys := make(map[string][]string) + for j, key := range sblock.block.Keys { + // a key can have multiple listener addresses if there are multiple + // arguments to the 'bind' directive (although they will all have + // the same port, since the port is defined by the key or is implicit + // through automatic HTTPS) + addrs, err := st.listenerAddrsForServerBlockKey(sblock, key) + if err != nil { + return nil, fmt.Errorf("server block %d, key %d (%s): determining listener address: %v", i, j, key, err) + } + + // associate this key with each listener address it is served on + for _, addr := range addrs { + addrToKeys[addr] = append(addrToKeys[addr], key) + } + } + + // now that we know which addresses serve which keys of this + // server block, we iterate that mapping and create a list of + // new server blocks for each address where the keys of the + // server block are only the ones which use the address; but + // the contents (tokens) are of course the same + for addr, keys := range addrToKeys { + sbmap[addr] = append(sbmap[addr], serverBlock{ + block: caddyfile.ServerBlock{ + Keys: keys, + Segments: sblock.block.Segments, + }, + pile: sblock.pile, + }) + } + } + + return sbmap, nil +} + +// consolidateAddrMappings eliminates repetition of identical server blocks in a mapping of +// single listener addresses to lists of server blocks. Since multiple addresses may serve +// identical sites (server block contents), this function turns a 1:many mapping into a +// many:many mapping. Server block contents (tokens) must be exactly identical so that +// reflect.DeepEqual returns true in order for the addresses to be combined. Identical +// entries are deleted from the addrToServerBlocks map. Essentially, each pairing (each +// association from multiple addresses to multiple server blocks; i.e. each element of +// the returned slice) becomes a server definition in the output JSON. +func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]serverBlock) []sbAddrAssociation { + var sbaddrs []sbAddrAssociation + for addr, sblocks := range addrToServerBlocks { + // we start with knowing that at least this address + // maps to these server blocks + a := sbAddrAssociation{ + addresses: []string{addr}, + serverBlocks: sblocks, + } + + // now find other addresses that map to identical + // server blocks and add them to our list of + // addresses, while removing them from the map + for otherAddr, otherSblocks := range addrToServerBlocks { + if addr == otherAddr { + continue + } + if reflect.DeepEqual(sblocks, otherSblocks) { + a.addresses = append(a.addresses, otherAddr) + delete(addrToServerBlocks, otherAddr) + } + } + + sbaddrs = append(sbaddrs, a) + } + return sbaddrs +} + +func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string) ([]string, error) { + addr, err := ParseAddress(key) + if err != nil { + return nil, fmt.Errorf("parsing key: %v", err) + } + addr = addr.Normalize() + + lnPort := defaultPort + if addr.Port != "" { + // port explicitly defined + lnPort = addr.Port + } else if certmagic.HostQualifies(addr.Host) { + // automatic HTTPS + lnPort = strconv.Itoa(certmagic.HTTPSPort) + } + + // the bind directive specifies hosts, but is optional + var lnHosts []string + for _, cfgVal := range sblock.pile["bind"] { + lnHosts = append(lnHosts, cfgVal.Value.([]string)...) + } + if len(lnHosts) == 0 { + lnHosts = []string{""} + } + + // use a map to prevent duplication + listeners := make(map[string]struct{}) + for _, host := range lnHosts { + listeners[net.JoinHostPort(host, lnPort)] = struct{}{} + } + + // now turn map into list + var listenersList []string + for lnStr := range listeners { + listenersList = append(listenersList, lnStr) + } + // sort.Strings(listenersList) // TODO: is sorting necessary? + + return listenersList, nil +} + +// Address represents a site address. It contains +// the original input value, and the component +// parts of an address. The component parts may be +// updated to the correct values as setup proceeds, +// but the original value should never be changed. +// +// The Host field must be in a normalized form. +type Address struct { + Original, Scheme, Host, Port, Path string +} + +// ParseAddress parses an address string into a structured format with separate +// scheme, host, port, and path portions, as well as the original input string. +func ParseAddress(str string) (Address, error) { + httpPort, httpsPort := strconv.Itoa(certmagic.HTTPPort), strconv.Itoa(certmagic.HTTPSPort) + + input := str + + // Split input into components (prepend with // to force host portion by default) + if !strings.Contains(str, "//") && !strings.HasPrefix(str, "/") { + str = "//" + str + } + + u, err := url.Parse(str) + if err != nil { + return Address{}, err + } + + // separate host and port + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + host, port, err = net.SplitHostPort(u.Host + ":") + if err != nil { + host = u.Host + } + } + + // see if we can set port based off scheme + if port == "" { + if u.Scheme == "http" { + port = httpPort + } else if u.Scheme == "https" { + port = httpsPort + } + } + + // error if scheme and port combination violate convention + if (u.Scheme == "http" && port == httpsPort) || (u.Scheme == "https" && port == httpPort) { + return Address{}, fmt.Errorf("[%s] scheme and port violate convention", input) + } + + return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err +} + +// TODO: which of the methods on Address are even used? + +// String returns a human-readable form of a. It will +// be a cleaned-up and filled-out URL string. +func (a Address) String() string { + if a.Host == "" && a.Port == "" { + return "" + } + scheme := a.Scheme + if scheme == "" { + if a.Port == strconv.Itoa(certmagic.HTTPSPort) { + scheme = "https" + } else { + scheme = "http" + } + } + s := scheme + if s != "" { + s += "://" + } + if a.Port != "" && + ((scheme == "https" && a.Port != strconv.Itoa(caddyhttp.DefaultHTTPSPort)) || + (scheme == "http" && a.Port != strconv.Itoa(caddyhttp.DefaultHTTPPort))) { + s += net.JoinHostPort(a.Host, a.Port) + } else { + s += a.Host + } + if a.Path != "" { + s += a.Path + } + return s +} + +// Normalize returns a normalized version of a. +func (a Address) Normalize() Address { + path := a.Path + if !caseSensitivePath { + path = strings.ToLower(path) + } + + // ensure host is normalized if it's an IP address + host := a.Host + if ip := net.ParseIP(host); ip != nil { + host = ip.String() + } + + return Address{ + Original: a.Original, + Scheme: strings.ToLower(a.Scheme), + Host: strings.ToLower(host), + Port: a.Port, + Path: path, + } +} + +// Key returns a string form of a, much like String() does, but this +// method doesn't add anything default that wasn't in the original. +func (a Address) Key() string { + res := "" + if a.Scheme != "" { + res += a.Scheme + "://" + } + if a.Host != "" { + res += a.Host + } + // insert port only if the original has its own explicit port + if a.Port != "" && + len(a.Original) >= len(res) && + strings.HasPrefix(a.Original[len(res):], ":"+a.Port) { + res += ":" + a.Port + } + if a.Path != "" { + res += a.Path + } + return res +} + +const ( + defaultPort = "2015" + caseSensitivePath = false // TODO: Used? +) diff --git a/caddyconfig/httpcaddyfile/addresses_test.go b/caddyconfig/httpcaddyfile/addresses_test.go new file mode 100644 index 0000000..d6aa6f6 --- /dev/null +++ b/caddyconfig/httpcaddyfile/addresses_test.go @@ -0,0 +1,166 @@ +package httpcaddyfile + +import ( + "strings" + "testing" +) + +func TestParseAddress(t *testing.T) { + for i, test := range []struct { + input string + scheme, host, port, path string + shouldErr bool + }{ + {`localhost`, "", "localhost", "", "", false}, + {`localhost:1234`, "", "localhost", "1234", "", false}, + {`localhost:`, "", "localhost", "", "", false}, + {`0.0.0.0`, "", "0.0.0.0", "", "", false}, + {`127.0.0.1:1234`, "", "127.0.0.1", "1234", "", false}, + {`:1234`, "", "", "1234", "", false}, + {`[::1]`, "", "::1", "", "", false}, + {`[::1]:1234`, "", "::1", "1234", "", false}, + {`:`, "", "", "", "", false}, + {`:http`, "", "", "", "", true}, + {`:https`, "", "", "", "", true}, + {`localhost:http`, "", "", "", "", true}, // using service name in port is verboten, as of Go 1.12.8 + {`localhost:https`, "", "", "", "", true}, + {`http://localhost:https`, "", "", "", "", true}, // conflict + {`http://localhost:http`, "", "", "", "", true}, // repeated scheme + {`host:https/path`, "", "", "", "", true}, + {`http://localhost:443`, "", "", "", "", true}, // not conventional + {`https://localhost:80`, "", "", "", "", true}, // not conventional + {`http://localhost`, "http", "localhost", "80", "", false}, + {`https://localhost`, "https", "localhost", "443", "", false}, + {`http://127.0.0.1`, "http", "127.0.0.1", "80", "", false}, + {`https://127.0.0.1`, "https", "127.0.0.1", "443", "", false}, + {`http://[::1]`, "http", "::1", "80", "", false}, + {`http://localhost:1234`, "http", "localhost", "1234", "", false}, + {`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", "", false}, + {`http://[::1]:1234`, "http", "::1", "1234", "", false}, + {``, "", "", "", "", false}, + {`::1`, "", "::1", "", "", true}, + {`localhost::`, "", "localhost::", "", "", true}, + {`#$%@`, "", "", "", "", true}, + {`host/path`, "", "host", "", "/path", false}, + {`http://host/`, "http", "host", "80", "/", false}, + {`//asdf`, "", "asdf", "", "", false}, + {`:1234/asdf`, "", "", "1234", "/asdf", false}, + {`http://host/path`, "http", "host", "80", "/path", false}, + {`https://host:443/path/foo`, "https", "host", "443", "/path/foo", false}, + {`host:80/path`, "", "host", "80", "/path", false}, + {`/path`, "", "", "", "/path", false}, + } { + actual, err := ParseAddress(test.input) + + if err != nil && !test.shouldErr { + t.Errorf("Test %d (%s): Expected no error, but had error: %v", i, test.input, err) + } + if err == nil && test.shouldErr { + t.Errorf("Test %d (%s): Expected error, but had none", i, test.input) + } + + if !test.shouldErr && actual.Original != test.input { + t.Errorf("Test %d (%s): Expected original '%s', got '%s'", i, test.input, test.input, actual.Original) + } + if actual.Scheme != test.scheme { + t.Errorf("Test %d (%s): Expected scheme '%s', got '%s'", i, test.input, test.scheme, actual.Scheme) + } + if actual.Host != test.host { + t.Errorf("Test %d (%s): Expected host '%s', got '%s'", i, test.input, test.host, actual.Host) + } + if actual.Port != test.port { + t.Errorf("Test %d (%s): Expected port '%s', got '%s'", i, test.input, test.port, actual.Port) + } + if actual.Path != test.path { + t.Errorf("Test %d (%s): Expected path '%s', got '%s'", i, test.input, test.path, actual.Path) + } + } +} + +func TestAddressString(t *testing.T) { + for i, test := range []struct { + addr Address + expected string + }{ + {Address{Scheme: "http", Host: "host", Port: "1234", Path: "/path"}, "http://host:1234/path"}, + {Address{Scheme: "", Host: "host", Port: "", Path: ""}, "http://host"}, + {Address{Scheme: "", Host: "host", Port: "80", Path: ""}, "http://host"}, + {Address{Scheme: "", Host: "host", Port: "443", Path: ""}, "https://host"}, + {Address{Scheme: "https", Host: "host", Port: "443", Path: ""}, "https://host"}, + {Address{Scheme: "https", Host: "host", Port: "", Path: ""}, "https://host"}, + {Address{Scheme: "", Host: "host", Port: "80", Path: "/path"}, "http://host/path"}, + {Address{Scheme: "http", Host: "", Port: "1234", Path: ""}, "http://:1234"}, + {Address{Scheme: "", Host: "", Port: "", Path: ""}, ""}, + } { + actual := test.addr.String() + if actual != test.expected { + t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expected, actual) + } + } +} + +func TestKeyNormalization(t *testing.T) { + testCases := []struct { + input string + expect string + }{ + { + input: "http://host:1234/path", + expect: "http://host:1234/path", + }, + { + input: "HTTP://A/ABCDEF", + expect: "http://a/ABCDEF", + }, + { + input: "A/ABCDEF", + expect: "a/ABCDEF", + }, + { + input: "A:2015/Path", + expect: "a:2015/Path", + }, + { + input: ":80", + expect: ":80", + }, + { + input: ":443", + expect: ":443", + }, + { + input: ":1234", + expect: ":1234", + }, + { + input: "", + expect: "", + }, + { + input: ":", + expect: "", + }, + { + input: "[::]", + expect: "::", + }, + } + for i, tc := range testCases { + addr, err := ParseAddress(tc.input) + if err != nil { + t.Errorf("Test %d: Parsing address '%s': %v", i, tc.input, err) + continue + } + expect := tc.expect + if !caseSensitivePath { + // every other part of the address should be lowercased when normalized, + // so simply lower-case the whole thing to do case-insensitive comparison + // of the path as well + expect = strings.ToLower(expect) + } + if actual := addr.Normalize().Key(); actual != expect { + t.Errorf("Test %d: Normalized key for address '%s' was '%s' but expected '%s'", i, tc.input, actual, expect) + } + + } +} diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go new file mode 100644 index 0000000..0fdfcd5 --- /dev/null +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -0,0 +1,255 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httpcaddyfile + +import ( + "encoding/json" + "fmt" + "html" + "net/http" + "reflect" + + "github.com/caddyserver/caddy/caddyconfig" + "github.com/caddyserver/caddy/modules/caddyhttp" + "github.com/caddyserver/caddy/v2/modules/caddytls" +) + +func init() { + RegisterDirective("bind", parseBind) + RegisterDirective("root", parseRoot) + RegisterDirective("tls", parseTLS) + RegisterHandlerDirective("redir", parseRedir) +} + +func parseBind(h Helper) ([]ConfigValue, error) { + var lnHosts []string + for h.Next() { + lnHosts = append(lnHosts, h.RemainingArgs()...) + } + return h.NewBindAddresses(lnHosts), nil +} + +func parseRoot(h Helper) ([]ConfigValue, error) { + if !h.Next() { + return nil, h.ArgErr() + } + + matcherSet, ok, err := h.MatcherToken() + if err != nil { + return nil, err + } + if !ok { + // no matcher token; oops + h.Dispenser.Prev() + } + + if !h.NextArg() { + return nil, h.ArgErr() + } + root := h.Val() + if h.NextArg() { + return nil, h.ArgErr() + } + + varsHandler := caddyhttp.VarsMiddleware{"root": root} + route := caddyhttp.Route{ + HandlersRaw: []json.RawMessage{ + caddyconfig.JSONModuleObject(varsHandler, "handler", "vars", nil), + }, + } + if matcherSet != nil { + route.MatcherSetsRaw = []map[string]json.RawMessage{matcherSet} + } + + return h.NewVarsRoute(route), nil +} + +func parseTLS(h Helper) ([]ConfigValue, error) { + var configVals []ConfigValue + + cp := new(caddytls.ConnectionPolicy) + var fileLoader caddytls.FileLoader + var folderLoader caddytls.FolderLoader + var mgr caddytls.ACMEManagerMaker + var off bool + + for h.Next() { + // file certificate loader + firstLine := h.RemainingArgs() + switch len(firstLine) { + case 0: + case 1: + if firstLine[0] == "off" { + off = true + } else { + mgr.Email = firstLine[0] + } + case 2: + fileLoader = append(fileLoader, caddytls.CertKeyFilePair{ + Certificate: firstLine[0], + Key: firstLine[1], + // TODO: add tags, for enterprise module's certificate selection + }) + default: + return nil, h.ArgErr() + } + + var hasBlock bool + for h.NextBlock() { + hasBlock = true + + switch h.Val() { + + // connection policy + case "protocols": + args := h.RemainingArgs() + if len(args) == 0 { + return nil, h.SyntaxErr("one or two protocols") + } + if len(args) > 0 { + if _, ok := caddytls.SupportedProtocols[args[0]]; !ok { + return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[0]) + } + cp.ProtocolMin = args[0] + } + if len(args) > 1 { + if _, ok := caddytls.SupportedProtocols[args[1]]; !ok { + return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[1]) + } + cp.ProtocolMax = args[1] + } + case "ciphers": + for h.NextArg() { + if _, ok := caddytls.SupportedCipherSuites[h.Val()]; !ok { + return nil, h.Errf("Wrong cipher suite name or cipher suite not supported: '%s'", h.Val()) + } + cp.CipherSuites = append(cp.CipherSuites, h.Val()) + } + case "curves": + for h.NextArg() { + if _, ok := caddytls.SupportedCurves[h.Val()]; !ok { + return nil, h.Errf("Wrong curve name or curve not supported: '%s'", h.Val()) + } + cp.Curves = append(cp.Curves, h.Val()) + } + case "alpn": + args := h.RemainingArgs() + if len(args) == 0 { + return nil, h.ArgErr() + } + cp.ALPN = args + + // certificate folder loader + case "load": + folderLoader = append(folderLoader, h.RemainingArgs()...) + + // automation policy + case "ca": + arg := h.RemainingArgs() + if len(arg) != 1 { + return nil, h.ArgErr() + } + mgr.CA = arg[0] + + // TODO: other properties for automation manager + } + } + + // a naked tls directive is not allowed + if len(firstLine) == 0 && !hasBlock { + return nil, h.ArgErr() + } + } + + // connection policy + configVals = append(configVals, ConfigValue{ + Class: "tls.connection_policy", + Value: cp, + }) + + // certificate loaders + if len(fileLoader) > 0 { + configVals = append(configVals, ConfigValue{ + Class: "tls.certificate_loader", + Value: fileLoader, + }) + } + if len(folderLoader) > 0 { + configVals = append(configVals, ConfigValue{ + Class: "tls.certificate_loader", + Value: folderLoader, + }) + } + + // automation policy + if off { + configVals = append(configVals, ConfigValue{ + Class: "tls.off", + Value: true, + }) + } else if !reflect.DeepEqual(mgr, caddytls.ACMEManagerMaker{}) { + configVals = append(configVals, ConfigValue{ + Class: "tls.automation_manager", + Value: mgr, + }) + } + + return configVals, nil +} + +func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) { + if !h.Next() { + return nil, h.ArgErr() + } + + if !h.NextArg() { + return nil, h.ArgErr() + } + to := h.Val() + + var code string + if h.NextArg() { + code = h.Val() + } + if code == "permanent" { + code = "301" + } + if code == "temporary" || code == "" { + code = "307" + } + var body string + if code == "meta" { + // Script tag comes first since that will better imitate a redirect in the browser's + // history, but the meta tag is a fallback for most non-JS clients. + const metaRedir = `<!DOCTYPE html> +<html> + <head> + <title>Redirecting...</title> + <script>window.location.replace("%s");</script> + <meta http-equiv="refresh" content="0; URL='%s'"> + </head> + <body>Redirecting to <a href="%s">%s</a>...</body> +</html> +` + safeTo := html.EscapeString(to) + body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo) + } + + return caddyhttp.StaticResponse{ + StatusCode: caddyhttp.WeakString(code), + Headers: http.Header{"Location": []string{to}}, + Body: body, + }, nil +} diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go new file mode 100644 index 0000000..526ac87 --- /dev/null +++ b/caddyconfig/httpcaddyfile/directives.go @@ -0,0 +1,182 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httpcaddyfile + +import ( + "encoding/json" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +// defaultDirectiveOrder specifies the order +// to apply directives in HTTP routes. +// TODO: finish the ability to customize this +var defaultDirectiveOrder = []string{ + "rewrite", + "try_files", + "headers", + "encode", + "templates", + "redir", + "static_response", // TODO: "reply" or "respond"? + "reverse_proxy", + "file_server", +} + +// RegisterDirective registers a unique directive dir with an +// associated unmarshaling (setup) function. When directive dir +// is encountered in a Caddyfile, setupFunc will be called to +// unmarshal its tokens. +func RegisterDirective(dir string, setupFunc UnmarshalFunc) { + if _, ok := registeredDirectives[dir]; ok { + panic("directive " + dir + " already registered") + } + registeredDirectives[dir] = setupFunc +} + +// RegisterHandlerDirective is like RegisterDirective, but for +// directives which specifically output only an HTTP handler. +func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) { + RegisterDirective(dir, func(h Helper) ([]ConfigValue, error) { + if !h.Next() { + return nil, h.ArgErr() + } + + matcherSet, ok, err := h.MatcherToken() + if err != nil { + return nil, err + } + if ok { + h.Dispenser.Delete() // strip matcher token + } + + h.Dispenser.Reset() // pretend this lookahead never happened + val, err := setupFunc(h) + if err != nil { + return nil, err + } + + return h.NewRoute(matcherSet, val), nil + }) +} + +// Helper is a type which helps setup a value from +// Caddyfile tokens. +type Helper struct { + *caddyfile.Dispenser + warnings *[]caddyconfig.Warning + matcherDefs map[string]map[string]json.RawMessage +} + +// JSON converts val into JSON. Any errors are added to warnings. +func (h Helper) JSON(val interface{}, warnings *[]caddyconfig.Warning) json.RawMessage { + return caddyconfig.JSON(val, h.warnings) +} + +// MatcherToken assumes the current token is (possibly) a matcher, and +// if so, returns the matcher set along with a true value. If the current +// token is not a matcher, nil and false is returned. Note that a true +// value may be returned with a nil matcher set if it is a catch-all. +func (h Helper) MatcherToken() (map[string]json.RawMessage, bool, error) { + if !h.NextArg() { + return nil, false, nil + } + return matcherSetFromMatcherToken(h.Dispenser.Token(), h.matcherDefs, h.warnings) +} + +// NewRoute returns config values relevant to creating a new HTTP route. +func (h Helper) NewRoute(matcherSet map[string]json.RawMessage, + handler caddyhttp.MiddlewareHandler) []ConfigValue { + mod, err := caddy.GetModule(caddy.GetModuleName(handler)) + if err != nil { + // TODO: append to warnings + } + var matcherSetsRaw []map[string]json.RawMessage + if matcherSet != nil { + matcherSetsRaw = append(matcherSetsRaw, matcherSet) + } + return []ConfigValue{ + { + Class: "route", + Value: caddyhttp.Route{ + MatcherSetsRaw: matcherSetsRaw, + HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", mod.ID(), h.warnings)}, + }, + }, + } +} + +// NewBindAddresses returns config values relevant to adding +// listener bind addresses to the config. +func (h Helper) NewBindAddresses(addrs []string) []ConfigValue { + return []ConfigValue{{Class: "bind", Value: addrs}} +} + +// NewVarsRoute returns config values relevant to adding a +// "vars" wrapper route to the config. +func (h Helper) NewVarsRoute(route caddyhttp.Route) []ConfigValue { + return []ConfigValue{{Class: "var", Value: route}} +} + +// ConfigValue represents a value to be added to the final +// configuration, or a value to be consulted when building +// the final configuration. +type ConfigValue struct { + // The kind of value this is. As the config is + // being built, the adapter will look in the + // "pile" for values belonging to a certain + // class when it is setting up a certain part + // of the config. The associated value will be + // type-asserted and placed accordingly. + Class string + + // The value to be used when building the config. + // Generally its type is associated with the + // name of the Class. + Value interface{} + + directive string +} + +// serverBlock pairs a Caddyfile server block +// with a "pile" of config values, keyed by class +// name. +type serverBlock struct { + block caddyfile.ServerBlock + pile map[string][]ConfigValue // config values obtained from directives +} + +type ( + // UnmarshalFunc is a function which can unmarshal Caddyfile + // tokens into zero or more config values using a Helper type. + // These are passed in a call to RegisterDirective. + UnmarshalFunc func(h Helper) ([]ConfigValue, error) + + // UnmarshalHandlerFunc is like UnmarshalFunc, except the + // output of the unmarshaling is an HTTP handler. This + // function does not need to deal with HTTP request matching + // which is abstracted away. Since writing HTTP handlers + // with Caddyfile support is very common, this is a more + // convenient way to add a handler to the chain since a lot + // of the details common to HTTP handlers are taken care of + // for you. These are passed to a call to + // RegisterHandlerDirective. + UnmarshalHandlerFunc func(h Helper) (caddyhttp.MiddlewareHandler, error) +) + +var registeredDirectives = make(map[string]UnmarshalFunc) diff --git a/caddyconfig/httpcaddyfile/handlers.go b/caddyconfig/httpcaddyfile/handlers.go new file mode 100644 index 0000000..9a29e97 --- /dev/null +++ b/caddyconfig/httpcaddyfile/handlers.go @@ -0,0 +1,56 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httpcaddyfile + +import ( + "encoding/json" + "fmt" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +func (st *ServerType) parseMatcherDefinitions(d *caddyfile.Dispenser) (map[string]map[string]json.RawMessage, error) { + matchers := make(map[string]map[string]json.RawMessage) + for d.Next() { + definitionName := d.Val() + for d.NextBlock() { + matcherName := d.Val() + mod, err := caddy.GetModule("http.matchers." + matcherName) + if err != nil { + return nil, fmt.Errorf("getting matcher module '%s': %v", matcherName, err) + } + unm, ok := mod.New().(caddyfile.Unmarshaler) + if !ok { + return nil, fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName) + } + err = unm.UnmarshalCaddyfile(d.NewFromNextTokens()) + if err != nil { + return nil, err + } + rm, ok := unm.(caddyhttp.RequestMatcher) + if !ok { + return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName) + } + if _, ok := matchers[definitionName]; !ok { + matchers[definitionName] = make(map[string]json.RawMessage) + } + matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil) + } + } + return matchers, nil +} diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go new file mode 100644 index 0000000..42c1be5 --- /dev/null +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -0,0 +1,519 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httpcaddyfile + +import ( + "encoding/json" + "fmt" + "reflect" + "sort" + "strconv" + "strings" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/caddyserver/caddy/v2/modules/caddytls" + "github.com/mholt/certmagic" +) + +func init() { + caddyconfig.RegisterAdapter("caddyfile", caddyfile.Adapter{ServerType: ServerType{}}) +} + +// ServerType can set up a config from an HTTP Caddyfile. +type ServerType struct { +} + +// Setup makes a config from the tokens. +func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock, + options map[string]string) (*caddy.Config, []caddyconfig.Warning, error) { + var warnings []caddyconfig.Warning + + var serverBlocks []serverBlock + for _, sblock := range originalServerBlocks { + serverBlocks = append(serverBlocks, serverBlock{ + block: sblock, + pile: make(map[string][]ConfigValue), + }) + } + + for _, sb := range serverBlocks { + // replace shorthand placeholders (which are + // convenient when writing a Caddyfile) with + // their actual placeholder identifiers or + // variable names + replacer := strings.NewReplacer( + "{uri}", "{http.request.uri}", + "{path}", "{http.request.uri.path}", + "{host}", "{http.request.host}", + "{hostport}", "{http.request.hostport}", + "{method}", "{http.request.method}", + "{scheme}", "{http.request.scheme}", + "{file}", "{http.request.uri.path.file}", + "{dir}", "{http.request.uri.path.dir}", + "{query}", "{http.request.uri.query}", + ) + for _, segment := range sb.block.Segments { + for i := 0; i < len(segment); i++ { + segment[i].Text = replacer.Replace(segment[i].Text) + } + } + + // extract matcher definitions + d := sb.block.DispenseDirective("matcher") + matcherDefs, err := st.parseMatcherDefinitions(d) + if err != nil { + return nil, nil, err + } + + for _, segment := range sb.block.Segments { + dir := segment.Directive() + if dir == "matcher" { + // TODO: This is a special case because we pre-processed it; handle this better + continue + } + if dirFunc, ok := registeredDirectives[dir]; ok { + results, err := dirFunc(Helper{ + Dispenser: segment.NewDispenser(), + warnings: &warnings, + matcherDefs: matcherDefs, + }) + if err != nil { + return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err) + } + for _, result := range results { + result.directive = dir + sb.pile[result.Class] = append(sb.pile[result.Class], result) + } + } else { + tkn := segment[0] + return nil, warnings, fmt.Errorf("%s:%d: unrecognized directive: %s", tkn.File, tkn.Line, dir) + } + } + } + + // map + sbmap, err := st.mapAddressToServerBlocks(serverBlocks) + if err != nil { + return nil, warnings, err + } + + // reduce + pairings := st.consolidateAddrMappings(sbmap) + + // each pairing of listener addresses to list of server + // blocks is basically a server definition + servers, err := st.serversFromPairings(pairings, &warnings) + if err != nil { + return nil, warnings, err + } + + // now that each server is configured, make the HTTP app + httpApp := caddyhttp.App{ + HTTPPort: tryInt(options["http-port"], &warnings), + HTTPSPort: tryInt(options["https-port"], &warnings), + Servers: servers, + } + + // now for the TLS app! (TODO: refactor into own func) + tlsApp := caddytls.TLS{Certificates: make(map[string]json.RawMessage)} + for _, p := range pairings { + for _, sblock := range p.serverBlocks { + // tls automation policies + if mmVals, ok := sblock.pile["tls.automation_manager"]; ok { + for _, mmVal := range mmVals { + mm := mmVal.Value.(caddytls.ManagerMaker) + sblockHosts, err := st.autoHTTPSHosts(sblock) + if err != nil { + return nil, warnings, err + } + tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, caddytls.AutomationPolicy{ + Hosts: sblockHosts, + ManagementRaw: caddyconfig.JSONModuleObject(mm, "module", mm.(caddy.Module).CaddyModule().ID(), &warnings), + }) + } + } + + // tls certificate loaders + if clVals, ok := sblock.pile["tls.certificate_loader"]; ok { + for _, clVal := range clVals { + loader := clVal.Value.(caddytls.CertificateLoader) + loaderName := caddy.GetModuleName(loader) + tlsApp.Certificates[loaderName] = caddyconfig.JSON(loader, &warnings) + } + } + } + } + // consolidate automation policies that are the exact same + tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies) + + // annnd the top-level config, then we're done! + cfg := &caddy.Config{AppsRaw: make(map[string]json.RawMessage)} + if !reflect.DeepEqual(httpApp, caddyhttp.App{}) { + cfg.AppsRaw["http"] = caddyconfig.JSON(httpApp, &warnings) + } + if !reflect.DeepEqual(tlsApp, caddytls.TLS{}) { + cfg.AppsRaw["tls"] = caddyconfig.JSON(tlsApp, &warnings) + } + + return cfg, warnings, nil +} + +// hostsFromServerBlockKeys returns a list of all the +// hostnames found in the keys of the server block sb. +// The list may not be in a consistent order. +func (st *ServerType) hostsFromServerBlockKeys(sb caddyfile.ServerBlock) ([]string, error) { + // first get each unique hostname + hostMap := make(map[string]struct{}) + for _, sblockKey := range sb.Keys { + addr, err := ParseAddress(sblockKey) + if err != nil { + return nil, fmt.Errorf("parsing server block key: %v", err) + } + addr = addr.Normalize() + hostMap[addr.Host] = struct{}{} + } + + // convert map to slice + sblockHosts := make([]string, 0, len(hostMap)) + for host := range hostMap { + sblockHosts = append(sblockHosts, host) + } + + return sblockHosts, nil +} + +// serversFromPairings creates the servers for each pairing of addresses +// to server blocks. Each pairing is essentially a server definition. +func (st *ServerType) serversFromPairings(pairings []sbAddrAssociation, warnings *[]caddyconfig.Warning) (map[string]*caddyhttp.Server, error) { + servers := make(map[string]*caddyhttp.Server) + + for i, p := range pairings { + srv := &caddyhttp.Server{ + Listen: p.addresses, + } + + for _, sblock := range p.serverBlocks { + matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock.block) + if err != nil { + return nil, fmt.Errorf("server block %v: compiling matcher sets: %v", sblock.block.Keys, err) + } + + // if there are user-defined variables, then siteVarSubroute will + // wrap the handlerSubroute; otherwise handlerSubroute will be the + // site's primary subroute. + siteVarSubroute, handlerSubroute := new(caddyhttp.Subroute), new(caddyhttp.Subroute) + + // tls: connection policies and toggle auto HTTPS + + autoHTTPSQualifiedHosts, err := st.autoHTTPSHosts(sblock) + if err != nil { + return nil, err + } + if _, ok := sblock.pile["tls.off"]; ok { + // tls off: disable TLS (and automatic HTTPS) for server block's names + if srv.AutoHTTPS == nil { + srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig) + } + srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, autoHTTPSQualifiedHosts...) + } else if cpVals, ok := sblock.pile["tls.connection_policy"]; ok { + // tls connection policies + for _, cpVal := range cpVals { + cp := cpVal.Value.(*caddytls.ConnectionPolicy) + // only create if there is a non-empty policy + if !reflect.DeepEqual(cp, new(caddytls.ConnectionPolicy)) { + // make sure the policy covers all hostnames from the block + hosts, err := st.hostsFromServerBlockKeys(sblock.block) + if err != nil { + return nil, err + } + + // TODO: are matchers needed if every hostname of the config is matched? + cp.Matchers = map[string]json.RawMessage{ + "sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones + } + srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp) + } + } + // TODO: consolidate equal conn policies + } + + // vars: special routes that will have to wrap the normal handlers + // so that these variables can be used across their matchers too + for _, cfgVal := range sblock.pile["var"] { + siteVarSubroute.Routes = append(siteVarSubroute.Routes, cfgVal.Value.(caddyhttp.Route)) + } + + // set up each handler directive + dirRoutes := sblock.pile["route"] + // TODO: The ordering here depends on... if there is a list of + // directives to use, then sort by that, otherwise just use in + // the order they appear in the slice (which is the order they + // appeared in the Caddyfile) + sortByList := true + if sortByList { + dirPositions := make(map[string]int) + for i, dir := range defaultDirectiveOrder { + dirPositions[dir] = i + } + sort.SliceStable(dirRoutes, func(i, j int) bool { + iDir, jDir := dirRoutes[i].directive, dirRoutes[j].directive + return dirPositions[iDir] < dirPositions[jDir] + }) + } + for _, r := range dirRoutes { + handlerSubroute.Routes = append(handlerSubroute.Routes, r.Value.(caddyhttp.Route)) + } + + // the route that contains the site's handlers will + // be assumed to be the sub-route for this site... + siteSubroute := handlerSubroute + + // ... unless, of course, there are variables that might + // be used by the site's matchers or handlers, in which + // case we need to nest the handlers in a sub-sub-route, + // and the variables go in the sub-route so the variables + // get evaluated first + if len(siteVarSubroute.Routes) > 0 { + subSubRoute := caddyhttp.Subroute{Routes: siteSubroute.Routes} + siteSubroute.Routes = append( + siteVarSubroute.Routes, + caddyhttp.Route{ + HandlersRaw: []json.RawMessage{ + caddyconfig.JSONModuleObject(subSubRoute, "handler", "subroute", warnings), + }, + }, + ) + } + + siteSubroute.Routes = consolidateRoutes(siteSubroute.Routes) + + srv.Routes = append(srv.Routes, caddyhttp.Route{ + MatcherSetsRaw: matcherSetsEnc, + HandlersRaw: []json.RawMessage{ + caddyconfig.JSONModuleObject(siteSubroute, "handler", "subroute", warnings), + }, + }) + } + + srv.Routes = consolidateRoutes(srv.Routes) + + servers[fmt.Sprintf("srv%d", i)] = srv + } + + return servers, nil +} + +func (st ServerType) autoHTTPSHosts(sb serverBlock) ([]string, error) { + // get the hosts for this server block... + hosts, err := st.hostsFromServerBlockKeys(sb.block) + if err != nil { + return nil, err + } + // ...and of those, which ones qualify for auto HTTPS + var autoHTTPSQualifiedHosts []string + for _, h := range hosts { + if certmagic.HostQualifies(h) { + autoHTTPSQualifiedHosts = append(autoHTTPSQualifiedHosts, h) + } + } + return autoHTTPSQualifiedHosts, nil +} + +// consolidateRoutes combines routes with the same properties +// (same matchers, same Terminal and Group settings) for a +// cleaner overall output. +func consolidateRoutes(routes caddyhttp.RouteList) caddyhttp.RouteList { + for i := 0; i < len(routes)-1; i++ { + if reflect.DeepEqual(routes[i].MatcherSetsRaw, routes[i+1].MatcherSetsRaw) && + routes[i].Terminal == routes[i+1].Terminal && + routes[i].Group == routes[i+1].Group { + // keep the handlers in the same order, then splice out repetitive route + routes[i].HandlersRaw = append(routes[i].HandlersRaw, routes[i+1].HandlersRaw...) + routes = append(routes[:i+1], routes[i+2:]...) + i-- + } + } + return routes +} + +// consolidateAutomationPolicies combines automation policies that are the same, +// for a cleaner overall output. +func consolidateAutomationPolicies(aps []caddytls.AutomationPolicy) []caddytls.AutomationPolicy { + for i := 0; i < len(aps); i++ { + for j := 0; j < len(aps); j++ { + if j == i { + continue + } + if reflect.DeepEqual(aps[i].ManagementRaw, aps[j].ManagementRaw) { + aps[i].Hosts = append(aps[i].Hosts, aps[j].Hosts...) + } + aps = append(aps[:j], aps[j+1:]...) + i-- + break + } + } + return aps +} + +func matcherSetFromMatcherToken( + tkn caddyfile.Token, + matcherDefs map[string]map[string]json.RawMessage, + warnings *[]caddyconfig.Warning, +) (map[string]json.RawMessage, bool, error) { + // matcher tokens can be wildcards, simple path matchers, + // or refer to a pre-defined matcher by some name + if tkn.Text == "*" { + // match all requests == no matchers, so nothing to do + return nil, true, nil + } else if strings.HasPrefix(tkn.Text, "/") { + // convenient way to specify a single path match + return map[string]json.RawMessage{ + "path": caddyconfig.JSON(caddyhttp.MatchPath{tkn.Text}, warnings), + }, true, nil + } else if strings.HasPrefix(tkn.Text, "match:") { + // pre-defined matcher + matcherName := strings.TrimPrefix(tkn.Text, "match:") + m, ok := matcherDefs[matcherName] + if !ok { + return nil, false, fmt.Errorf("unrecognized matcher name: %+v", matcherName) + } + return m, true, nil + } + + return nil, false, nil +} + +func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([]map[string]json.RawMessage, error) { + type hostPathPair struct { + hostm caddyhttp.MatchHost + pathm caddyhttp.MatchPath + } + + // keep routes with common host and path matchers together + var matcherPairs []*hostPathPair + + for _, key := range sblock.Keys { + addr, err := ParseAddress(key) + if err != nil { + return nil, fmt.Errorf("server block %v: parsing and standardizing address '%s': %v", sblock.Keys, key, err) + } + addr = addr.Normalize() + + // choose a matcher pair that should be shared by this + // server block; if none exists yet, create one + var chosenMatcherPair *hostPathPair + for _, mp := range matcherPairs { + if (len(mp.pathm) == 0 && addr.Path == "") || + (len(mp.pathm) == 1 && mp.pathm[0] == addr.Path) { + chosenMatcherPair = mp + break + } + } + if chosenMatcherPair == nil { + chosenMatcherPair = new(hostPathPair) + if addr.Path != "" { + chosenMatcherPair.pathm = []string{addr.Path} + } + matcherPairs = append(matcherPairs, chosenMatcherPair) + } + + // add this server block's keys to the matcher + // pair if it doesn't already exist + if addr.Host != "" { + var found bool + for _, h := range chosenMatcherPair.hostm { + if h == addr.Host { + found = true + break + } + } + if !found { + chosenMatcherPair.hostm = append(chosenMatcherPair.hostm, addr.Host) + } + } + } + + // iterate each pairing of host and path matchers and + // put them into a map for JSON encoding + var matcherSets []map[string]caddyhttp.RequestMatcher + for _, mp := range matcherPairs { + matcherSet := make(map[string]caddyhttp.RequestMatcher) + if len(mp.hostm) > 0 { + matcherSet["host"] = mp.hostm + } + if len(mp.pathm) > 0 { + matcherSet["path"] = mp.pathm + } + if len(matcherSet) > 0 { + matcherSets = append(matcherSets, matcherSet) + } + } + + // finally, encode each of the matcher sets + var matcherSetsEnc []map[string]json.RawMessage + for _, ms := range matcherSets { + msEncoded, err := encodeMatcherSet(ms) + if err != nil { + return nil, fmt.Errorf("server block %v: %v", sblock.Keys, err) + } + matcherSetsEnc = append(matcherSetsEnc, msEncoded) + } + + return matcherSetsEnc, nil +} + +func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (map[string]json.RawMessage, error) { + msEncoded := make(map[string]json.RawMessage) + for matcherName, val := range matchers { + jsonBytes, err := json.Marshal(val) + if err != nil { + return nil, fmt.Errorf("marshaling matcher set %#v: %v", matchers, err) + } + msEncoded[matcherName] = jsonBytes + } + return msEncoded, nil +} + +// tryInt tries to convert str to an integer. If it fails, it downgrades +// the error to a warning and returns 0. +func tryInt(str string, warnings *[]caddyconfig.Warning) int { + if str == "" { + return 0 + } + val, err := strconv.Atoi(str) + if err != nil && warnings != nil { + *warnings = append(*warnings, caddyconfig.Warning{Message: err.Error()}) + } + return val +} + +type matcherSetAndTokens struct { + matcherSet map[string]json.RawMessage + tokens []caddyfile.Token +} + +// sbAddrAssocation is a mapping from a list of +// addresses to a list of server blocks that are +// served on those addresses. +type sbAddrAssociation struct { + addresses []string + serverBlocks []serverBlock +} + +// Interface guard +var _ caddyfile.ServerType = (*ServerType)(nil) diff --git a/cmd/caddy/main.go b/cmd/caddy/main.go index c1824d7..5f6b8bb 100644 --- a/cmd/caddy/main.go +++ b/cmd/caddy/main.go @@ -18,6 +18,7 @@ import ( caddycmd "github.com/caddyserver/caddy/v2/cmd" // this is where modules get plugged in + _ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/brotli" diff --git a/cmd/commands.go b/cmd/commands.go index e63e2d5..99ec642 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -31,6 +31,7 @@ import ( "strings" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/mholt/certmagic" "github.com/mitchellh/go-ps" ) @@ -38,6 +39,7 @@ import ( func cmdStart() (int, error) { startCmd := flag.NewFlagSet("start", flag.ExitOnError) startCmdConfigFlag := startCmd.String("config", "", "Configuration file") + startCmdConfigAdapterFlag := startCmd.String("config-adapter", "", "Name of config adapter to apply") startCmd.Parse(os.Args[2:]) // open a listener to which the child process will connect when @@ -62,6 +64,9 @@ func cmdStart() (int, error) { if *startCmdConfigFlag != "" { cmd.Args = append(cmd.Args, "--config", *startCmdConfigFlag) } + if *startCmdConfigAdapterFlag != "" { + cmd.Args = append(cmd.Args, "--config-adapter", *startCmdConfigAdapterFlag) + } stdinpipe, err := cmd.StdinPipe() if err != nil { return caddy.ExitCodeFailedStartup, @@ -137,7 +142,8 @@ func cmdStart() (int, error) { func cmdRun() (int, error) { runCmd := flag.NewFlagSet("run", flag.ExitOnError) runCmdConfigFlag := runCmd.String("config", "", "Configuration file") - runCmdPrintEnvFlag := runCmd.Bool("print-env", false, "Print environment (useful for debugging)") + runCmdConfigAdapterFlag := runCmd.String("config-adapter", "", "Name of config adapter to apply") + runCmdPrintEnvFlag := runCmd.Bool("print-env", false, "Print environment") runCmdPingbackFlag := runCmd.String("pingback", "", "Echo confirmation bytes to this address on success") runCmd.Parse(os.Args[2:]) @@ -149,16 +155,10 @@ func cmdRun() (int, error) { } } - // if a config file was specified for bootstrapping - // the server instance, load it now - var config []byte - if *runCmdConfigFlag != "" { - var err error - config, err = ioutil.ReadFile(*runCmdConfigFlag) - if err != nil { - return caddy.ExitCodeFailedStartup, - fmt.Errorf("reading config file: %v", err) - } + // get the config in caddy's native format + config, err := loadConfig(*runCmdConfigFlag, *runCmdConfigAdapterFlag) + if err != nil { + return caddy.ExitCodeFailedStartup, err } // set a fitting User-Agent for ACME requests @@ -167,7 +167,7 @@ func cmdRun() (int, error) { certmagic.UserAgent = "Caddy/" + cleanModVersion // start the admin endpoint along with any initial config - err := caddy.StartAdmin(config) + err = caddy.StartAdmin(config) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("starting caddy administration endpoint: %v", err) @@ -226,6 +226,7 @@ func cmdStop() (int, error) { func cmdReload() (int, error) { reloadCmd := flag.NewFlagSet("load", flag.ExitOnError) reloadCmdConfigFlag := reloadCmd.String("config", "", "Configuration file") + reloadCmdConfigAdapterFlag := reloadCmd.String("config-adapter", "", "Name of config adapter to apply") reloadCmdAddrFlag := reloadCmd.String("address", "", "Address of the administration listener, if different from config") reloadCmd.Parse(os.Args[2:]) @@ -235,11 +236,10 @@ func cmdReload() (int, error) { fmt.Errorf("no configuration to load (use --config)") } - // load the configuration file - config, err := ioutil.ReadFile(*reloadCmdConfigFlag) + // get the config in caddy's native format + config, err := loadConfig(*reloadCmdConfigFlag, *reloadCmdConfigAdapterFlag) if err != nil { - return caddy.ExitCodeFailedStartup, - fmt.Errorf("reading config file: %v", err) + return caddy.ExitCodeFailedStartup, err } // get the address of the admin listener and craft endpoint URL @@ -306,3 +306,52 @@ func cmdEnviron() (int, error) { } return caddy.ExitCodeSuccess, nil } + +func cmdAdaptConfig() (int, error) { + adaptCmd := flag.NewFlagSet("adapt", flag.ExitOnError) + adaptCmdAdapterFlag := adaptCmd.String("adapter", "", "Name of config adapter") + adaptCmdInputFlag := adaptCmd.String("input", "", "Configuration file to adapt") + adaptCmdPrettyFlag := adaptCmd.Bool("pretty", false, "Format the output for human readability") + adaptCmd.Parse(os.Args[2:]) + + if *adaptCmdAdapterFlag == "" || *adaptCmdInputFlag == "" { + return caddy.ExitCodeFailedStartup, + fmt.Errorf("usage: caddy adapt-config --adapter <name> --input <file>") + } + + cfgAdapter := caddyconfig.GetAdapter(*adaptCmdAdapterFlag) + if cfgAdapter == nil { + return caddy.ExitCodeFailedStartup, + fmt.Errorf("unrecognized config adapter: %s", *adaptCmdAdapterFlag) + } + + input, err := ioutil.ReadFile(*adaptCmdInputFlag) + if err != nil { + return caddy.ExitCodeFailedStartup, + fmt.Errorf("reading input file: %v", err) + } + + opts := make(map[string]string) + if *adaptCmdPrettyFlag { + opts["pretty"] = "true" + } + + adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts) + if err != nil { + return caddy.ExitCodeFailedStartup, err + } + + // print warnings to stderr + for _, warn := range warnings { + msg := warn.Message + if warn.Directive != "" { + msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message) + } + log.Printf("[WARNING][%s] %s:%d: %s", *adaptCmdAdapterFlag, warn.File, warn.Line, msg) + } + + // print result to stdout + fmt.Println(string(adaptedConfig)) + + return caddy.ExitCodeSuccess, nil +} diff --git a/cmd/main.go b/cmd/main.go index 16d065b..e0a3686 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -25,6 +25,7 @@ import ( "os" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" ) // Main implements the main function of the caddy command. @@ -62,6 +63,7 @@ var commands = map[string]commandFunc{ "version": cmdVersion, "list-modules": cmdListModules, "environ": cmdEnviron, + "adapt-config": cmdAdaptConfig, } func usageString() string { @@ -85,3 +87,66 @@ func handlePingbackConn(conn net.Conn, expect []byte) error { } return nil } + +// loadConfig loads the config from configFile and adapts it +// using adapterName. If adapterName is specified, configFile +// must be also. It prints any warnings to stderr, and returns +// the resulting JSON config bytes. +func loadConfig(configFile, adapterName string) ([]byte, error) { + // specifying an adapter without a config file is ambiguous + if configFile == "" && adapterName != "" { + return nil, fmt.Errorf("cannot adapt config without config file (use --config)") + } + + // load initial config and adapter + var config []byte + var cfgAdapter caddyconfig.Adapter + var err error + if configFile != "" { + config, err = ioutil.ReadFile(configFile) + if err != nil { + return nil, fmt.Errorf("reading config file: %v", err) + } + } else if adapterName == "" { + // as a special case when no config file or adapter + // is specified, see if the Caddyfile adapter is + // plugged in, and if so, try using a default Caddyfile + cfgAdapter = caddyconfig.GetAdapter("caddyfile") + if cfgAdapter != nil { + config, err = ioutil.ReadFile("Caddyfile") + if err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("reading default Caddyfile: %v", err) + } + configFile = "Caddyfile" + } + } + + // load config adapter + if adapterName != "" { + cfgAdapter = caddyconfig.GetAdapter(adapterName) + if cfgAdapter == nil { + return nil, fmt.Errorf("unrecognized config adapter: %s", adapterName) + } + } + + // adapt config + if cfgAdapter != nil { + adaptedConfig, warnings, err := cfgAdapter.Adapt(config, map[string]string{ + "filename": configFile, + // TODO: all other options... (http-port, etc...) + }) + if err != nil { + return nil, fmt.Errorf("adapting config using %s: %v", adapterName, err) + } + for _, warn := range warnings { + msg := warn.Message + if warn.Directive != "" { + msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message) + } + fmt.Printf("[WARNING][%s] %s:%d: %s", adapterName, warn.File, warn.Line, msg) + } + config = adaptedConfig + } + + return config, nil +} @@ -99,11 +99,16 @@ func (ctx Context) LoadModule(name string, rawMsg json.RawMessage) (interface{}, return nil, fmt.Errorf("module '%s' has no constructor", mod.Name) } - val := mod.New() + val := mod.New().(interface{}) - // value must be a pointer for unmarshaling into concrete type + // value must be a pointer for unmarshaling into concrete type, even if + // the module's concrete type is a slice or map; New() *should* return + // a pointer, otherwise unmarshaling errors or panics will occur if rv := reflect.ValueOf(val); rv.Kind() != reflect.Ptr { - val = reflect.New(rv.Type()).Elem().Addr().Interface() + log.Printf("[WARNING] ModuleInfo.New() for module '%s' did not return a pointer,"+ + " so we are using reflection to make a pointer instead; please fix this by"+ + " using new(Type) or &Type notation in your module's New() function.", name) + val = reflect.New(rv.Type()).Elem().Addr().Interface().(Module) } // fill in its config only if there is a config to fill in @@ -3,20 +3,22 @@ module github.com/caddyserver/caddy/v2 go 1.12 require ( - github.com/DataDog/zstd v1.4.0 // indirect + github.com/DataDog/zstd v1.4.1 // indirect github.com/Masterminds/goutils v1.1.0 // indirect github.com/Masterminds/semver v1.4.2 // indirect github.com/Masterminds/sprig v2.20.0+incompatible github.com/andybalholm/brotli v0.0.0-20190704151324-71eb68cc467c + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.0 github.com/go-acme/lego v2.6.0+incompatible - github.com/google/go-cmp v0.3.0 // indirect + github.com/google/go-cmp v0.3.1 // indirect github.com/google/uuid v1.1.1 // indirect github.com/huandu/xstrings v1.2.0 // indirect github.com/imdario/mergo v0.3.7 // indirect github.com/klauspost/compress v1.7.1-0.20190613161414-0b31f265a57b github.com/klauspost/cpuid v1.2.1 - github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2 + github.com/kr/pretty v0.1.0 // indirect + github.com/mholt/certmagic v0.6.2 github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936 github.com/rs/cors v1.6.0 github.com/russross/blackfriday/v2 v2.0.1 @@ -24,6 +26,8 @@ require ( github.com/starlight-go/starlight v0.0.0-20181207205707-b06f321544f3 go.starlark.net v0.0.0-20190604130855-6ddc71c0ba77 golang.org/x/net v0.0.0-20190603091049-60506f45cf65 + golang.org/x/sys v0.0.0-20190422165155-953cdadca894 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v2 v2.2.2 // indirect ) @@ -1,5 +1,5 @@ -github.com/DataDog/zstd v1.4.0 h1:vhoV+DUHnRZdKW1i5UMjAk2G4JY8wN4ayRfYDNdEhwo= -github.com/DataDog/zstd v1.4.0/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM= +github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= @@ -12,6 +12,8 @@ github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1q github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/go-acme/lego v2.5.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M= @@ -19,8 +21,8 @@ github.com/go-acme/lego v2.6.0+incompatible h1:KxcEWOF5hKtgou4xIqPaXSRF9DoO4OJ90 github.com/go-acme/lego v2.6.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M= github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721 h1:KRMr9A3qfbVM7iV/WcLY/rL5LICqwMHLhwRXKu99fXw= github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4= -github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= @@ -32,8 +34,13 @@ github.com/klauspost/compress v1.7.1-0.20190613161414-0b31f265a57b/go.mod h1:RyI github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2 h1:xKE9kZ5C8gelJC3+BNM6LJs1x21rivK7yxfTZMAuY2s= -github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mholt/certmagic v0.6.2 h1:yy9cKm3rtxdh12SW4E51lzG3Eo6N59LEOfBQ0CTnMms= +github.com/mholt/certmagic v0.6.2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY= github.com/miekg/dns v1.1.3 h1:1g0r1IvskvgL8rR+AcHzUA+oFmGcQlaIm4IqakufeMM= github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936 h1:kw1v0NlnN+GZcU8Ma8CLF2Zzgjfx95gs3/GN3vYAPpo= @@ -64,12 +71,16 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/square/go-jose.v2 v2.2.2 h1:orlkJ3myw8CN1nVQHBFfloD+L3egixIa4FvUP6RosSA= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= @@ -23,28 +23,76 @@ import ( "sync" ) -// Module represents a Caddy module. -type Module struct { +// Module is a type that is used as a Caddy module. +type Module interface { + // This method indicates the type is a Caddy + // module. The returned ModuleInfo must have + // both a name and a constructor function. + // This method must not have any side-effects. + CaddyModule() ModuleInfo +} + +// ModuleInfo represents a registered Caddy module. +type ModuleInfo struct { // Name is the full name of the module. It // must be unique and properly namespaced. Name string - // New returns a new, empty instance of - // the module's type. The host module - // which loads this module will likely - // invoke methods on the returned value. - // It must return a pointer; if not, it - // is converted into one. - New func() interface{} + // New returns a pointer to a new, empty + // instance of the module's type. The host + // module which instantiates this module will + // likely type-assert and invoke methods on + // the returned value. This function must not + // have any side-effects. + New func() Module } -func (m Module) String() string { return m.Name } +// Namespace returns the module's namespace (scope) +// which is all but the last element of its name. +// If there is no explicit namespace in the name, +// the whole name is considered the namespace. +func (mi ModuleInfo) Namespace() string { + lastDot := strings.LastIndex(mi.Name, ".") + if lastDot < 0 { + return mi.Name + } + return mi.Name[:lastDot] +} + +// ID returns a module's ID, which is the +// last element of its name. +func (mi ModuleInfo) ID() string { + if mi.Name == "" { + return "" + } + parts := strings.Split(mi.Name, ".") + return parts[len(parts)-1] +} -// RegisterModule registers a module. Modules must call -// this function in the init phase of runtime. -func RegisterModule(mod Module) error { - if mod.Name == "caddy" { - return fmt.Errorf("modules cannot be named 'caddy'") +func (mi ModuleInfo) String() string { return mi.Name } + +// RegisterModule registers a module by receiving a +// plain/empty value of the module. For registration to +// be properly recorded, this should be called in the +// init phase of runtime. Typically, the module package +// will do this as a side-effect of being imported. +// This function returns an error if the module's info +// is incomplete or invalid, or if the module is +// already registered. +func RegisterModule(instance Module) error { + mod := instance.CaddyModule() + + if mod.Name == "" { + return fmt.Errorf("missing ModuleInfo.Name") + } + if mod.Name == "caddy" || mod.Name == "admin" { + return fmt.Errorf("module name '%s' is reserved", mod.Name) + } + if mod.New == nil { + return fmt.Errorf("missing ModuleInfo.New") + } + if val := mod.New(); val == nil { + return fmt.Errorf("ModuleInfo.New must return a non-nil module instance") } modulesMu.Lock() @@ -57,18 +105,27 @@ func RegisterModule(mod Module) error { return nil } -// GetModule returns a module by its full name. -func GetModule(name string) (Module, error) { +// GetModule returns module information from its full name. +func GetModule(name string) (ModuleInfo, error) { modulesMu.Lock() defer modulesMu.Unlock() - m, ok := modules[name] if !ok { - return Module{}, fmt.Errorf("module not registered: %s", name) + return ModuleInfo{}, fmt.Errorf("module not registered: %s", name) } return m, nil } +// GetModuleName returns a module's name from an instance of its value. +// If the value is not a module, an empty name will be returned. +func GetModuleName(instance interface{}) string { + var name string + if mod, ok := instance.(Module); ok { + name = mod.CaddyModule().Name + } + return name +} + // GetModules returns all modules in the given scope/namespace. // For example, a scope of "foo" returns modules named "foo.bar", // "foo.loo", but not "bar", "foo.bar.loo", etc. An empty scope @@ -78,7 +135,7 @@ func GetModule(name string) (Module, error) { // // Because modules are registered to a map, the returned slice // will be sorted to keep it deterministic. -func GetModules(scope string) []Module { +func GetModules(scope string) []ModuleInfo { modulesMu.Lock() defer modulesMu.Unlock() @@ -90,7 +147,7 @@ func GetModules(scope string) []Module { scopeParts = []string{} } - var mods []Module + var mods []ModuleInfo iterateModules: for name, m := range modules { modParts := strings.Split(name, ".") @@ -203,6 +260,6 @@ func strictUnmarshalJSON(data []byte, v interface{}) error { } var ( - modules = make(map[string]Module) + modules = make(map[string]ModuleInfo) modulesMu sync.Mutex ) diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index 467b40f..b4b1ec6 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -15,9 +15,12 @@ package caddyhttp import ( + "bytes" "context" "crypto/tls" + "encoding/json" "fmt" + "io" "log" weakrand "math/rand" "net" @@ -34,10 +37,7 @@ import ( func init() { weakrand.Seed(time.Now().UnixNano()) - err := caddy.RegisterModule(caddy.Module{ - Name: "http", - New: func() interface{} { return new(App) }, - }) + err := caddy.RegisterModule(App{}) if err != nil { log.Fatal(err) } @@ -55,6 +55,14 @@ type App struct { ctx caddy.Context } +// CaddyModule returns the Caddy module information. +func (App) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http", + New: func() caddy.Module { return new(App) }, + } +} + // Provision sets up the app. func (app *App) Provision(ctx caddy.Context) error { app.ctx = ctx @@ -224,7 +232,7 @@ func (app *App) automaticHTTPS() error { // find all qualifying domain names, de-duplicated domainSet := make(map[string]struct{}) for _, route := range srv.Routes { - for _, matcherSet := range route.matcherSets { + for _, matcherSet := range route.MatcherSets { for _, m := range matcherSet { if hm, ok := m.(*MatchHost); ok { for _, d := range *hm { @@ -244,6 +252,14 @@ func (app *App) automaticHTTPS() error { for d := range domainSet { domains = append(domains, d) if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) { + // if a certificate for this name is already loaded, + // don't obtain another one for it, unless we are + // supposed to ignore loaded certificates + if !srv.AutoHTTPS.IgnoreLoadedCerts && + len(tlsApp.CertificatesWithSAN(d)) > 0 { + log.Printf("[INFO][%s] Skipping automatic certificate management because a certificate with that SAN is already loaded", d) + continue + } domainsForCerts = append(domainsForCerts, d) } } @@ -319,16 +335,16 @@ func (app *App) automaticHTTPS() error { } redirTo += "{http.request.uri}" - redirRoutes = append(redirRoutes, ServerRoute{ - matcherSets: []MatcherSet{ + redirRoutes = append(redirRoutes, Route{ + MatcherSets: []MatcherSet{ { MatchProtocol("http"), MatchHost(domains), }, }, - handlers: []MiddlewareHandler{ + Handlers: []MiddlewareHandler{ StaticResponse{ - StatusCode: weakString(strconv.Itoa(http.StatusTemporaryRedirect)), // TODO: use permanent redirect instead + StatusCode: WeakString(strconv.Itoa(http.StatusTemporaryRedirect)), // TODO: use permanent redirect instead Headers: http.Header{ "Location": []string{redirTo}, "Connection": []string{"close"}, @@ -431,6 +447,77 @@ type MiddlewareHandler interface { // emptyHandler is used as a no-op handler. var emptyHandler HandlerFunc = func(http.ResponseWriter, *http.Request) error { return nil } +// WeakString is a type that unmarshals any JSON value +// as a string literal, with the following exceptions: +// 1) actual string values are decoded as strings, and +// 2) null is decoded as empty string +// and provides methods for getting the value as various +// primitive types. However, using this type removes any +// type safety as far as deserializing JSON is concerned. +type WeakString string + +// UnmarshalJSON satisfies json.Unmarshaler according to +// this type's documentation. +func (ws *WeakString) UnmarshalJSON(b []byte) error { + if len(b) == 0 { + return io.EOF + } + if b[0] == byte('"') && b[len(b)-1] == byte('"') { + var s string + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *ws = WeakString(s) + return nil + } + if bytes.Equal(b, []byte("null")) { + return nil + } + *ws = WeakString(b) + return nil +} + +// MarshalJSON marshals was a boolean if true or false, +// a number if an integer, or a string otherwise. +func (ws WeakString) MarshalJSON() ([]byte, error) { + if ws == "true" { + return []byte("true"), nil + } + if ws == "false" { + return []byte("false"), nil + } + if num, err := strconv.Atoi(string(ws)); err == nil { + return json.Marshal(num) + } + return json.Marshal(string(ws)) +} + +// Int returns ws as an integer. If ws is not an +// integer, 0 is returned. +func (ws WeakString) Int() int { + num, _ := strconv.Atoi(string(ws)) + return num +} + +// Float64 returns ws as a float64. If ws is not a +// float value, the zero value is returned. +func (ws WeakString) Float64() float64 { + num, _ := strconv.ParseFloat(string(ws), 64) + return num +} + +// Bool returns ws as a boolean. If ws is not a +// boolean, false is returned. +func (ws WeakString) Bool() bool { + return string(ws) == "true" +} + +// String returns ws as a string. +func (ws WeakString) String() string { + return string(ws) +} + const ( // DefaultHTTPPort is the default port for HTTP. DefaultHTTPPort = 80 diff --git a/modules/caddyhttp/caddylog/log.go b/modules/caddyhttp/caddylog/log.go index 902f60f..3f636d1 100644 --- a/modules/caddyhttp/caddylog/log.go +++ b/modules/caddyhttp/caddylog/log.go @@ -24,10 +24,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.log", - New: func() interface{} { return new(Log) }, - }) + caddy.RegisterModule(Log{}) } // Log implements a simple logging middleware. @@ -36,6 +33,14 @@ type Log struct { counter int } +// CaddyModule returns the Caddy module information. +func (Log) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.log", + New: func() caddy.Module { return new(Log) }, + } +} + func (l *Log) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { start := time.Now() diff --git a/modules/caddyhttp/encode/brotli/brotli.go b/modules/caddyhttp/encode/brotli/brotli.go index 0890d43..cf055aa 100644 --- a/modules/caddyhttp/encode/brotli/brotli.go +++ b/modules/caddyhttp/encode/brotli/brotli.go @@ -16,17 +16,16 @@ package caddybrotli import ( "fmt" + "strconv" "github.com/andybalholm/brotli" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.encoders.brotli", - New: func() interface{} { return new(Brotli) }, - }) + caddy.RegisterModule(Brotli{}) } // Brotli can create brotli encoders. Note that brotli @@ -35,6 +34,30 @@ type Brotli struct { Quality *int `json:"quality,omitempty"` } +// CaddyModule returns the Caddy module information. +func (Brotli) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.encoders.brotli", + New: func() caddy.Module { return new(Brotli) }, + } +} + +// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. +func (b *Brotli) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if !d.NextArg() { + continue + } + qualityStr := d.Val() + quality, err := strconv.Atoi(qualityStr) + if err != nil { + return err + } + b.Quality = &quality + } + return nil +} + // Validate validates b's configuration. func (b Brotli) Validate() error { if b.Quality != nil { @@ -64,6 +87,7 @@ func (b Brotli) NewEncoder() encode.Encoder { // Interface guards var ( - _ encode.Encoding = (*Brotli)(nil) - _ caddy.Validator = (*Brotli)(nil) + _ encode.Encoding = (*Brotli)(nil) + _ caddy.Validator = (*Brotli)(nil) + _ caddyfile.Unmarshaler = (*Brotli)(nil) ) diff --git a/modules/caddyhttp/encode/caddyfile.go b/modules/caddyhttp/encode/caddyfile.go new file mode 100644 index 0000000..5762bd3 --- /dev/null +++ b/modules/caddyhttp/encode/caddyfile.go @@ -0,0 +1,99 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package encode + +import ( + "encoding/json" + "fmt" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +func init() { + httpcaddyfile.RegisterHandlerDirective("encode", parseCaddyfile) +} + +// TODO: This is a good example of why UnmarshalCaddyfile is still a good idea... hmm. +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + enc := new(Encode) + err := enc.UnmarshalCaddyfile(h.Dispenser) + if err != nil { + return nil, err + } + return enc, nil +} + +// TODO: Keep UnmarshalCaddyfile pattern? + +// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: +// +// encode [<matcher>] <formats...> { +// gzip [<level>] +// zstd +// brotli [<quality>] +// } +// +// Specifying the formats on the first line will use those formats' defaults. +func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + for _, arg := range d.RemainingArgs() { + mod, err := caddy.GetModule("http.encoders." + arg) + if err != nil { + return fmt.Errorf("finding encoder module '%s': %v", mod.Name, err) + } + encoding, ok := mod.New().(Encoding) + if !ok { + return fmt.Errorf("module %s is not an HTTP encoding", mod.Name) + } + if enc.EncodingsRaw == nil { + enc.EncodingsRaw = make(map[string]json.RawMessage) + } + enc.EncodingsRaw[arg] = caddyconfig.JSON(encoding, nil) + } + + for d.NextBlock() { + name := d.Val() + mod, err := caddy.GetModule("http.encoders." + name) + if err != nil { + return fmt.Errorf("getting encoder module '%s': %v", mod.Name, err) + } + unm, ok := mod.New().(caddyfile.Unmarshaler) + if !ok { + return fmt.Errorf("encoder module '%s' is not a Caddyfile unmarshaler", mod.Name) + } + err = unm.UnmarshalCaddyfile(d.NewFromNextTokens()) + if err != nil { + return err + } + encoding, ok := unm.(Encoding) + if !ok { + return fmt.Errorf("module %s is not an HTTP encoding", mod.Name) + } + if enc.EncodingsRaw == nil { + enc.EncodingsRaw = make(map[string]json.RawMessage) + } + enc.EncodingsRaw[name] = caddyconfig.JSON(encoding, nil) + } + } + + return nil +} + +// Interface guard +var _ caddyfile.Unmarshaler = (*Encode)(nil) diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go index b2c1327..723b988 100644 --- a/modules/caddyhttp/encode/encode.go +++ b/modules/caddyhttp/encode/encode.go @@ -35,10 +35,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.encode", - New: func() interface{} { return new(Encode) }, - }) + caddy.RegisterModule(Encode{}) } // Encode is a middleware which can encode responses. @@ -50,21 +47,25 @@ type Encode struct { writerPools map[string]*sync.Pool // TODO: these pools do not get reused through config reloads... } +// CaddyModule returns the Caddy module information. +func (Encode) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.encode", + New: func() caddy.Module { return new(Encode) }, + } +} + // Provision provisions enc. func (enc *Encode) Provision(ctx caddy.Context) error { - enc.writerPools = make(map[string]*sync.Pool) - for modName, rawMsg := range enc.EncodingsRaw { val, err := ctx.LoadModule("http.encoders."+modName, rawMsg) if err != nil { return fmt.Errorf("loading encoder module '%s': %v", modName, err) } - encoder := val.(Encoding) - - enc.writerPools[encoder.AcceptEncoding()] = &sync.Pool{ - New: func() interface{} { - return encoder.NewEncoder() - }, + encoding := val.(Encoding) + err = enc.addEncoding(encoding) + if err != nil { + return err } } enc.EncodingsRaw = nil // allow GC to deallocate - TODO: Does this help? @@ -85,10 +86,28 @@ func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh defer w.(*responseWriter).Close() break } - return next.ServeHTTP(w, r) } +func (enc *Encode) addEncoding(e Encoding) error { + ae := e.AcceptEncoding() + if ae == "" { + return fmt.Errorf("encoder does not specify an Accept-Encoding value") + } + if _, ok := enc.writerPools[ae]; ok { + return fmt.Errorf("encoder already added: %s", ae) + } + if enc.writerPools == nil { + enc.writerPools = make(map[string]*sync.Pool) + } + enc.writerPools[ae] = &sync.Pool{ + New: func() interface{} { + return e.NewEncoder() + }, + } + return nil +} + // openResponseWriter creates a new response writer that may (or may not) // encode the response with encodingName. The returned response writer MUST // be closed after the handler completes. diff --git a/modules/caddyhttp/encode/gzip/gzip.go b/modules/caddyhttp/encode/gzip/gzip.go index 45c5f54..d6d67f7 100644 --- a/modules/caddyhttp/encode/gzip/gzip.go +++ b/modules/caddyhttp/encode/gzip/gzip.go @@ -18,16 +18,15 @@ import ( "compress/flate" "compress/gzip" // TODO: consider using https://github.com/klauspost/compress/gzip "fmt" + "strconv" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.encoders.gzip", - New: func() interface{} { return new(Gzip) }, - }) + caddy.RegisterModule(Gzip{}) } // Gzip can create gzip encoders. @@ -35,6 +34,30 @@ type Gzip struct { Level int `json:"level,omitempty"` } +// CaddyModule returns the Caddy module information. +func (Gzip) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.encoders.gzip", + New: func() caddy.Module { return new(Gzip) }, + } +} + +// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. +func (g *Gzip) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if !d.NextArg() { + continue + } + levelStr := d.Val() + level, err := strconv.Atoi(levelStr) + if err != nil { + return err + } + g.Level = level + } + return nil +} + // Provision provisions g's configuration. func (g *Gzip) Provision(ctx caddy.Context) error { if g.Level == 0 { @@ -69,7 +92,8 @@ var defaultGzipLevel = 5 // Interface guards var ( - _ encode.Encoding = (*Gzip)(nil) - _ caddy.Provisioner = (*Gzip)(nil) - _ caddy.Validator = (*Gzip)(nil) + _ encode.Encoding = (*Gzip)(nil) + _ caddy.Provisioner = (*Gzip)(nil) + _ caddy.Validator = (*Gzip)(nil) + _ caddyfile.Unmarshaler = (*Gzip)(nil) ) diff --git a/modules/caddyhttp/encode/zstd/zstd.go b/modules/caddyhttp/encode/zstd/zstd.go index acebff5..f2b4e85 100644 --- a/modules/caddyhttp/encode/zstd/zstd.go +++ b/modules/caddyhttp/encode/zstd/zstd.go @@ -16,20 +16,31 @@ package caddyzstd import ( "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" "github.com/klauspost/compress/zstd" ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.encoders.zstd", - New: func() interface{} { return new(Zstd) }, - }) + caddy.RegisterModule(Zstd{}) } // Zstd can create Zstandard encoders. type Zstd struct{} +// CaddyModule returns the Caddy module information. +func (Zstd) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.encoders.zstd", + New: func() caddy.Module { return new(Zstd) }, + } +} + +// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. +func (z *Zstd) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + return nil +} + // AcceptEncoding returns the name of the encoding as // used in the Accept-Encoding request headers. func (Zstd) AcceptEncoding() string { return "zstd" } @@ -40,5 +51,8 @@ func (z Zstd) NewEncoder() encode.Encoder { return writer } -// Interface guard -var _ encode.Encoding = (*Zstd)(nil) +// Interface guards +var ( + _ encode.Encoding = (*Zstd)(nil) + _ caddyfile.Unmarshaler = (*Zstd)(nil) +) diff --git a/modules/caddyhttp/fileserver/caddyfile.go b/modules/caddyhttp/fileserver/caddyfile.go new file mode 100644 index 0000000..6fa94e7 --- /dev/null +++ b/modules/caddyhttp/fileserver/caddyfile.go @@ -0,0 +1,104 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fileserver + +import ( + "encoding/json" + + "github.com/caddyserver/caddy/modules/caddyhttp/rewrite" + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +func init() { + httpcaddyfile.RegisterHandlerDirective("file_server", parseCaddyfile) + httpcaddyfile.RegisterDirective("try_files", parseTryFiles) +} + +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + var fsrv FileServer + + for h.Next() { + args := h.RemainingArgs() + switch len(args) { + case 0: + case 1: + if args[0] != "browse" { + return nil, h.ArgErr() + } + fsrv.Browse = new(Browse) + default: + return nil, h.ArgErr() + } + + for h.NextBlock() { + switch h.Val() { + case "hide": + fsrv.Hide = h.RemainingArgs() + if len(fsrv.Hide) == 0 { + return nil, h.ArgErr() + } + case "index": + fsrv.IndexNames = h.RemainingArgs() + if len(fsrv.Hide) == 0 { + return nil, h.ArgErr() + } + case "root": + if !h.Args(&fsrv.Root) { + return nil, h.ArgErr() + } + case "browse": + if fsrv.Browse != nil { + return nil, h.Err("browsing is already configured") + } + fsrv.Browse = new(Browse) + h.Args(&fsrv.Browse.TemplateFile) + default: + return nil, h.Errf("unknown subdirective '%s'", h.Val()) + } + } + } + + // if no root was configured explicitly, use site root + if fsrv.Root == "" { + fsrv.Root = "{http.var.root}" + } + + return &fsrv, nil +} + +func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { + if !h.Next() { + return nil, h.ArgErr() + } + + try := h.RemainingArgs() + if len(try) == 0 { + return nil, h.ArgErr() + } + + handler := rewrite.Rewrite{ + URI: "{http.matchers.file.relative}{http.request.uri.query}", + } + + matcherSet := map[string]json.RawMessage{ + "file": h.JSON(MatchFile{ + Root: "{http.var.root}", + TryFiles: try, + }, nil), + } + + return h.NewRoute(matcherSet, handler), nil +} diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go index 17d5c11..b091250 100644 --- a/modules/caddyhttp/fileserver/matcher.go +++ b/modules/caddyhttp/fileserver/matcher.go @@ -21,14 +21,12 @@ import ( "time" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.file", - New: func() interface{} { return new(MatchFile) }, - }) + caddy.RegisterModule(MatchFile{}) } // MatchFile is an HTTP request matcher that can match @@ -51,6 +49,50 @@ type MatchFile struct { TryPolicy string `json:"try_policy,omitempty"` } +// CaddyModule returns the Caddy module information. +func (MatchFile) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.file", + New: func() caddy.Module { return new(MatchFile) }, + } +} + +// UnmarshalCaddyfile sets up the matcher from Caddyfile tokens. Syntax: +// +// file { +// root <path> +// try_files <files...> +// try_policy first_exist|smallest_size|largest_size|most_recent_modified +// } +// +func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + for d.NextBlock() { + switch d.Val() { + case "root": + if !d.NextArg() { + return d.ArgErr() + } + m.Root = d.Val() + case "try_files": + m.TryFiles = d.RemainingArgs() + if len(m.TryFiles) == 0 { + return d.ArgErr() + } + case "try_policy": + if !d.NextArg() { + return d.ArgErr() + } + m.TryPolicy = d.Val() + } + } + } + if m.Root == "" { + m.Root = "{http.var.root}" + } + return nil +} + // Validate ensures m has a valid configuration. func (m MatchFile) Validate() error { switch m.TryPolicy { @@ -87,7 +129,7 @@ func (m MatchFile) Match(r *http.Request) bool { func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) { repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) - root := repl.ReplaceAll(m.Root, "") + root := repl.ReplaceAll(m.Root, ".") // if list of files to try was omitted entirely, // assume URL path diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index a66b753..cdac453 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -36,10 +36,7 @@ import ( func init() { weakrand.Seed(time.Now().UnixNano()) - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.file_server", - New: func() interface{} { return new(FileServer) }, - }) + caddy.RegisterModule(FileServer{}) } // FileServer implements a static file server responder for Caddy. @@ -48,8 +45,14 @@ type FileServer struct { Hide []string `json:"hide,omitempty"` IndexNames []string `json:"index_names,omitempty"` Browse *Browse `json:"browse,omitempty"` +} - // TODO: Content negotiation +// CaddyModule returns the Caddy module information. +func (FileServer) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.file_server", + New: func() caddy.Module { return new(FileServer) }, + } } // Provision sets up the static files responder. @@ -83,7 +86,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, _ cadd filesToHide := fsrv.transformHidePaths(repl) - root := repl.ReplaceAll(fsrv.Root, "") + root := repl.ReplaceAll(fsrv.Root, ".") suffix := repl.ReplaceAll(r.URL.Path, "") filename := sanitizedPathJoin(root, suffix) @@ -302,7 +305,7 @@ func calculateEtag(d os.FileInfo) string { return `"` + t + s + `"` } -var defaultIndexNames = []string{"index.html"} +var defaultIndexNames = []string{"index.html", "index.txt"} var bufPool = sync.Pool{ New: func() interface{} { diff --git a/modules/caddyhttp/headers/caddyfile.go b/modules/caddyhttp/headers/caddyfile.go new file mode 100644 index 0000000..5eaf064 --- /dev/null +++ b/modules/caddyhttp/headers/caddyfile.go @@ -0,0 +1,91 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package headers + +import ( + "net/http" + "strings" + + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +func init() { + httpcaddyfile.RegisterHandlerDirective("headers", parseCaddyfile) +} + +// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax: +// +// headers [<matcher>] [[+|-]<field> <value>] { +// [+][<field>] [<value>] +// [-<field>] +// } +// +// Either a block can be opened or a single header field can be configured +// in the first line, but not both in the same directive. +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + hdr := new(Headers) + for h.Next() { + // first see if headers are in the initial line + var hasArgs bool + if h.NextArg() { + hasArgs = true + field := h.Val() + h.NextArg() + value := h.Val() + processCaddyfileLine(hdr, field, value) + } + + // if not, they should be in a block + for h.NextBlock() { + if hasArgs { + return nil, h.Err("cannot specify headers in both arguments and block") + } + field := h.Val() + var value string + if h.NextArg() { + value = h.Val() + } + processCaddyfileLine(hdr, field, value) + } + } + return hdr, nil +} + +func processCaddyfileLine(hdr *Headers, field, value string) { + if strings.HasPrefix(field, "+") { + if hdr.Response == nil { + hdr.Response = &RespHeaderOps{HeaderOps: new(HeaderOps)} + } + if hdr.Response.Add == nil { + hdr.Response.Add = make(http.Header) + } + hdr.Response.Add.Set(field[1:], value) + } else if strings.HasPrefix(field, "-") { + if hdr.Response == nil { + hdr.Response = &RespHeaderOps{HeaderOps: new(HeaderOps)} + } + hdr.Response.Delete = append(hdr.Response.Delete, field[1:]) + hdr.Response.Deferred = true + } else { + if hdr.Response == nil { + hdr.Response = &RespHeaderOps{HeaderOps: new(HeaderOps)} + } + if hdr.Response.Set == nil { + hdr.Response.Set = make(http.Header) + } + hdr.Response.Set.Set(field, value) + } +} diff --git a/modules/caddyhttp/headers/headers.go b/modules/caddyhttp/headers/headers.go index 8f4976a..e740004 100644 --- a/modules/caddyhttp/headers/headers.go +++ b/modules/caddyhttp/headers/headers.go @@ -23,10 +23,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.headers", - New: func() interface{} { return new(Headers) }, - }) + caddy.RegisterModule(Headers{}) } // Headers is a middleware which can mutate HTTP headers. @@ -35,6 +32,14 @@ type Headers struct { Response *RespHeaderOps `json:"response,omitempty"` } +// CaddyModule returns the Caddy module information. +func (Headers) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.headers", + New: func() caddy.Module { return new(Headers) }, + } +} + // HeaderOps defines some operations to // perform on HTTP headers. type HeaderOps struct { diff --git a/modules/caddyhttp/markdown/markdown.go b/modules/caddyhttp/markdown/markdown.go index 3ba4d02..122aad6 100644 --- a/modules/caddyhttp/markdown/markdown.go +++ b/modules/caddyhttp/markdown/markdown.go @@ -28,16 +28,21 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.markdown", - New: func() interface{} { return new(Markdown) }, - }) + caddy.RegisterModule(Markdown{}) } // Markdown is a middleware for rendering a Markdown response body. type Markdown struct { } +// CaddyModule returns the Caddy module information. +func (Markdown) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.markdown", + New: func() caddy.Module { return new(Markdown) }, + } +} + func (m Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { buf := bufPool.Get().(*bytes.Buffer) buf.Reset() diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 6c5a23e..0dac151 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -28,6 +28,7 @@ import ( "strings" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/pkg/caddyscript" "go.starlark.net/starlark" ) @@ -79,50 +80,31 @@ type ( ) func init() { - caddy.RegisterModule(caddy.Module{ + caddy.RegisterModule(MatchHost{}) + caddy.RegisterModule(MatchPath{}) + caddy.RegisterModule(MatchPathRE{}) + caddy.RegisterModule(MatchMethod{}) + caddy.RegisterModule(MatchQuery{}) + caddy.RegisterModule(MatchHeader{}) + caddy.RegisterModule(MatchHeaderRE{}) + caddy.RegisterModule(new(MatchProtocol)) + caddy.RegisterModule(MatchRemoteIP{}) + caddy.RegisterModule(MatchNegate{}) + caddy.RegisterModule(new(MatchStarlarkExpr)) +} + +// CaddyModule returns the Caddy module information. +func (MatchHost) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ Name: "http.matchers.host", - New: func() interface{} { return new(MatchHost) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.path", - New: func() interface{} { return new(MatchPath) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.path_regexp", - New: func() interface{} { return new(MatchPathRE) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.method", - New: func() interface{} { return new(MatchMethod) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.query", - New: func() interface{} { return new(MatchQuery) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.header", - New: func() interface{} { return new(MatchHeader) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.header_regexp", - New: func() interface{} { return new(MatchHeaderRE) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.protocol", - New: func() interface{} { return new(MatchProtocol) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.remote_ip", - New: func() interface{} { return new(MatchRemoteIP) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.not", - New: func() interface{} { return new(MatchNegate) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.starlark_expr", - New: func() interface{} { return new(MatchStarlarkExpr) }, - }) + New: func() caddy.Module { return new(MatchHost) }, + } +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *MatchHost) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + *m = d.RemainingArgs() + return nil } // Match returns true if r matches m. @@ -158,6 +140,14 @@ outer: return false } +// CaddyModule returns the Caddy module information. +func (MatchPath) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.path", + New: func() caddy.Module { return new(MatchPath) }, + } +} + // Match returns true if r matches m. func (m MatchPath) Match(r *http.Request) bool { for _, matchPath := range m { @@ -177,12 +167,44 @@ func (m MatchPath) Match(r *http.Request) bool { return false } +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *MatchPath) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + *m = d.RemainingArgs() + } + return nil +} + +// CaddyModule returns the Caddy module information. +func (MatchPathRE) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.path_regexp", + New: func() caddy.Module { return new(MatchPathRE) }, + } +} + // Match returns true if r matches m. func (m MatchPathRE) Match(r *http.Request) bool { repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) return m.MatchRegexp.Match(r.URL.Path, repl, "path_regexp") } +// CaddyModule returns the Caddy module information. +func (MatchMethod) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.method", + New: func() caddy.Module { return new(MatchMethod) }, + } +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *MatchMethod) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + *m = d.RemainingArgs() + } + return nil +} + // Match returns true if r matches m. func (m MatchMethod) Match(r *http.Request) bool { for _, method := range m { @@ -193,6 +215,26 @@ func (m MatchMethod) Match(r *http.Request) bool { return false } +// CaddyModule returns the Caddy module information. +func (MatchQuery) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.query", + New: func() caddy.Module { return new(MatchQuery) }, + } +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *MatchQuery) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + parts := strings.SplitN(d.Val(), "=", 2) + if len(parts) != 2 { + return d.Errf("malformed query matcher token: %s; must be in param=val format", d.Val()) + } + url.Values(*m).Set(parts[0], parts[1]) + } + return nil +} + // Match returns true if r matches m. func (m MatchQuery) Match(r *http.Request) bool { for param, vals := range m { @@ -206,6 +248,26 @@ func (m MatchQuery) Match(r *http.Request) bool { return false } +// CaddyModule returns the Caddy module information. +func (MatchHeader) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.header", + New: func() caddy.Module { return new(MatchHeader) }, + } +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *MatchHeader) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + var field, val string + if !d.Args(&field, &val) { + return d.Errf("expected both field and value") + } + http.Header(*m).Set(field, val) + } + return nil +} + // Match returns true if r matches m. func (m MatchHeader) Match(r *http.Request) bool { for field, allowedFieldVals := range m { @@ -227,6 +289,29 @@ func (m MatchHeader) Match(r *http.Request) bool { return true } +// CaddyModule returns the Caddy module information. +func (MatchHeaderRE) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.header_regexp", + New: func() caddy.Module { return new(MatchHeaderRE) }, + } +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *MatchHeaderRE) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + if *m == nil { + *m = make(map[string]*MatchRegexp) + } + for d.Next() { + var field, val string + if !d.Args(&field, &val) { + return d.ArgErr() + } + (*m)[field] = &MatchRegexp{Pattern: val} + } + return nil +} + // Match returns true if r matches m. func (m MatchHeaderRE) Match(r *http.Request) bool { for field, rm := range m { @@ -261,6 +346,14 @@ func (m MatchHeaderRE) Validate() error { return nil } +// CaddyModule returns the Caddy module information. +func (MatchProtocol) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.protocol", + New: func() caddy.Module { return new(MatchProtocol) }, + } +} + // Match returns true if r matches m. func (m MatchProtocol) Match(r *http.Request) bool { switch string(m) { @@ -274,6 +367,26 @@ func (m MatchProtocol) Match(r *http.Request) bool { return false } +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *MatchProtocol) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + var proto string + if !d.Args(&proto) { + return d.Err("expected exactly one protocol") + } + *m = MatchProtocol(proto) + } + return nil +} + +// CaddyModule returns the Caddy module information. +func (MatchNegate) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.not", + New: func() caddy.Module { return new(MatchNegate) }, + } +} + // UnmarshalJSON unmarshals data into m's unexported map field. // This is done because we cannot embed the map directly into // the struct, but we need a struct because we need another @@ -282,6 +395,12 @@ func (m *MatchNegate) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &m.matchersRaw) } +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *MatchNegate) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + // TODO: figure out how this will work + return nil +} + // Provision loads the matcher modules to be negated. func (m *MatchNegate) Provision(ctx caddy.Context) error { for modName, rawMsg := range m.matchersRaw { @@ -301,6 +420,22 @@ func (m MatchNegate) Match(r *http.Request) bool { return !m.matchers.Match(r) } +// CaddyModule returns the Caddy module information. +func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.remote_ip", + New: func() caddy.Module { return new(MatchRemoteIP) }, + } +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + m.Ranges = d.RemainingArgs() + } + return nil +} + // Provision parses m's IP ranges, either from IP or CIDR expressions. func (m *MatchRemoteIP) Provision(ctx caddy.Context) error { for _, str := range m.Ranges { @@ -362,6 +497,14 @@ func (m MatchRemoteIP) Match(r *http.Request) bool { return false } +// CaddyModule returns the Caddy module information. +func (MatchStarlarkExpr) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.starlark_expr", // TODO: Rename to 'starlark'? + New: func() caddy.Module { return new(MatchStarlarkExpr) }, + } +} + // Match returns true if r matches m. func (m MatchStarlarkExpr) Match(r *http.Request) bool { input := string(m) @@ -379,7 +522,7 @@ func (m MatchStarlarkExpr) Match(r *http.Request) bool { // MatchRegexp is an embeddable type for matching // using regular expressions. type MatchRegexp struct { - Name string `json:"name"` + Name string `json:"name,omitempty"` Pattern string `json:"pattern"` compiled *regexp.Regexp } @@ -431,6 +574,23 @@ func (mre *MatchRegexp) Match(input string, repl caddy.Replacer, scope string) b return true } +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + args := d.RemainingArgs() + switch len(args) { + case 1: + mre.Pattern = args[0] + case 2: + mre.Name = args[0] + mre.Pattern = args[1] + default: + return d.ArgErr() + } + } + return nil +} + // ResponseMatcher is a type which can determine if a given response // status code and its headers match some criteria. type ResponseMatcher struct { @@ -506,4 +666,14 @@ var ( _ caddy.Provisioner = (*MatchNegate)(nil) _ RequestMatcher = (*MatchStarlarkExpr)(nil) _ caddy.Provisioner = (*MatchRegexp)(nil) + + _ caddyfile.Unmarshaler = (*MatchHost)(nil) + _ caddyfile.Unmarshaler = (*MatchPath)(nil) + _ caddyfile.Unmarshaler = (*MatchPathRE)(nil) + _ caddyfile.Unmarshaler = (*MatchMethod)(nil) + _ caddyfile.Unmarshaler = (*MatchQuery)(nil) + _ caddyfile.Unmarshaler = (*MatchHeader)(nil) + _ caddyfile.Unmarshaler = (*MatchHeaderRE)(nil) + _ caddyfile.Unmarshaler = (*MatchProtocol)(nil) + _ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil) ) diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go index 439d245..cc29789 100644 --- a/modules/caddyhttp/replacer.go +++ b/modules/caddyhttp/replacer.go @@ -15,6 +15,7 @@ package caddyhttp import ( + "fmt" "net" "net/http" "net/textproto" @@ -28,6 +29,7 @@ import ( func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.ResponseWriter) { httpVars := func(key string) (string, bool) { if req != nil { + // query string parameters if strings.HasPrefix(key, queryReplPrefix) { vals := req.URL.Query()[key[len(queryReplPrefix):]] // always return true, since the query param might @@ -35,6 +37,7 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon return strings.Join(vals, ","), true } + // request header fields if strings.HasPrefix(key, reqHeaderReplPrefix) { field := key[len(reqHeaderReplPrefix):] vals := req.Header[textproto.CanonicalMIMEHeaderKey(field)] @@ -43,6 +46,7 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon return strings.Join(vals, ","), true } + // cookies if strings.HasPrefix(key, cookieReplPrefix) { name := key[len(cookieReplPrefix):] for _, cookie := range req.Cookies() { @@ -87,14 +91,7 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon return req.URL.RawQuery, true } - if strings.HasPrefix(key, respHeaderReplPrefix) { - field := key[len(respHeaderReplPrefix):] - vals := w.Header()[textproto.CanonicalMIMEHeaderKey(field)] - // always return true, since the header field might - // be present only in some requests - return strings.Join(vals, ","), true - } - + // hostname labels if strings.HasPrefix(key, hostLabelReplPrefix) { idxStr := key[len(hostLabelReplPrefix):] idx, err := strconv.Atoi(idxStr) @@ -111,6 +108,7 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon return hostLabels[idx], true } + // path parts if strings.HasPrefix(key, pathPartsReplPrefix) { idxStr := key[len(pathPartsReplPrefix):] idx, err := strconv.Atoi(idxStr) @@ -129,9 +127,31 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon } return pathParts[idx], true } + + // middleware variables + if strings.HasPrefix(key, varsReplPrefix) { + varName := key[len(varsReplPrefix):] + tbl := req.Context().Value(VarCtxKey).(map[string]interface{}) + raw, ok := tbl[varName] + if !ok { + // variables can be dynamic, so always return true + // even when it may not be set; treat as empty + return "", true + } + // do our best to convert it to a string efficiently + switch val := raw.(type) { + case string: + return val, true + case fmt.Stringer: + return val.String(), true + default: + return fmt.Sprintf("%s", val), true + } + } } if w != nil { + // response header fields if strings.HasPrefix(key, respHeaderReplPrefix) { field := key[len(respHeaderReplPrefix):] vals := w.Header()[textproto.CanonicalMIMEHeaderKey(field)] @@ -153,5 +173,6 @@ const ( cookieReplPrefix = "http.request.cookie." hostLabelReplPrefix = "http.request.host.labels." pathPartsReplPrefix = "http.request.uri.path." + varsReplPrefix = "http.var." respHeaderReplPrefix = "http.response.header." ) diff --git a/modules/caddyhttp/requestbody/requestbody.go b/modules/caddyhttp/requestbody/requestbody.go index 3763cfe..9b16250 100644 --- a/modules/caddyhttp/requestbody/requestbody.go +++ b/modules/caddyhttp/requestbody/requestbody.go @@ -22,10 +22,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.request_body", - New: func() interface{} { return new(RequestBody) }, - }) + caddy.RegisterModule(RequestBody{}) } // RequestBody is a middleware for manipulating the request body. @@ -33,6 +30,14 @@ type RequestBody struct { MaxSize int64 `json:"max_size,omitempty"` } +// CaddyModule returns the Caddy module information. +func (RequestBody) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.request_body", // TODO: better name for this? + New: func() caddy.Module { return new(RequestBody) }, + } +} + func (rb RequestBody) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { if r.Body == nil { return next.ServeHTTP(w, r) diff --git a/modules/caddyhttp/reverseproxy/module.go b/modules/caddyhttp/reverseproxy/module.go index 2e6a338..21aca1d 100755 --- a/modules/caddyhttp/reverseproxy/module.go +++ b/modules/caddyhttp/reverseproxy/module.go @@ -16,12 +16,38 @@ package reverseproxy import ( "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) -// Register caddy module. func init() { - caddy.RegisterModule(caddy.Module{ + caddy.RegisterModule(new(LoadBalanced)) + httpcaddyfile.RegisterHandlerDirective("reverse_proxy", parseCaddyfile) // TODO: "proxy"? +} + +// CaddyModule returns the Caddy module information. +func (*LoadBalanced) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ Name: "http.handlers.reverse_proxy", - New: func() interface{} { return new(LoadBalanced) }, - }) + New: func() caddy.Module { return new(LoadBalanced) }, + } +} + +// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax: +// +// proxy [<matcher>] <to> +// +// TODO: This needs to be finished. It definitely needs to be able to open a block... +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + lb := new(LoadBalanced) + for h.Next() { + allTo := h.RemainingArgs() + if len(allTo) == 0 { + return nil, h.ArgErr() + } + for _, to := range allTo { + lb.Upstreams = append(lb.Upstreams, &UpstreamConfig{Host: to}) + } + } + return lb, nil } diff --git a/modules/caddyhttp/reverseproxy/upstream.go b/modules/caddyhttp/reverseproxy/upstream.go index 10df80b..1f0693e 100755 --- a/modules/caddyhttp/reverseproxy/upstream.go +++ b/modules/caddyhttp/reverseproxy/upstream.go @@ -179,21 +179,21 @@ type LoadBalanced struct { // The following struct fields are set by caddy configuration. // TryInterval is the max duration for which request retrys will be performed for a request. - TryInterval string `json:"try_interval"` + TryInterval string `json:"try_interval,omitempty"` // Upstreams are the configs for upstream hosts - Upstreams []*UpstreamConfig `json:"upstreams"` + Upstreams []*UpstreamConfig `json:"upstreams,omitempty"` // LoadBalanceType is the string representation of what loadbalancing algorithm to use. i.e. "random" or "round_robin". - LoadBalanceType string `json:"load_balance_type"` + LoadBalanceType string `json:"load_balance_type,omitempty"` // NoHealthyUpstreamsMessage is returned as a response when there are no healthy upstreams to loadbalance to. - NoHealthyUpstreamsMessage string `json:"no_healthy_upstreams_message"` + NoHealthyUpstreamsMessage string `json:"no_healthy_upstreams_message,omitempty"` // TODO :- store healthcheckers as package level state where each upstream gets a single healthchecker // currently a healthchecker is created for each upstream defined, even if a healthchecker was previously created // for that upstream - HealthCheckers []*HealthChecker + HealthCheckers []*HealthChecker `json:"health_checkers,omitempty"` } // Cleanup stops all health checkers on a loadbalanced reverse proxy. @@ -320,22 +320,22 @@ func (lb *LoadBalanced) random() *upstream { // UpstreamConfig represents the config of an upstream. type UpstreamConfig struct { // Host is the host name of the upstream server. - Host string `json:"host"` + Host string `json:"host,omitempty"` // FastHealthCheckDuration is the duration for which a health check is performed when a node is considered unhealthy. - FastHealthCheckDuration string `json:"fast_health_check_duration"` + FastHealthCheckDuration string `json:"fast_health_check_duration,omitempty"` - CircuitBreaker json.RawMessage `json:"circuit_breaker"` + CircuitBreaker json.RawMessage `json:"circuit_breaker,omitempty"` // // CircuitBreakerConfig is the config passed to setup a circuit breaker. - // CircuitBreakerConfig *circuitbreaker.Config `json:"circuit_breaker"` + // CircuitBreakerConfig *circuitbreaker.Config `json:"circuit_breaker,omitempty"` circuitbreaker CircuitBreaker // HealthCheckDuration is the default duration for which a health check is performed. - HealthCheckDuration string `json:"health_check_duration"` + HealthCheckDuration string `json:"health_check_duration,omitempty"` // HealthCheckPath is the path at the upstream host to use for healthchecks. - HealthCheckPath string `json:"health_check_path"` + HealthCheckPath string `json:"health_check_path,omitempty"` } // upstream represents an upstream host. diff --git a/modules/caddyhttp/rewrite/caddyfile.go b/modules/caddyhttp/rewrite/caddyfile.go new file mode 100644 index 0000000..a977a72 --- /dev/null +++ b/modules/caddyhttp/rewrite/caddyfile.go @@ -0,0 +1,37 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rewrite + +import ( + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +func init() { + httpcaddyfile.RegisterHandlerDirective("rewrite", parseCaddyfile) +} + +// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax: +// +// rewrite [<matcher>] <to> +// +// The <to> parameter becomes the new URI. +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + var rewr Rewrite + for h.Next() { + rewr.URI = h.Val() + } + return rewr, nil +} diff --git a/modules/caddyhttp/rewrite/rewrite.go b/modules/caddyhttp/rewrite/rewrite.go index ac113ff..f434a38 100644 --- a/modules/caddyhttp/rewrite/rewrite.go +++ b/modules/caddyhttp/rewrite/rewrite.go @@ -24,10 +24,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.rewrite", - New: func() interface{} { return new(Rewrite) }, - }) + caddy.RegisterModule(Rewrite{}) } // Rewrite is a middleware which can rewrite HTTP requests. @@ -37,6 +34,14 @@ type Rewrite struct { Rehandle bool `json:"rehandle,omitempty"` } +// CaddyModule returns the Caddy module information. +func (Rewrite) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.rewrite", + New: func() caddy.Module { return new(Rewrite) }, + } +} + func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) var rehandleNeeded bool diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go index b0672b1..1efbad6 100644 --- a/modules/caddyhttp/routes.go +++ b/modules/caddyhttp/routes.go @@ -22,37 +22,38 @@ import ( "github.com/caddyserver/caddy/v2" ) -// ServerRoute represents a set of matching rules, +// Route represents a set of matching rules, // middlewares, and a responder for handling HTTP // requests. -type ServerRoute struct { - Group string `json:"group,omitempty"` - MatcherSets []map[string]json.RawMessage `json:"match,omitempty"` - Handle []json.RawMessage `json:"handle,omitempty"` - Terminal bool `json:"terminal,omitempty"` +type Route struct { + Group string `json:"group,omitempty"` + MatcherSetsRaw []map[string]json.RawMessage `json:"match,omitempty"` + HandlersRaw []json.RawMessage `json:"handle,omitempty"` + Terminal bool `json:"terminal,omitempty"` // decoded values - matcherSets []MatcherSet - handlers []MiddlewareHandler + MatcherSets []MatcherSet `json:"-"` + Handlers []MiddlewareHandler `json:"-"` } // Empty returns true if the route has all zero/default values. -func (sr ServerRoute) Empty() bool { - return len(sr.MatcherSets) == 0 && - len(sr.Handle) == 0 && - len(sr.handlers) == 0 && - !sr.Terminal && - sr.Group == "" +func (r Route) Empty() bool { + return len(r.MatcherSetsRaw) == 0 && + len(r.MatcherSets) == 0 && + len(r.HandlersRaw) == 0 && + len(r.Handlers) == 0 && + !r.Terminal && + r.Group == "" } -func (sr ServerRoute) anyMatcherSetMatches(r *http.Request) bool { - for _, ms := range sr.matcherSets { - if ms.Match(r) { +func (r Route) anyMatcherSetMatches(req *http.Request) bool { + for _, ms := range r.MatcherSets { + if ms.Match(req) { return true } } // if no matchers, always match - return len(sr.matcherSets) == 0 + return len(r.MatcherSets) == 0 } // MatcherSet is a set of matchers which @@ -73,13 +74,13 @@ func (mset MatcherSet) Match(r *http.Request) bool { // RouteList is a list of server routes that can // create a middleware chain. -type RouteList []ServerRoute +type RouteList []Route // Provision sets up all the routes by loading the modules. func (routes RouteList) Provision(ctx caddy.Context) error { for i, route := range routes { // matchers - for _, matcherSet := range route.MatcherSets { + for _, matcherSet := range route.MatcherSetsRaw { var matchers MatcherSet for modName, rawMsg := range matcherSet { val, err := ctx.LoadModule("http.matchers."+modName, rawMsg) @@ -88,19 +89,19 @@ func (routes RouteList) Provision(ctx caddy.Context) error { } matchers = append(matchers, val.(RequestMatcher)) } - routes[i].matcherSets = append(routes[i].matcherSets, matchers) + routes[i].MatcherSets = append(routes[i].MatcherSets, matchers) } - routes[i].MatcherSets = nil // allow GC to deallocate - TODO: Does this help? + routes[i].MatcherSetsRaw = nil // allow GC to deallocate - TODO: Does this help? // handlers - for j, rawMsg := range route.Handle { + for j, rawMsg := range route.HandlersRaw { mh, err := ctx.LoadModuleInline("handler", "http.handlers", rawMsg) if err != nil { return fmt.Errorf("loading handler module in position %d: %v", j, err) } - routes[i].handlers = append(routes[i].handlers, mh.(MiddlewareHandler)) + routes[i].Handlers = append(routes[i].Handlers, mh.(MiddlewareHandler)) } - routes[i].Handle = nil // allow GC to deallocate - TODO: Does this help? + routes[i].HandlersRaw = nil // allow GC to deallocate - TODO: Does this help? } return nil } @@ -135,7 +136,7 @@ func (routes RouteList) BuildCompositeRoute(req *http.Request) Handler { } // apply the rest of the route - for _, mh := range route.handlers { + for _, mh := range route.Handlers { // we have to be sure to wrap mh outside // of our current stack frame so that the // reference to this mh isn't overwritten diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index d79d8d3..f820f71 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -57,7 +57,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { repl := caddy.NewReplacer() ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl) ctx = context.WithValue(ctx, ServerCtxKey, s) - ctx = context.WithValue(ctx, TableCtxKey, make(map[string]interface{})) // TODO: Implement this + ctx = context.WithValue(ctx, VarCtxKey, make(map[string]interface{})) r = r.WithContext(ctx) // once the pointer to the request won't change @@ -201,6 +201,14 @@ type AutoHTTPSConfig struct { // that certificates will not be provisioned and managed // for these names. SkipCerts []string `json:"skip_certificates,omitempty"` + + // By default, automatic HTTPS will obtain and renew + // certificates for qualifying hostnames. However, if + // a certificate with a matching SAN is already loaded + // into the cache, certificate management will not be + // enabled. To force automated certificate management + // regardless of loaded certificates, set this to true. + IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"` } // Skipped returns true if name is in skipSlice, which @@ -225,6 +233,6 @@ const ( // For referencing the server instance ServerCtxKey caddy.CtxKey = "server" - // For the request's variable table (TODO: implement this) - TableCtxKey caddy.CtxKey = "table" + // For the request's variable table + VarCtxKey caddy.CtxKey = "vars" ) diff --git a/modules/caddyhttp/staticerror.go b/modules/caddyhttp/staticerror.go index 3a8e8bc..3a45366 100644 --- a/modules/caddyhttp/staticerror.go +++ b/modules/caddyhttp/staticerror.go @@ -18,22 +18,26 @@ import ( "fmt" "net/http" "strconv" - "strings" "github.com/caddyserver/caddy/v2" ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.error", - New: func() interface{} { return new(StaticError) }, - }) + caddy.RegisterModule(StaticError{}) } // StaticError implements a simple handler that returns an error. type StaticError struct { Error string `json:"error,omitempty"` - StatusCode weakString `json:"status_code,omitempty"` + StatusCode WeakString `json:"status_code,omitempty"` +} + +// CaddyModule returns the Caddy module information. +func (StaticError) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.error", + New: func() caddy.Module { return new(StaticError) }, + } } func (e StaticError) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error { @@ -53,43 +57,3 @@ func (e StaticError) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler // Interface guard var _ MiddlewareHandler = (*StaticError)(nil) - -// weakString is a type that unmarshals any JSON value -// as a string literal, and provides methods for -// getting the value as different primitive types. -// However, using this type removes any type safety -// as far as deserializing JSON is concerned. -type weakString string - -// UnmarshalJSON satisfies json.Unmarshaler. It -// unmarshals b by always interpreting it as a -// string literal. -func (ws *weakString) UnmarshalJSON(b []byte) error { - *ws = weakString(strings.Trim(string(b), `"`)) - return nil -} - -// Int returns ws as an integer. If ws is not an -// integer, 0 is returned. -func (ws weakString) Int() int { - num, _ := strconv.Atoi(string(ws)) - return num -} - -// Float64 returns ws as a float64. If ws is not a -// float value, the zero value is returned. -func (ws weakString) Float64() float64 { - num, _ := strconv.ParseFloat(string(ws), 64) - return num -} - -// Bool returns ws as a boolean. If ws is not a -// boolean, false is returned. -func (ws weakString) Bool() bool { - return string(ws) == "true" -} - -// String returns ws as a string. -func (ws weakString) String() string { - return string(ws) -} diff --git a/modules/caddyhttp/staticresp.go b/modules/caddyhttp/staticresp.go index 291d992..942459b 100644 --- a/modules/caddyhttp/staticresp.go +++ b/modules/caddyhttp/staticresp.go @@ -20,21 +20,61 @@ import ( "strconv" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.static_response", - New: func() interface{} { return new(StaticResponse) }, - }) + caddy.RegisterModule(StaticResponse{}) + // TODO: Caddyfile directive } // StaticResponse implements a simple responder for static responses. type StaticResponse struct { - StatusCode weakString `json:"status_code"` - Headers http.Header `json:"headers"` - Body string `json:"body"` - Close bool `json:"close"` + StatusCode WeakString `json:"status_code,omitempty"` + Headers http.Header `json:"headers,omitempty"` + Body string `json:"body,omitempty"` + Close bool `json:"close,omitempty"` +} + +// CaddyModule returns the Caddy module information. +func (StaticResponse) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.static_response", + New: func() caddy.Module { return new(StaticResponse) }, + } +} + +// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: +// +// static_response [<matcher>] <status> { +// body <text> +// close +// } +// +func (s *StaticResponse) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + var statusCodeStr string + if d.Args(&statusCodeStr) { + s.StatusCode = WeakString(statusCodeStr) + } + for d.NextBlock() { + switch d.Val() { + case "body": + if s.Body != "" { + return d.Err("body already specified") + } + if !d.Args(&s.Body) { + return d.ArgErr() + } + case "close": + if s.Close { + return d.Err("close already specified") + } + s.Close = true + } + } + } + return nil } func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error { diff --git a/modules/caddyhttp/staticresp_test.go b/modules/caddyhttp/staticresp_test.go index 49adedd..cd0d1a1 100644 --- a/modules/caddyhttp/staticresp_test.go +++ b/modules/caddyhttp/staticresp_test.go @@ -30,7 +30,7 @@ func TestStaticResponseHandler(t *testing.T) { w := httptest.NewRecorder() s := StaticResponse{ - StatusCode: weakString(strconv.Itoa(http.StatusNotFound)), + StatusCode: WeakString(strconv.Itoa(http.StatusNotFound)), Headers: http.Header{ "X-Test": []string{"Testing"}, }, diff --git a/modules/caddyhttp/subroute.go b/modules/caddyhttp/subroute.go index 9172146..3b0d718 100644 --- a/modules/caddyhttp/subroute.go +++ b/modules/caddyhttp/subroute.go @@ -22,10 +22,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.subroute", - New: func() interface{} { return new(Subroute) }, - }) + caddy.RegisterModule(Subroute{}) } // Subroute implements a handler that compiles and executes routes. @@ -37,6 +34,14 @@ type Subroute struct { Routes RouteList `json:"routes,omitempty"` } +// CaddyModule returns the Caddy module information. +func (Subroute) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.subroute", + New: func() caddy.Module { return new(Subroute) }, + } +} + // Provision sets up subrouting. func (sr *Subroute) Provision(ctx caddy.Context) error { if sr.Routes != nil { diff --git a/modules/caddyhttp/table.go b/modules/caddyhttp/table.go deleted file mode 100644 index 5b1fed5..0000000 --- a/modules/caddyhttp/table.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2015 Matthew Holt and The Caddy Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddyhttp - -import ( - "net/http" - - "github.com/caddyserver/caddy/v2" -) - -func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.table", - New: func() interface{} { return new(tableMiddleware) }, - }) - - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.table", - New: func() interface{} { return new(tableMatcher) }, - }) -} - -type tableMiddleware struct { -} - -func (t tableMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error { - // tbl := r.Context().Value(TableCtxKey).(map[string]interface{}) - - // TODO: implement this... - - return nil -} - -type tableMatcher struct { -} - -func (m tableMatcher) Match(r *http.Request) bool { - return false // TODO: implement -} - -// Interface guards -var _ MiddlewareHandler = (*tableMiddleware)(nil) -var _ RequestMatcher = (*tableMatcher)(nil) diff --git a/modules/caddyhttp/templates/caddyfile.go b/modules/caddyhttp/templates/caddyfile.go new file mode 100644 index 0000000..d948da0 --- /dev/null +++ b/modules/caddyhttp/templates/caddyfile.go @@ -0,0 +1,62 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package templates + +import ( + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +func init() { + httpcaddyfile.RegisterHandlerDirective("templates", parseCaddyfile) +} + +// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax: +// +// templates [<matcher>] { +// mime <types...> +// between <open_delim> <close_delim> +// root <path> +// } +// +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + t := new(Templates) + for h.Next() { + for h.NextBlock() { + switch h.Val() { + case "mime": + t.MIMETypes = h.RemainingArgs() + if len(t.MIMETypes) == 0 { + return nil, h.ArgErr() + } + case "between": + t.Delimiters = h.RemainingArgs() + if len(t.Delimiters) != 2 { + return nil, h.ArgErr() + } + case "root": + if !h.Args(&t.IncludeRoot) { + return nil, h.ArgErr() + } + } + } + } + + if t.IncludeRoot == "" { + t.IncludeRoot = "{http.var.root}" + } + + return t, nil +} diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go index 9a41b6d..1cd347c 100644 --- a/modules/caddyhttp/templates/templates.go +++ b/modules/caddyhttp/templates/templates.go @@ -27,10 +27,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.templates", - New: func() interface{} { return new(Templates) }, - }) + caddy.RegisterModule(Templates{}) } // Templates is a middleware which execute response bodies as templates. @@ -40,6 +37,14 @@ type Templates struct { Delimiters []string `json:"delimiters,omitempty"` } +// CaddyModule returns the Caddy module information. +func (Templates) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.templates", + New: func() caddy.Module { return new(Templates) }, + } +} + // Provision provisions t. func (t *Templates) Provision(ctx caddy.Context) error { if t.MIMETypes == nil { @@ -108,7 +113,8 @@ func (t *Templates) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy func (t *Templates) executeTemplate(rr caddyhttp.ResponseRecorder, r *http.Request) error { var fs http.FileSystem if t.IncludeRoot != "" { - fs = http.Dir(t.IncludeRoot) + repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) + fs = http.Dir(repl.ReplaceAll(t.IncludeRoot, ".")) } ctx := &templateContext{ diff --git a/modules/caddyhttp/templates/tplcontext.go b/modules/caddyhttp/templates/tplcontext.go index ffcc636..a51e54b 100644 --- a/modules/caddyhttp/templates/tplcontext.go +++ b/modules/caddyhttp/templates/tplcontext.go @@ -136,19 +136,6 @@ func (c templateContext) Cookie(name string) string { return "" } -// Hostname gets the (remote) hostname of the client making the request. -// Performance warning: This involves a DNS lookup. -func (c templateContext) Hostname() string { - ip := c.RemoteIP() - - hostnameList, err := net.LookupAddr(ip) - if err != nil || len(hostnameList) == 0 { - return c.Req.RemoteAddr - } - - return hostnameList[0] -} - // RemoteIP gets the IP address of the client making the request. func (c templateContext) RemoteIP() string { ip, _, err := net.SplitHostPort(c.Req.RemoteAddr) diff --git a/modules/caddyhttp/vars.go b/modules/caddyhttp/vars.go new file mode 100644 index 0000000..bbd4568 --- /dev/null +++ b/modules/caddyhttp/vars.go @@ -0,0 +1,81 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyhttp + +import ( + "net/http" + + "github.com/caddyserver/caddy/v2" +) + +func init() { + caddy.RegisterModule(VarsMiddleware{}) + caddy.RegisterModule(VarsMatcher{}) +} + +// VarsMiddleware is an HTTP middleware which sets variables +// in the context, mainly for use by placeholders. +type VarsMiddleware map[string]string + +// CaddyModule returns the Caddy module information. +func (VarsMiddleware) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.vars", + New: func() caddy.Module { return new(VarsMiddleware) }, + } +} + +func (t VarsMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error { + vars := r.Context().Value(VarCtxKey).(map[string]interface{}) + repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) + for k, v := range t { + keyExpanded := repl.ReplaceAll(k, "") + valExpanded := repl.ReplaceAll(v, "") + vars[keyExpanded] = valExpanded + } + return next.ServeHTTP(w, r) +} + +// VarsMatcher is an HTTP request matcher which can match +// requests based on variables in the context. +type VarsMatcher map[string]string + +// CaddyModule returns the Caddy module information. +func (VarsMatcher) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.vars", + New: func() caddy.Module { return new(VarsMatcher) }, + } +} + +// Match matches a request based on variables in the context. +func (m VarsMatcher) Match(r *http.Request) bool { + vars := r.Context().Value(VarCtxKey).(map[string]string) + repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) + for k, v := range m { + keyExpanded := repl.ReplaceAll(k, "") + valExpanded := repl.ReplaceAll(v, "") + if vars[keyExpanded] != valExpanded { + return false + } + } + return true +} + +// Interface guards +var ( + _ MiddlewareHandler = (*VarsMiddleware)(nil) + _ RequestMatcher = (*VarsMatcher)(nil) +) diff --git a/modules/caddytls/acmemanager.go b/modules/caddytls/acmemanager.go index 578cdb3..36f1c21 100644 --- a/modules/caddytls/acmemanager.go +++ b/modules/caddytls/acmemanager.go @@ -28,10 +28,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "tls.management.acme", - New: func() interface{} { return new(ACMEManagerMaker) }, - }) + caddy.RegisterModule(ACMEManagerMaker{}) } // ACMEManagerMaker makes an ACME manager @@ -57,9 +54,17 @@ type ACMEManagerMaker struct { keyType certcrypto.KeyType } -// newManager is a no-op to satisfy the ManagerMaker interface, +// CaddyModule returns the Caddy module information. +func (ACMEManagerMaker) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "tls.management.acme", + New: func() caddy.Module { return new(ACMEManagerMaker) }, + } +} + +// NewManager is a no-op to satisfy the ManagerMaker interface, // because this manager type is a special case. -func (m *ACMEManagerMaker) newManager(interactive bool) (certmagic.Manager, error) { +func (m ACMEManagerMaker) NewManager(interactive bool) (certmagic.Manager, error) { return nil, nil } @@ -203,4 +208,4 @@ func onDemandAskRequest(ask string, name string) error { } // Interface guard -var _ managerMaker = (*ACMEManagerMaker)(nil) +var _ ManagerMaker = (*ACMEManagerMaker)(nil) diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index 8cb6ffe..e061281 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -172,7 +172,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error { // add all the cipher suites in order, without duplicates cipherSuitesAdded := make(map[uint16]struct{}) for _, csName := range p.CipherSuites { - csID := supportedCipherSuites[csName] + csID := SupportedCipherSuites[csName] if _, ok := cipherSuitesAdded[csID]; !ok { cipherSuitesAdded[csID] = struct{}{} cfg.CipherSuites = append(cfg.CipherSuites, csID) @@ -182,7 +182,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error { // add all the curve preferences in order, without duplicates curvesAdded := make(map[tls.CurveID]struct{}) for _, curveName := range p.Curves { - curveID := supportedCurves[curveName] + curveID := SupportedCurves[curveName] if _, ok := curvesAdded[curveID]; !ok { curvesAdded[curveID] = struct{}{} cfg.CurvePreferences = append(cfg.CurvePreferences, curveID) @@ -203,10 +203,10 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error { // min and max protocol versions if p.ProtocolMin != "" { - cfg.MinVersion = supportedProtocols[p.ProtocolMin] + cfg.MinVersion = SupportedProtocols[p.ProtocolMin] } if p.ProtocolMax != "" { - cfg.MaxVersion = supportedProtocols[p.ProtocolMax] + cfg.MaxVersion = SupportedProtocols[p.ProtocolMax] } if p.ProtocolMin > p.ProtocolMax { return fmt.Errorf("protocol min (%x) cannot be greater than protocol max (%x)", p.ProtocolMin, p.ProtocolMax) diff --git a/modules/caddytls/fileloader.go b/modules/caddytls/fileloader.go index 5f277c8..b2cc132 100644 --- a/modules/caddytls/fileloader.go +++ b/modules/caddytls/fileloader.go @@ -23,14 +23,19 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "tls.certificates.load_files", - New: func() interface{} { return fileLoader{} }, - }) + caddy.RegisterModule(FileLoader{}) } -// fileLoader loads certificates and their associated keys from disk. -type fileLoader []CertKeyFilePair +// FileLoader loads certificates and their associated keys from disk. +type FileLoader []CertKeyFilePair + +// CaddyModule returns the Caddy module information. +func (FileLoader) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "tls.certificates.load_files", + New: func() caddy.Module { return new(FileLoader) }, + } +} // CertKeyFilePair pairs certificate and key file names along with their // encoding format so that they can be loaded from disk. @@ -42,7 +47,7 @@ type CertKeyFilePair struct { } // LoadCertificates returns the certificates to be loaded by fl. -func (fl fileLoader) LoadCertificates() ([]Certificate, error) { +func (fl FileLoader) LoadCertificates() ([]Certificate, error) { var certs []Certificate for _, pair := range fl { certData, err := ioutil.ReadFile(pair.Certificate) @@ -73,4 +78,4 @@ func (fl fileLoader) LoadCertificates() ([]Certificate, error) { } // Interface guard -var _ CertificateLoader = (fileLoader)(nil) +var _ CertificateLoader = (FileLoader)(nil) diff --git a/modules/caddytls/folderloader.go b/modules/caddytls/folderloader.go index 24a7fbb..da1dff0 100644 --- a/modules/caddytls/folderloader.go +++ b/modules/caddytls/folderloader.go @@ -28,22 +28,27 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "tls.certificates.load_folders", - New: func() interface{} { return folderLoader{} }, - }) + caddy.RegisterModule(FolderLoader{}) } -// folderLoader loads certificates and their associated keys from disk +// FolderLoader loads certificates and their associated keys from disk // by recursively walking the specified directories, looking for PEM // files which contain both a certificate and a key. -type folderLoader []string +type FolderLoader []string + +// CaddyModule returns the Caddy module information. +func (FolderLoader) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "tls.certificates.load_folders", + New: func() caddy.Module { return new(FolderLoader) }, + } +} // LoadCertificates loads all the certificates+keys in the directories // listed in fl from all files ending with .pem. This method of loading // certificates expects the certificate and key to be bundled into the // same file. -func (fl folderLoader) LoadCertificates() ([]Certificate, error) { +func (fl FolderLoader) LoadCertificates() ([]Certificate, error) { var certs []Certificate for _, dir := range fl { err := filepath.Walk(dir, func(fpath string, info os.FileInfo, err error) error { @@ -135,4 +140,4 @@ func x509CertFromCertAndKeyPEMFile(fpath string) (tls.Certificate, error) { return cert, nil } -var _ CertificateLoader = (folderLoader)(nil) +var _ CertificateLoader = (FolderLoader)(nil) diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go index ee146d4..47fb296 100644 --- a/modules/caddytls/matchers.go +++ b/modules/caddytls/matchers.go @@ -20,14 +20,19 @@ import ( "github.com/caddyserver/caddy/v2" ) +func init() { + caddy.RegisterModule(MatchServerName{}) +} + // MatchServerName matches based on SNI. type MatchServerName []string -func init() { - caddy.RegisterModule(caddy.Module{ +// CaddyModule returns the Caddy module information. +func (MatchServerName) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ Name: "tls.handshake_match.sni", - New: func() interface{} { return MatchServerName{} }, - }) + New: func() caddy.Module { return new(MatchServerName) }, + } } // Match matches hello based on SNI. diff --git a/modules/caddytls/sessiontickets.go b/modules/caddytls/sessiontickets.go index c47f823..2eb0773 100644 --- a/modules/caddytls/sessiontickets.go +++ b/modules/caddytls/sessiontickets.go @@ -29,7 +29,7 @@ import ( // SessionTicketService configures and manages TLS session tickets. type SessionTicketService struct { KeySource json.RawMessage `json:"key_source,omitempty"` - RotationInterval caddy.Duration `json:"rotation_interval,omitempty"` + RotationInterval caddy.Duration `json:"rotation_interval,omitempty"` MaxKeys int `json:"max_keys,omitempty"` DisableRotation bool `json:"disable_rotation,omitempty"` Disabled bool `json:"disabled,omitempty"` diff --git a/modules/caddytls/standardstek/stek.go b/modules/caddytls/standardstek/stek.go index 6a4b1c8..6d10c76 100644 --- a/modules/caddytls/standardstek/stek.go +++ b/modules/caddytls/standardstek/stek.go @@ -24,10 +24,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "tls.stek.standard", - New: func() interface{} { return new(standardSTEKProvider) }, - }) + caddy.RegisterModule(standardSTEKProvider{}) } type standardSTEKProvider struct { @@ -35,6 +32,14 @@ type standardSTEKProvider struct { timer *time.Timer } +// CaddyModule returns the Caddy module information. +func (standardSTEKProvider) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "tls.stek.standard", + New: func() caddy.Module { return new(standardSTEKProvider) }, + } +} + // Initialize sets the configuration for s and returns the starting keys. func (s *standardSTEKProvider) Initialize(config *caddytls.SessionTicketService) ([][32]byte, error) { // keep a reference to the config; we'll need it when rotating keys diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index e70fbd1..88b7790 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -30,10 +30,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "tls", - New: func() interface{} { return new(TLS) }, - }) + caddy.RegisterModule(TLS{}) // opt-in TLS 1.3 for Go1.12 // TODO: remove this line when Go1.13 is released. @@ -45,14 +42,22 @@ func init() { // TLS represents a process-wide TLS configuration. type TLS struct { Certificates map[string]json.RawMessage `json:"certificates,omitempty"` - Automation AutomationConfig `json:"automation,omitempty"` - SessionTickets SessionTicketService `json:"session_tickets,omitempty"` + Automation AutomationConfig `json:"automation"` + SessionTickets SessionTicketService `json:"session_tickets"` certificateLoaders []CertificateLoader certCache *certmagic.Cache ctx caddy.Context } +// CaddyModule returns the Caddy module information. +func (TLS) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "tls", + New: func() caddy.Module { return new(TLS) }, + } +} + // Provision sets up the configuration for the TLS app. func (t *TLS) Provision(ctx caddy.Context) error { t.ctx = ctx @@ -73,7 +78,7 @@ func (t *TLS) Provision(ctx caddy.Context) error { if err != nil { return fmt.Errorf("loading TLS automation management module: %s", err) } - t.Automation.Policies[i].Management = val.(managerMaker) + t.Automation.Policies[i].Management = val.(ManagerMaker) t.Automation.Policies[i].ManagementRaw = nil // allow GC to deallocate - TODO: Does this help? } @@ -105,16 +110,12 @@ func (t *TLS) Provision(ctx caddy.Context) error { onDemandRateLimiter.SetLimit(0) } - return nil -} - -// Start activates the TLS module. -func (t *TLS) Start() error { + // load manual/static (unmanaged) certificates - we do this in + // provision so that other apps (such as http) can know which + // certificates have been manually loaded magic := certmagic.New(t.certCache, certmagic.Config{ - Storage: t.ctx.Storage(), + Storage: ctx.Storage(), }) - - // load manual/static (unmanaged) certificates for _, loader := range t.certificateLoaders { certs, err := loader.LoadCertificates() if err != nil { @@ -128,6 +129,11 @@ func (t *TLS) Start() error { } } + return nil +} + +// Start activates the TLS module. +func (t *TLS) Start() error { // load automated (managed) certificates if automatedRawMsg, ok := t.Certificates[automateKey]; ok { var names []string @@ -204,6 +210,12 @@ func (t *TLS) getAutomationPolicyForName(name string) AutomationPolicy { return AutomationPolicy{Management: mgmt} } +// CertificatesWithSAN returns the list of all certificates +// in the cache the match the given SAN value. +func (t *TLS) CertificatesWithSAN(san string) []certmagic.Certificate { + return t.certCache.CertificatesWithSAN(san) +} + // CertificateLoader is a type that can load certificates. // Certificates can optionally be associated with tags. type CertificateLoader interface { @@ -230,7 +242,7 @@ type AutomationPolicy struct { Hosts []string `json:"hosts,omitempty"` ManagementRaw json.RawMessage `json:"management,omitempty"` - Management managerMaker `json:"-"` + Management ManagerMaker `json:"-"` } // makeCertMagicConfig converts ap into a CertMagic config. Passing onDemand @@ -245,7 +257,7 @@ func (ap AutomationPolicy) makeCertMagicConfig(ctx caddy.Context) certmagic.Conf } return certmagic.Config{ - NewManager: ap.Management.newManager, + NewManager: ap.Management.NewManager, } } @@ -283,9 +295,9 @@ type RateLimit struct { Burst int `json:"burst,omitempty"` } -// managerMaker makes a certificate manager. -type managerMaker interface { - newManager(interactive bool) (certmagic.Manager, error) +// ManagerMaker makes a certificate manager. +type ManagerMaker interface { + NewManager(interactive bool) (certmagic.Manager, error) } // These perpetual values are used for on-demand TLS. diff --git a/modules/caddytls/values.go b/modules/caddytls/values.go index 0c62058..b10fe22 100644 --- a/modules/caddytls/values.go +++ b/modules/caddytls/values.go @@ -22,12 +22,16 @@ import ( "github.com/klauspost/cpuid" ) -// supportedCipherSuites is the unordered map of cipher suite +// SupportedCipherSuites is the unordered map of cipher suite // string names to their definition in crypto/tls. All values // should be IANA-reserved names. See // https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml +// Two of the cipher suite constants in the standard lib do not use the +// full IANA name, but we do; see: +// https://github.com/golang/go/issues/32061 and +// https://github.com/golang/go/issues/30325#issuecomment-512862374. // TODO: might not be needed much longer: https://github.com/golang/go/issues/30325 -var supportedCipherSuites = map[string]uint16{ +var SupportedCipherSuites = map[string]uint16{ "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, @@ -84,22 +88,24 @@ func getOptimalDefaultCipherSuites() []uint16 { return defaultCipherSuitesWithoutAESNI } -// supportedCurves is the unordered map of supported curves. +// SupportedCurves is the unordered map of supported curves. // https://golang.org/pkg/crypto/tls/#CurveID -var supportedCurves = map[string]tls.CurveID{ - "X25519": tls.X25519, - "P256": tls.CurveP256, - "P384": tls.CurveP384, - "P521": tls.CurveP521, +var SupportedCurves = map[string]tls.CurveID{ + // TODO: Use IANA names, probably? see https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8 + // All named crypto/elliptic curves have secpXXXr1 IANA names. + "x25519": tls.X25519, // x25519, 29 + "p256": tls.CurveP256, // secp256r1, 23 + "p384": tls.CurveP384, // secp384r1, 24 + "p521": tls.CurveP521, // secp521r1, 25 } // supportedCertKeyTypes is all the key types that are supported // for certificates that are obtained through ACME. var supportedCertKeyTypes = map[string]certcrypto.KeyType{ - "RSA2048": certcrypto.RSA2048, - "RSA4096": certcrypto.RSA4096, - "P256": certcrypto.EC256, - "P384": certcrypto.EC384, + "rsa_2048": certcrypto.RSA2048, + "rsa_4096": certcrypto.RSA4096, + "ec_p256": certcrypto.EC256, + "ec_p384": certcrypto.EC384, } // defaultCurves is the list of only the curves we want to use @@ -115,9 +121,9 @@ var defaultCurves = []tls.CurveID{ tls.CurveP256, } -// supportedProtocols is a map of supported protocols. -// HTTP/2 only supports TLS 1.2 and higher. -var supportedProtocols = map[string]uint16{ +// SupportedProtocols is a map of supported protocols. +// Note that HTTP/2 only supports TLS 1.2 and higher. +var SupportedProtocols = map[string]uint16{ "tls1.0": tls.VersionTLS10, "tls1.1": tls.VersionTLS11, "tls1.2": tls.VersionTLS12, diff --git a/modules_test.go b/modules_test.go index ecba21f..ef7edf7 100644 --- a/modules_test.go +++ b/modules_test.go @@ -21,7 +21,7 @@ import ( func TestGetModules(t *testing.T) { modulesMu.Lock() - modules = map[string]Module{ + modules = map[string]ModuleInfo{ "a": {Name: "a"}, "a.b": {Name: "a.b"}, "a.b.c": {Name: "a.b.c"}, @@ -38,11 +38,11 @@ func TestGetModules(t *testing.T) { for i, tc := range []struct { input string - expect []Module + expect []ModuleInfo }{ { input: "", - expect: []Module{ + expect: []ModuleInfo{ {Name: "a"}, {Name: "b"}, {Name: "c"}, @@ -50,7 +50,7 @@ func TestGetModules(t *testing.T) { }, { input: "a", - expect: []Module{ + expect: []ModuleInfo{ {Name: "a.b"}, {Name: "a.c"}, {Name: "a.d"}, @@ -58,7 +58,7 @@ func TestGetModules(t *testing.T) { }, { input: "a.b", - expect: []Module{ + expect: []ModuleInfo{ {Name: "a.b.c"}, {Name: "a.b.cd"}, }, @@ -68,7 +68,7 @@ func TestGetModules(t *testing.T) { }, { input: "b", - expect: []Module{ + expect: []ModuleInfo{ {Name: "b.a"}, {Name: "b.b"}, }, @@ -23,10 +23,7 @@ import ( ) func init() { - RegisterModule(Module{ - Name: "caddy.storage.file_system", - New: func() interface{} { return new(fileStorage) }, - }) + RegisterModule(fileStorage{}) } // StorageConverter is a type that can convert itself @@ -43,6 +40,14 @@ type fileStorage struct { Root string `json:"root"` } +// CaddyModule returns the Caddy module information. +func (fileStorage) CaddyModule() ModuleInfo { + return ModuleInfo{ + Name: "caddy.storage.file_system", + New: func() Module { return new(fileStorage) }, + } +} + func (s fileStorage) CertMagicStorage() (certmagic.Storage, error) { return &certmagic.FileStorage{Path: s.Root}, nil } |