diff --git a/admin.go b/admin.go index ba3f0602..77999130 100644 --- a/admin.go +++ b/admin.go @@ -95,9 +95,9 @@ func StartAdmin(initialConfigJSON []byte) error { mux.HandleFunc("/debug/pprof/trace", pprof.Trace) ///// END PPROF STUFF ////// - for _, m := range GetModules("admin") { - routes := m.New().([]AdminRoute) - for _, route := range routes { + for _, m := range GetModules("admin.routers") { + adminrtr := m.New().(AdminRouter) + for _, route := range adminrtr.Routes() { mux.Handle(route.Pattern, route) } } @@ -146,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 index ab4905ad..377f77b6 100644 --- a/caddyconfig/caddyfile/adapter.go +++ b/caddyconfig/caddyfile/adapter.go @@ -37,14 +37,12 @@ func (a Adapter) Adapt(body []byte, options map[string]string) ([]byte, []caddyc options = make(map[string]string) } - directives := a.ServerType.ValidDirectives() - filename := options["filename"] if filename == "" { filename = "Caddyfile" } - serverBlocks, err := Parse(filename, bytes.NewReader(body), directives) + serverBlocks, err := Parse(filename, bytes.NewReader(body)) if err != nil { return nil, nil, err } @@ -77,10 +75,6 @@ type Unmarshaler interface { // ServerType is a type that can evaluate a Caddyfile and set up a caddy config. type ServerType interface { - // ValidDirectives returns a list of the - // server type's recognized directives. - ValidDirectives() []string - // Setup takes the server blocks which // contain tokens, as well as options // (e.g. CLI flags) and creates a Caddy diff --git a/caddyconfig/caddyfile/dispenser.go b/caddyconfig/caddyfile/dispenser.go index 1cf5d048..0d2c7899 100755 --- a/caddyconfig/caddyfile/dispenser.go +++ b/caddyconfig/caddyfile/dispenser.go @@ -31,6 +31,7 @@ type Dispenser struct { } // 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, @@ -51,15 +52,15 @@ func (d *Dispenser) Next() bool { } // Prev moves to the previous token. It does the inverse -// of Next(). Generally, this should only be used in -// special cases such as deleting a token from the slice -// that d is iterating. In that case, without using Prev(), -// the dispenser would be pointing at the wrong token since -// deleting a token implicitly advances the cursor. +// 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 > 0 { + if d.cursor > -1 { d.cursor-- - return true + return d.cursor > -1 } return false } @@ -223,8 +224,7 @@ func (d *Dispenser) RemainingArgs() []string { // "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 { - var tkns []Token - tkns = append(tkns, d.Token()) + tkns := []Token{d.Token()} for d.NextArg() { tkns = append(tkns, d.Token()) } @@ -245,10 +245,14 @@ func (d *Dispenser) NewFromNextTokens() *Dispenser { // Token returns the current token. func (d *Dispenser) Token() Token { - if d.cursor < 0 || d.cursor >= len(d.tokens) { + return d.TokenAt(d.cursor) +} + +func (d *Dispenser) TokenAt(cursor int) Token { + if cursor < 0 || cursor >= len(d.tokens) { return Token{} } - return d.tokens[d.cursor] + return d.tokens[cursor] } // Cursor returns the current cursor (token index). @@ -256,6 +260,10 @@ 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 diff --git a/caddyconfig/caddyfile/parse.go b/caddyconfig/caddyfile/parse.go index cc91e3dd..e5b25fc3 100755 --- a/caddyconfig/caddyfile/parse.go +++ b/caddyconfig/caddyfile/parse.go @@ -28,12 +28,12 @@ import ( // 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, validDirectives []string) ([]ServerBlock, error) { +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), validDirectives: validDirectives} + p := parser{Dispenser: NewDispenser(filename, tokens)} return p.parseAll() } @@ -56,9 +56,9 @@ func allTokens(input io.Reader) ([]Token, error) { type parser struct { *Dispenser block ServerBlock // current server block being parsed - validDirectives []string // a directive must be valid or it's an error eof bool // if we encounter a valid EOF in a hard place definedSnippets map[string][]Token + nesting int } func (p *parser) parseAll() ([]ServerBlock, error) { @@ -72,14 +72,16 @@ func (p *parser) parseAll() ([]ServerBlock, error) { 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{Tokens: make(map[string][]Token)} - + p.block = ServerBlock{} return p.begin() } @@ -186,7 +188,7 @@ func (p *parser) blockContents() error { return err } - // Only look for close curly brace if there was an opening + // only look for close curly brace if there was an opening if errOpenCurlyBrace == nil { err = p.closeCurlyBrace() if err != nil { @@ -205,6 +207,7 @@ func (p *parser) directives() error { for p.Next() { // end of server block if p.Val() == "}" { + // p.nesting has already been decremented break } @@ -218,11 +221,15 @@ func (p *parser) directives() error { continue } - // normal case: parse a directive on this line + // 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 } @@ -345,25 +352,24 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) { // are loaded into the current server block for later use // by directive setup functions. func (p *parser) directive() error { - dir := replaceEnvVars(p.Val()) - nesting := 0 + // evaluate any env vars in directive token + p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text) - if !p.validDirective(dir) { - return p.Errf("Unknown directive '%s'", dir) - } + // a segment is a list of tokens associated with this directive + var segment Segment - // The directive itself is appended as a relevant token - p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor]) + // the directive itself is appended as a relevant token + segment = append(segment, p.Token()) for p.Next() { if p.Val() == "{" { - nesting++ - } else if p.isNewLine() && nesting == 0 { + p.nesting++ + } else if p.isNewLine() && p.nesting == 0 { p.cursor-- // read too far break - } else if p.Val() == "}" && nesting > 0 { - nesting-- - } else if p.Val() == "}" && nesting == 0 { + } 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 { @@ -373,12 +379,15 @@ func (p *parser) directive() error { continue } p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text) - p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor]) + segment = append(segment, p.Token()) } - if nesting > 0 { + p.block.Segments = append(p.block.Segments, segment) + + if p.nesting > 0 { return p.EOFErr() } + return nil } @@ -404,19 +413,6 @@ func (p *parser) closeCurlyBrace() error { return nil } -// validDirective returns true if dir is in p.validDirectives. -func (p *parser) validDirective(dir string) bool { - if p.validDirectives == nil { - return true - } - for _, d := range p.validDirectives { - if d == dir { - return true - } - } - return false -} - // replaceEnvVars replaces environment variables that appear in the token // and understands both the $UNIX and %WINDOWS% syntaxes. func replaceEnvVars(s string) string { @@ -447,13 +443,6 @@ func replaceEnvReferences(s, refStart, refEnd string) string { return s } -// ServerBlock associates any number of keys (usually addresses -// of some sort) with tokens (grouped by directive name). -type ServerBlock struct { - Keys []string - Tokens map[string][]Token -} - func (p *parser) isSnippet() (bool, string) { keys := p.block.Keys // A snippet block is a single key with parens. Nothing else qualifies. @@ -480,6 +469,7 @@ func (p *parser) snippetTokens() ([]Token, error) { } } if p.Val() == "{" { + p.nesting++ count++ } tokens = append(tokens, p.tokens[p.cursor]) @@ -490,3 +480,43 @@ func (p *parser) snippetTokens() ([]Token, error) { } 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 index 654c68d2..19959de8 100755 --- a/caddyconfig/caddyfile/parse_test.go +++ b/caddyconfig/caddyfile/parse_test.go @@ -22,6 +22,8 @@ import ( "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"} @@ -53,84 +55,67 @@ func TestParseOneAndImport(t *testing.T) { input string shouldErr bool keys []string - tokens map[string]int // map of directive name to number of tokens expected + numTokens []int // number of tokens to expect in each segment }{ {`localhost`, false, []string{ "localhost", - }, map[string]int{}}, + }, []int{}}, {`localhost dir1`, false, []string{ "localhost", - }, map[string]int{ - "dir1": 1, - }}, + }, []int{1}}, {`localhost:1234 dir1 foo bar`, false, []string{ "localhost:1234", - }, map[string]int{ - "dir1": 3, - }}, + }, []int{3}, + }, {`localhost { dir1 }`, false, []string{ "localhost", - }, map[string]int{ - "dir1": 1, - }}, + }, []int{1}}, {`localhost:1234 { dir1 foo bar dir2 }`, false, []string{ "localhost:1234", - }, map[string]int{ - "dir1": 3, - "dir2": 1, - }}, + }, []int{3, 1}}, {`http://localhost https://localhost dir1 foo bar`, false, []string{ "http://localhost", "https://localhost", - }, map[string]int{ - "dir1": 3, - }}, + }, []int{3}}, {`http://localhost https://localhost { dir1 foo bar }`, false, []string{ "http://localhost", "https://localhost", - }, map[string]int{ - "dir1": 3, - }}, + }, []int{3}}, {`http://localhost, https://localhost { dir1 foo bar }`, false, []string{ "http://localhost", "https://localhost", - }, map[string]int{ - "dir1": 3, - }}, + }, []int{3}}, {`http://localhost, { }`, true, []string{ "http://localhost", - }, map[string]int{}}, + }, []int{}}, {`host1:80, http://host2.com dir1 foo bar dir2 baz`, false, []string{ "host1:80", "http://host2.com", - }, map[string]int{ - "dir1": 3, - "dir2": 2, - }}, + }, []int{3, 2}}, {`http://host1.com, http://host2.com, @@ -138,7 +123,7 @@ func TestParseOneAndImport(t *testing.T) { "http://host1.com", "http://host2.com", "https://host3.com", - }, map[string]int{}}, + }, []int{}}, {`http://host1.com:1234, https://host2.com dir1 foo { @@ -147,10 +132,7 @@ func TestParseOneAndImport(t *testing.T) { dir2`, false, []string{ "http://host1.com:1234", "https://host2.com", - }, map[string]int{ - "dir1": 6, - "dir2": 1, - }}, + }, []int{6, 1}}, {`127.0.0.1 dir1 { @@ -160,34 +142,25 @@ func TestParseOneAndImport(t *testing.T) { foo bar }`, false, []string{ "127.0.0.1", - }, map[string]int{ - "dir1": 5, - "dir2": 5, - }}, + }, []int{5, 5}}, {`localhost dir1 { foo`, true, []string{ "localhost", - }, map[string]int{ - "dir1": 3, - }}, + }, []int{3}}, {`localhost dir1 { }`, false, []string{ "localhost", - }, map[string]int{ - "dir1": 3, - }}, + }, []int{3}}, {`localhost dir1 { } }`, true, []string{ "localhost", - }, map[string]int{ - "dir1": 3, - }}, + }, []int{}}, {`localhost dir1 { @@ -197,48 +170,38 @@ func TestParseOneAndImport(t *testing.T) { } dir2 foo bar`, false, []string{ "localhost", - }, map[string]int{ - "dir1": 7, - "dir2": 3, - }}, + }, []int{7, 3}}, - {``, false, []string{}, map[string]int{}}, + {``, false, []string{}, []int{}}, {`localhost dir1 arg1 import testdata/import_test1.txt`, false, []string{ "localhost", - }, map[string]int{ - "dir1": 2, - "dir2": 3, - "dir3": 1, - }}, + }, []int{2, 3, 1}}, {`import testdata/import_test2.txt`, false, []string{ "host1", - }, map[string]int{ - "dir1": 1, - "dir2": 2, - }}, + }, []int{1, 2}}, - {`import testdata/import_test1.txt testdata/import_test2.txt`, true, []string{}, map[string]int{}}, + {`import testdata/import_test1.txt testdata/import_test2.txt`, true, []string{}, []int{}}, - {`import testdata/not_found.txt`, true, []string{}, map[string]int{}}, + {`import testdata/not_found.txt`, true, []string{}, []int{}}, - {`""`, false, []string{}, map[string]int{}}, + {`""`, false, []string{}, []int{}}, - {``, false, []string{}, map[string]int{}}, + {``, false, []string{}, []int{}}, // test cases found by fuzzing! - {`import }{$"`, true, []string{}, map[string]int{}}, - {`import /*/*.txt`, true, []string{}, map[string]int{}}, - {`import /???/?*?o`, true, []string{}, map[string]int{}}, - {`import /??`, true, []string{}, map[string]int{}}, - {`import /[a-z]`, true, []string{}, map[string]int{}}, - {`import {$}`, true, []string{}, map[string]int{}}, - {`import {%}`, true, []string{}, map[string]int{}}, - {`import {$$}`, true, []string{}, map[string]int{}}, - {`import {%%}`, true, []string{}, map[string]int{}}, + {`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) @@ -261,15 +224,16 @@ func TestParseOneAndImport(t *testing.T) { } } - if len(result.Tokens) != len(test.tokens) { - t.Errorf("Test %d: Expected %d directives, had %d", - i, len(test.tokens), len(result.Tokens)) + 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 directive, tokens := range result.Tokens { - if len(tokens) != test.tokens[directive] { - t.Errorf("Test %d, directive '%s': Expected %d tokens, counted %d", - i, directive, test.tokens[directive], len(tokens)) + + 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 } } @@ -289,12 +253,12 @@ func TestRecursiveImport(t *testing.T) { t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys) return false } - if len(got.Tokens) != 2 { - t.Errorf("got wrong number of tokens: expect 2, got %d", len(got.Tokens)) + if len(got.Segments) != 2 { + t.Errorf("got wrong number of segments: expect 2, got %d", len(got.Segments)) return false } - if len(got.Tokens["dir1"]) != 1 || len(got.Tokens["dir2"]) != 2 { - t.Errorf("got unexpect tokens: %v", got.Tokens) + if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 2 { + t.Errorf("got unexpect tokens: %v", got.Segments) return false } return true @@ -384,12 +348,12 @@ func TestDirectiveImport(t *testing.T) { t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys) return false } - if len(got.Tokens) != 2 { - t.Errorf("got wrong number of tokens: expect 2, got %d", len(got.Tokens)) + if len(got.Segments) != 2 { + t.Errorf("got wrong number of segments: expect 2, got %d", len(got.Segments)) return false } - if len(got.Tokens["dir1"]) != 1 || len(got.Tokens["proxy"]) != 8 { - t.Errorf("got unexpect tokens: %v", got.Tokens) + if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 8 { + t.Errorf("got unexpect tokens: %v", got.Segments) return false } return true @@ -557,21 +521,21 @@ func TestEnvironmentReplacement(t *testing.T) { 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].Tokens["dir1"][1].Text, "foobar"; 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].Tokens["dir1"][1].Text, "servername.com/foobar"; expected != actual { + 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].Tokens["dir1"][1].Text, "{%ADDRESS}"; expected != actual { + if actual, expected := blocks[0].Segments[0][1].Text, "{%ADDRESS}"; expected != actual { t.Errorf("Expected host to be '%s' but was '%s'", expected, actual) } @@ -585,22 +549,18 @@ func TestEnvironmentReplacement(t *testing.T) { // in quoted field p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"") blocks, _ = p.parseAll() - if actual, expected := blocks[0].Tokens["dir1"][1].Text, "Test foobar test"; expected != actual { + 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].Tokens["answer"][1].Text, "{{ .Name }} foobar"; expected != actual { + 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 testParser(input string) parser { - return parser{Dispenser: newTestDispenser(input)} -} - func TestSnippets(t *testing.T) { p := testParser(` (common) { @@ -617,7 +577,7 @@ func TestSnippets(t *testing.T) { } for _, b := range blocks { t.Log(b.Keys) - t.Log(b.Tokens) + t.Log(b.Segments) } if len(blocks) != 1 { t.Fatalf("Expect exactly one server block. Got %d.", len(blocks)) @@ -625,16 +585,15 @@ func TestSnippets(t *testing.T) { 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].Tokens) != 2 { - t.Fatalf("Server block should have tokens from import") + if len(blocks[0].Segments) != 2 { + t.Fatalf("Server block should have tokens from import, got: %+v", blocks[0]) } - if actual, expected := blocks[0].Tokens["gzip"][0].Text, "gzip"; expected != actual { + 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].Tokens["errors"][1].Text, "stderr"; 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) { @@ -666,9 +625,9 @@ func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) { } for _, b := range blocks { t.Log(b.Keys) - t.Log(b.Tokens) + t.Log(b.Segments) } - auth := blocks[0].Tokens["basicauth"] + 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: @@ -701,7 +660,7 @@ func TestSnippetAcrossMultipleFiles(t *testing.T) { } for _, b := range blocks { t.Log(b.Keys) - t.Log(b.Tokens) + t.Log(b.Segments) } if len(blocks) != 1 { t.Fatalf("Expect exactly one server block. Got %d.", len(blocks)) @@ -709,10 +668,14 @@ func TestSnippetAcrossMultipleFiles(t *testing.T) { 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].Tokens) != 1 { + if len(blocks[0].Segments) != 1 { t.Fatalf("Server block should have tokens from import") } - if actual, expected := blocks[0].Tokens["gzip"][0].Text, "gzip"; expected != actual { + 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/httpcaddyfile/addresses.go b/caddyconfig/httpcaddyfile/addresses.go index 6ecee265..2adb818b 100644 --- a/caddyconfig/httpcaddyfile/addresses.go +++ b/caddyconfig/httpcaddyfile/addresses.go @@ -22,7 +22,7 @@ import ( "strconv" "strings" - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/mholt/certmagic" ) @@ -73,8 +73,8 @@ import ( // 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 []caddyfile.ServerBlock) (map[string][]caddyfile.ServerBlock, error) { - sbmap := make(map[string][]caddyfile.ServerBlock) +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 @@ -83,7 +83,7 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []caddyfile. // 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.Keys { + 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 @@ -105,9 +105,12 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []caddyfile. // 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], caddyfile.ServerBlock{ - Keys: keys, - Tokens: sblock.Tokens, + sbmap[addr] = append(sbmap[addr], serverBlock{ + block: caddyfile.ServerBlock{ + Keys: keys, + Segments: sblock.block.Segments, + }, + pile: sblock.pile, }) } } @@ -123,7 +126,7 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []caddyfile. // 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][]caddyfile.ServerBlock) []sbAddrAssociation { +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 @@ -151,11 +154,12 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]ca return sbaddrs } -func (st *ServerType) listenerAddrsForServerBlockKey(sblock caddyfile.ServerBlock, key string) ([]string, error) { - addr, err := standardizeAddress(key) +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 != "" { @@ -168,11 +172,8 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock caddyfile.ServerBloc // the bind directive specifies hosts, but is optional var lnHosts []string - for i, token := range sblock.Tokens["bind"] { - if i == 0 { - continue - } - lnHosts = append(lnHosts, token.Text) + for _, cfgVal := range sblock.pile["bind"] { + lnHosts = append(lnHosts, cfgVal.Value.([]string)...) } if len(lnHosts) == 0 { lnHosts = []string{""} @@ -205,7 +206,53 @@ type Address struct { Original, Scheme, Host, Port, Path string } -// String returns a human-friendly print of the address. +// 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 "" @@ -235,16 +282,7 @@ func (a Address) String() string { return s } -// VHost returns a sensible concatenation of Host:Port/Path from a. -// It's basically the a.Original but without the scheme. -func (a Address) VHost() string { - if idx := strings.Index(a.Original, "://"); idx > -1 { - return a.Original[idx+3:] - } - return a.Original -} - -// Normalize normalizes URL: turn scheme and host names into lower case +// Normalize returns a normalized version of a. func (a Address) Normalize() Address { path := a.Path if !caseSensitivePath { @@ -266,8 +304,8 @@ func (a Address) Normalize() Address { } } -// Key is similar to String, just replaces scheme and host values with modified values. -// Unlike String it doesn't add anything default (scheme, port, etc) +// 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 != "" { @@ -276,11 +314,11 @@ func (a Address) Key() string { if a.Host != "" { res += a.Host } - if a.Port != "" { - if strings.HasPrefix(a.Original[len(res):], ":"+a.Port) { - // insert port only if the original has its own explicit port - res += ":" + a.Port - } + // 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 @@ -288,63 +326,7 @@ func (a Address) Key() string { return res } -// standardizeAddress parses an address string into a structured format with separate -// scheme, host, port, and path portions, as well as the original input string. -func standardizeAddress(str string) (Address, error) { - httpPort, httpsPort := strconv.Itoa(certmagic.HTTPPort), strconv.Itoa(certmagic.HTTPSPort) - - input := str - - // Split input into components (prepend with // to assert host 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 - } - } - - // repeated or conflicting scheme is confusing, so error - if u.Scheme != "" && (port == "http" || port == "https") { - return Address{}, fmt.Errorf("[%s] scheme specified twice in address", input) - } - - // 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) - } - - // standardize http and https ports to their respective port numbers - if port == "http" { - u.Scheme = "http" - port = httpPort - } else if port == "https" { - u.Scheme = "https" - port = httpsPort - } - - return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err -} - const ( defaultPort = "2015" - caseSensitivePath = false + caseSensitivePath = false // TODO: Used? ) diff --git a/caddyconfig/httpcaddyfile/addresses_test.go b/caddyconfig/httpcaddyfile/addresses_test.go index 7e03d29b..d6aa6f6f 100644 --- a/caddyconfig/httpcaddyfile/addresses_test.go +++ b/caddyconfig/httpcaddyfile/addresses_test.go @@ -1,22 +1,11 @@ -// 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 "testing" +import ( + "strings" + "testing" +) -func TestStandardizeAddress(t *testing.T) { +func TestParseAddress(t *testing.T) { for i, test := range []struct { input string scheme, host, port, path string @@ -31,14 +20,15 @@ func TestStandardizeAddress(t *testing.T) { {`[::1]`, "", "::1", "", "", false}, {`[::1]:1234`, "", "::1", "1234", "", false}, {`:`, "", "", "", "", false}, - {`localhost:http`, "http", "localhost", "80", "", false}, - {`localhost:https`, "https", "localhost", "443", "", false}, - {`:http`, "http", "", "80", "", false}, - {`:https`, "https", "", "443", "", 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 - {`http://localhost:443`, "", "", "", "", true}, // not conventional - {`https://localhost:80`, "", "", "", "", true}, // not conventional + {`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}, @@ -58,10 +48,9 @@ func TestStandardizeAddress(t *testing.T) { {`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}, - {`host:https/path`, "https", "host", "443", "/path", false}, {`/path`, "", "", "", "/path", false}, } { - actual, err := standardizeAddress(test.input) + 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) @@ -88,24 +77,6 @@ func TestStandardizeAddress(t *testing.T) { } } -func TestAddressVHost(t *testing.T) { - for i, test := range []struct { - addr Address - expected string - }{ - {Address{Original: "host:1234"}, "host:1234"}, - {Address{Original: "host:1234/foo"}, "host:1234/foo"}, - {Address{Original: "host/foo"}, "host/foo"}, - {Address{Original: "http://host/foo"}, "host/foo"}, - {Address{Original: "https://host/foo"}, "host/foo"}, - } { - actual := test.addr.VHost() - if actual != test.expected { - t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expected, actual) - } - } -} - func TestAddressString(t *testing.T) { for i, test := range []struct { addr Address @@ -127,3 +98,69 @@ func TestAddressString(t *testing.T) { } } } + +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 index 7e51e46a..0fdfcd5a 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -19,93 +19,221 @@ import ( "fmt" "html" "net/http" + "reflect" - "github.com/caddyserver/caddy/v2/caddyconfig" - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" - "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/caddyserver/caddy/caddyconfig" + "github.com/caddyserver/caddy/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddytls" ) -func (st *ServerType) parseRoot( - tkns []caddyfile.Token, - matcherDefs map[string]map[string]json.RawMessage, - warnings *[]caddyconfig.Warning, -) ([]caddyhttp.Route, error) { - var routes []caddyhttp.Route - - matchersAndTokens, err := st.tokensToMatcherSets(tkns, matcherDefs, warnings) - if err != nil { - return nil, err - } - - for _, mst := range matchersAndTokens { - d := caddyfile.NewDispenser("Caddyfile", mst.tokens) - - var root string - for d.Next() { - if !d.NextArg() { - return nil, d.ArgErr() - } - root = d.Val() - if d.NextArg() { - return nil, d.ArgErr() - } - } - - varsHandler := caddyhttp.VarsMiddleware{"root": root} - route := caddyhttp.Route{ - Handle: []json.RawMessage{ - caddyconfig.JSONModuleObject(varsHandler, "handler", "vars", warnings), - }, - } - if mst.matcherSet != nil { - route.MatcherSets = []map[string]json.RawMessage{mst.matcherSet} - } - - routes = append(routes, route) - } - - return routes, nil +func init() { + RegisterDirective("bind", parseBind) + RegisterDirective("root", parseRoot) + RegisterDirective("tls", parseTLS) + RegisterHandlerDirective("redir", parseRedir) } -func (st *ServerType) parseRedir( - tkns []caddyfile.Token, - matcherDefs map[string]map[string]json.RawMessage, - warnings *[]caddyconfig.Warning, -) ([]caddyhttp.Route, error) { - var routes []caddyhttp.Route +func parseBind(h Helper) ([]ConfigValue, error) { + var lnHosts []string + for h.Next() { + lnHosts = append(lnHosts, h.RemainingArgs()...) + } + return h.NewBindAddresses(lnHosts), nil +} - matchersAndTokens, err := st.tokensToMatcherSets(tkns, matcherDefs, warnings) +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() + } - for _, mst := range matchersAndTokens { - var route caddyhttp.Route + if !h.NextArg() { + return nil, h.ArgErr() + } + root := h.Val() + if h.NextArg() { + return nil, h.ArgErr() + } - d := caddyfile.NewDispenser("Caddyfile", mst.tokens) + 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} + } - for d.Next() { - if !d.NextArg() { - return nil, d.ArgErr() - } - to := d.Val() + return h.NewVarsRoute(route), nil +} - var code string - if d.NextArg() { - code = d.Val() +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] } - if code == "permanent" { - code = "301" + 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 } - 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 = ` + } + + // 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 = ` Redirecting... @@ -115,143 +243,13 @@ func (st *ServerType) parseRedir( Redirecting to %s... ` - safeTo := html.EscapeString(to) - body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo) - } - - handler := caddyhttp.StaticResponse{ - StatusCode: caddyhttp.WeakString(code), - Headers: http.Header{"Location": []string{to}}, - Body: body, - } - - route.Handle = append(route.Handle, - caddyconfig.JSONModuleObject(handler, "handler", "static_response", warnings)) - } - - if mst.matcherSet != nil { - route.MatcherSets = []map[string]json.RawMessage{mst.matcherSet} - } - - routes = append(routes, route) + safeTo := html.EscapeString(to) + body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo) } - return routes, nil -} - -func (st *ServerType) parseTLSAutomationManager(d *caddyfile.Dispenser) (caddytls.ACMEManagerMaker, error) { - var m caddytls.ACMEManagerMaker - - for d.Next() { - firstLine := d.RemainingArgs() - if len(firstLine) == 1 && firstLine[0] != "off" { - m.Email = firstLine[0] - } - - var hasBlock bool - for d.NextBlock() { - hasBlock = true - switch d.Val() { - case "ca": - arg := d.RemainingArgs() - if len(arg) != 1 { - return m, d.ArgErr() - } - m.CA = arg[0] - // TODO: other properties - } - } - - // a naked tls directive is not allowed - if len(firstLine) == 0 && !hasBlock { - return m, d.ArgErr() - } - } - - return m, nil -} - -func (st *ServerType) parseTLSCerts(d *caddyfile.Dispenser) (map[string]caddytls.CertificateLoader, error) { - var fileLoader caddytls.FileLoader - var folderLoader caddytls.FolderLoader - - for d.Next() { - // file loader - firstLine := d.RemainingArgs() - if len(firstLine) == 2 { - fileLoader = append(fileLoader, caddytls.CertKeyFilePair{ - Certificate: firstLine[0], - Key: firstLine[1], - // TODO: tags, for enterprise module's certificate selection - }) - } - - // folder loader - for d.NextBlock() { - if d.Val() == "load" { - folderLoader = append(folderLoader, d.RemainingArgs()...) - } - } - } - - // put configured loaders into the map - loaders := make(map[string]caddytls.CertificateLoader) - if len(fileLoader) > 0 { - loaders["load_files"] = fileLoader - } - if len(folderLoader) > 0 { - loaders["load_folders"] = folderLoader - } - - return loaders, nil -} - -func (st *ServerType) parseTLSConnPolicy(d *caddyfile.Dispenser) (*caddytls.ConnectionPolicy, error) { - cp := new(caddytls.ConnectionPolicy) - - for d.Next() { - for d.NextBlock() { - switch d.Val() { - case "protocols": - args := d.RemainingArgs() - if len(args) == 0 { - return nil, d.SyntaxErr("one or two protocols") - } - if len(args) > 0 { - if _, ok := caddytls.SupportedProtocols[args[0]]; !ok { - return nil, d.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, d.Errf("Wrong protocol name or protocol not supported: '%s'", args[1]) - } - cp.ProtocolMax = args[1] - } - case "ciphers": - for d.NextArg() { - if _, ok := caddytls.SupportedCipherSuites[d.Val()]; !ok { - return nil, d.Errf("Wrong cipher suite name or cipher suite not supported: '%s'", d.Val()) - } - cp.CipherSuites = append(cp.CipherSuites, d.Val()) - } - case "curves": - for d.NextArg() { - if _, ok := caddytls.SupportedCurves[d.Val()]; !ok { - return nil, d.Errf("Wrong curve name or curve not supported: '%s'", d.Val()) - } - cp.Curves = append(cp.Curves, d.Val()) - } - case "alpn": - args := d.RemainingArgs() - if len(args) == 0 { - return nil, d.ArgErr() - } - cp.ALPN = args - } - } - } - - return cp, nil + 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 00000000..526ac87e --- /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 index a90aa4aa..9a29e974 100644 --- a/caddyconfig/httpcaddyfile/handlers.go +++ b/caddyconfig/httpcaddyfile/handlers.go @@ -17,7 +17,6 @@ package httpcaddyfile import ( "encoding/json" "fmt" - "log" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" @@ -55,38 +54,3 @@ func (st *ServerType) parseMatcherDefinitions(d *caddyfile.Dispenser) (map[strin } return matchers, nil } - -// directiveBuckets returns a list of middleware/handler directives. -// Buckets are ordered, and directives should be evaluated in their -// bucket order. Within a bucket, directives are not ordered. Hence, -// the return value has a slice of buckets, where each bucket is a -// map, which is a strongly-typed reminder that directives within a -// bucket are not ordered. -func directiveBuckets() []map[string]struct{} { - directiveBuckets := []map[string]struct{}{ - // prefer odd-numbered buckets; evens are there for contingencies - {}, // 0 - {}, // 1 - keep empty unless necessary - {}, // 2 - {}, // 3 - first handlers, last responders - {}, // 4 - {}, // 5 - middle of chain - {}, // 6 - {}, // 7 - last handlers, first responders - {}, // 8 - {}, // 9 - keep empty unless necessary - {}, // 10 - } - for _, mod := range caddy.GetModules("http.handlers") { - if hd, ok := mod.New().(HandlerDirective); ok { - bucket := hd.Bucket() - if bucket < 0 || bucket >= len(directiveBuckets) { - log.Printf("[ERROR] directive %s: bucket out of range [0-%d): %d; skipping", - mod.Name, len(directiveBuckets), bucket) - continue - } - directiveBuckets[bucket][mod.ID()] = struct{}{} - } - } - return directiveBuckets -} diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index e5bf0483..1c12ccd6 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -17,17 +17,18 @@ package httpcaddyfile import ( "encoding/json" "fmt" + "log" "reflect" + "sort" "strconv" "strings" - "github.com/mholt/certmagic" - "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() { @@ -38,24 +39,57 @@ func init() { type ServerType struct { } -// ValidDirectives returns the list of known directives. -func (ServerType) ValidDirectives() []string { - dirs := []string{"matcher", "root", "tls", "redir"} // TODO: put special-case (hard-coded, or non-handler) directives here - for _, mod := range caddy.GetModules("http.handlers") { - if _, ok := mod.New().(HandlerDirective); ok { - dirs = append(dirs, mod.ID()) - } - } - return dirs -} +// TODO: error on unrecognized directives // 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 { + // 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 { + // TODO: this should be an error + log.Printf("%s not registered", dir) + } + } + } + // map - sbmap, err := st.mapAddressToServerBlocks(originalServerBlocks) + sbmap, err := st.mapAddressToServerBlocks(serverBlocks) if err != nil { return nil, warnings, err } @@ -63,6 +97,22 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock, // reduce pairings := st.consolidateAddrMappings(sbmap) + // TODO: shorthand placeholders + // for _, p := range pairings { + // for _, sblock := range p.serverBlocks { + // for _, tokens := range sblock.Tokens { + // for i := 0; i < len(tokens); i++ { + // switch tokens[i].Text { + // case "{uri}": + // tokens[i].Text = "{http.request.uri}" + // case "{path}": + // tokens[i].Text = "{http.request.uri.path}" + // } + // } + // } + // } + // } + // each pairing of listener addresses to list of server // blocks is basically a server definition servers, err := st.serversFromPairings(pairings, &warnings) @@ -81,45 +131,33 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock, tlsApp := caddytls.TLS{Certificates: make(map[string]json.RawMessage)} for _, p := range pairings { for _, sblock := range p.serverBlocks { - if tkns, ok := sblock.Tokens["tls"]; ok { - // extract all unique hostnames from the server block - // keys, then convert to a slice for use in the TLS app - hostMap := make(map[string]struct{}) - for _, sblockKey := range sblock.Keys { - addr, err := standardizeAddress(sblockKey) + // 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, fmt.Errorf("parsing server block key: %v", err) + return nil, warnings, err } - hostMap[addr.Host] = struct{}{} - } - sblockHosts := make([]string, 0, len(hostMap)) - for host := range hostMap { - sblockHosts = append(sblockHosts, host) + tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, caddytls.AutomationPolicy{ + Hosts: sblockHosts, + ManagementRaw: caddyconfig.JSONModuleObject(mm, "module", mm.(caddy.Module).CaddyModule().ID(), &warnings), + }) } + } - // parse tokens to get ACME manager config - acmeMgr, err := st.parseTLSAutomationManager(caddyfile.NewDispenser("Caddyfile", tkns)) - if err != nil { - return nil, warnings, err - } - - tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, caddytls.AutomationPolicy{ - Hosts: sblockHosts, - ManagementRaw: caddyconfig.JSONModuleObject(acmeMgr, "module", "acme", &warnings), - }) - - // parse tokens to get certificates to be loaded manually - certLoaders, err := st.parseTLSCerts(caddyfile.NewDispenser("Caddyfile", tkns)) - if err != nil { - return nil, nil, err - } - for loaderName, loader := range certLoaders { + // 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)} @@ -140,10 +178,11 @@ func (st *ServerType) hostsFromServerBlockKeys(sb caddyfile.ServerBlock) ([]stri // first get each unique hostname hostMap := make(map[string]struct{}) for _, sblockKey := range sb.Keys { - addr, err := standardizeAddress(sblockKey) + addr, err := ParseAddress(sblockKey) if err != nil { return nil, fmt.Errorf("parsing server block key: %v", err) } + addr = addr.Normalize() hostMap[addr.Host] = struct{}{} } @@ -167,121 +206,75 @@ func (st *ServerType) serversFromPairings(pairings []sbAddrAssociation, warnings } for _, sblock := range p.serverBlocks { - matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock) + matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock.block) if err != nil { - return nil, fmt.Errorf("server block %v: compiling matcher sets: %v", sblock.Keys, err) + return nil, fmt.Errorf("server block %v: compiling matcher sets: %v", sblock.block.Keys, err) } - // extract matcher definitions - d := caddyfile.NewDispenser("Caddyfile", sblock.Tokens["matcher"]) - matcherDefs, err := st.parseMatcherDefinitions(d) + // 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 } - - siteVarSubroute, handlerSubroute := new(caddyhttp.Subroute), new(caddyhttp.Subroute) - - // built-in directives - - // root: path to root of site - if tkns, ok := sblock.Tokens["root"]; ok { - routes, err := st.parseRoot(tkns, matcherDefs, warnings) - 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) } - siteVarSubroute.Routes = append(siteVarSubroute.Routes, routes...) - } - - // tls: off and conn policies - if tkns, ok := sblock.Tokens["tls"]; ok { - // get the hosts for this server block... - hosts, err := st.hostsFromServerBlockKeys(sblock) - 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) - } - } - - if len(tkns) == 2 && tkns[1].Text == "off" { - // 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 { - // tls connection policies - cp, err := st.parseTLSConnPolicy(caddyfile.NewDispenser("Caddyfile", tkns)) - 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) - } - } - - // set up each handler directive - for _, dirBucket := range directiveBuckets() { - for dir := range dirBucket { - // keep in mind that multiple occurrences of the directive may appear here - tkns, ok := sblock.Tokens[dir] - if !ok { - continue - } - - // extract matcher sets from matcher tokens, if any - matcherSetsMap, err := st.tokensToMatcherSets(tkns, matcherDefs, warnings) - - mod, err := caddy.GetModule("http.handlers." + dir) - if err != nil { - return nil, fmt.Errorf("getting handler module '%s': %v", mod.Name, err) - } - - // the tokens have been divided by matcher set for us, - // so iterate each one and set them up - for _, mst := range matcherSetsMap { - unm, ok := mod.New().(caddyfile.Unmarshaler) - if !ok { - return nil, fmt.Errorf("handler module '%s' is not a Caddyfile unmarshaler", mod.Name) - } - err = unm.UnmarshalCaddyfile(caddyfile.NewDispenser(d.File(), mst.tokens)) + 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 } - handler, ok := unm.(caddyhttp.MiddlewareHandler) - if !ok { - return nil, fmt.Errorf("handler module '%s' does not implement caddyhttp.MiddlewareHandler interface", mod.Name) - } - route := caddyhttp.Route{ - Handle: []json.RawMessage{ - caddyconfig.JSONModuleObject(handler, "handler", dir, warnings), - }, + // 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 } - if mst.matcherSet != nil { - route.MatcherSets = []map[string]json.RawMessage{mst.matcherSet} - } - handlerSubroute.Routes = append(handlerSubroute.Routes, route) + srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp) } - } + // TODO: consolidate equal conn policies } - // redir: static responses that redirect - if tkns, ok := sblock.Tokens["redir"]; ok { - routes, err := st.parseRedir(tkns, matcherDefs, warnings) - if err != nil { - return nil, err + // 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 } - handlerSubroute.Routes = append(handlerSubroute.Routes, routes...) + 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 @@ -298,7 +291,7 @@ func (st *ServerType) serversFromPairings(pairings []sbAddrAssociation, warnings siteSubroute.Routes = append( siteVarSubroute.Routes, caddyhttp.Route{ - Handle: []json.RawMessage{ + HandlersRaw: []json.RawMessage{ caddyconfig.JSONModuleObject(subSubRoute, "handler", "subroute", warnings), }, }, @@ -308,8 +301,8 @@ func (st *ServerType) serversFromPairings(pairings []sbAddrAssociation, warnings siteSubroute.Routes = consolidateRoutes(siteSubroute.Routes) srv.Routes = append(srv.Routes, caddyhttp.Route{ - MatcherSets: matcherSetsEnc, - Handle: []json.RawMessage{ + MatcherSetsRaw: matcherSetsEnc, + HandlersRaw: []json.RawMessage{ caddyconfig.JSONModuleObject(siteSubroute, "handler", "subroute", warnings), }, }) @@ -323,16 +316,32 @@ func (st *ServerType) serversFromPairings(pairings []sbAddrAssociation, warnings 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].MatcherSets, routes[i+1].MatcherSets) && + 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].Handle = append(routes[i].Handle, routes[i+1].Handle...) + routes[i].HandlersRaw = append(routes[i].HandlersRaw, routes[i+1].HandlersRaw...) routes = append(routes[:i+1], routes[i+2:]...) i-- } @@ -340,53 +349,26 @@ func consolidateRoutes(routes caddyhttp.RouteList) caddyhttp.RouteList { return routes } -func (st *ServerType) tokensToMatcherSets( - tkns []caddyfile.Token, - matcherDefs map[string]map[string]json.RawMessage, - warnings *[]caddyconfig.Warning, -) (map[string]matcherSetAndTokens, error) { - m := make(map[string]matcherSetAndTokens) - - for len(tkns) > 0 { - d := caddyfile.NewDispenser("Caddyfile", tkns) - d.Next() // consume directive token - - // look for matcher; it should be the next argument - var matcherToken caddyfile.Token - var matcherSet map[string]json.RawMessage - if d.NextArg() { - var ok bool - var err error - matcherSet, ok, err = st.matcherSetFromMatcherToken(d.Token(), matcherDefs, warnings) - if err != nil { - return nil, err +// 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 ok { - // found a matcher; save it, then splice it out - // since we don't want to parse it again - matcherToken = d.Token() - tkns = d.Delete() + if reflect.DeepEqual(aps[i].ManagementRaw, aps[j].ManagementRaw) { + aps[i].Hosts = append(aps[i].Hosts, aps[j].Hosts...) } - d.RemainingArgs() // advance to end of line + aps = append(aps[:j], aps[j+1:]...) + i-- + break } - for d.NextBlock() { - // skip entire block including any nested blocks; all - // we care about is accessing next directive occurrence - for d.Nested() { - d.NextBlock() - } - } - end := d.Cursor() + 1 - m[matcherToken.Text] = matcherSetAndTokens{ - matcherSet: matcherSet, - tokens: append(m[matcherToken.Text].tokens, tkns[:end]...), - } - tkns = tkns[end:] } - return m, nil + return aps } -func (st *ServerType) matcherSetFromMatcherToken( +func matcherSetFromMatcherToken( tkn caddyfile.Token, matcherDefs map[string]map[string]json.RawMessage, warnings *[]caddyconfig.Warning, @@ -424,10 +406,11 @@ func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([ var matcherPairs []*hostPathPair for _, key := range sblock.Keys { - addr, err := standardizeAddress(key) + 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 @@ -504,14 +487,6 @@ func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (map[string] return msEncoded, nil } -// HandlerDirective implements a directive for an HTTP handler, -// in that it can unmarshal its own configuration from Caddyfile -// tokens and also specify which directive bucket it belongs in. -type HandlerDirective interface { - caddyfile.Unmarshaler - Bucket() int -} - // 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 { @@ -535,7 +510,7 @@ type matcherSetAndTokens struct { // served on those addresses. type sbAddrAssociation struct { addresses []string - serverBlocks []caddyfile.ServerBlock + serverBlocks []serverBlock } // Interface guard diff --git a/context.go b/context.go index 17488e40..2fd84d5f 100644 --- a/context.go +++ b/context.go @@ -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 diff --git a/modules.go b/modules.go index f1b47659..ad03adf7 100644 --- a/modules.go +++ b/modules.go @@ -23,48 +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{} -} - -// ID returns a module's ID, which is the -// last element of its name. -func (m Module) ID() string { - if m.Name == "" { - return "" - } - parts := strings.Split(m.Name, ".") - return parts[len(parts)-1] + // 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 } // Namespace returns the module's namespace (scope) // which is all but the last element of its name. -func (m Module) Namespace() string { - lastDot := strings.LastIndex(m.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 "" + return mi.Name } - return m.Name[:lastDot] + return mi.Name[:lastDot] } -func (m Module) String() string { return m.Name } +// 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() @@ -77,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 @@ -98,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() @@ -110,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, ".") @@ -223,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 21c5b6d8..b4b1ec6e 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -37,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) } @@ -58,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 @@ -227,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 { @@ -331,13 +336,13 @@ func (app *App) automaticHTTPS() error { redirTo += "{http.request.uri}" redirRoutes = append(redirRoutes, Route{ - matcherSets: []MatcherSet{ + MatcherSets: []MatcherSet{ { MatchProtocol("http"), MatchHost(domains), }, }, - handlers: []MiddlewareHandler{ + Handlers: []MiddlewareHandler{ StaticResponse{ StatusCode: WeakString(strconv.Itoa(http.StatusTemporaryRedirect)), // TODO: use permanent redirect instead Headers: http.Header{ diff --git a/modules/caddyhttp/caddylog/log.go b/modules/caddyhttp/caddylog/log.go index 902f60fd..3f636d18 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 0a9f871a..cf055aaf 100644 --- a/modules/caddyhttp/encode/brotli/brotli.go +++ b/modules/caddyhttp/encode/brotli/brotli.go @@ -19,16 +19,13 @@ import ( "strconv" "github.com/andybalholm/brotli" - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "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 @@ -37,6 +34,14 @@ 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() { diff --git a/modules/caddyhttp/encode/caddyfile.go b/modules/caddyhttp/encode/caddyfile.go index 5aca6ac8..5762bd3b 100644 --- a/modules/caddyhttp/encode/caddyfile.go +++ b/modules/caddyhttp/encode/caddyfile.go @@ -22,8 +22,25 @@ import ( "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 [] { @@ -78,8 +95,5 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil } -// Bucket returns the HTTP Caddyfile handler bucket number. -func (enc Encode) Bucket() int { return 3 } - // Interface guard -var _ httpcaddyfile.HandlerDirective = (*Encode)(nil) +var _ caddyfile.Unmarshaler = (*Encode)(nil) diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go index 4e5f7437..723b988a 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,6 +47,14 @@ 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 { for modName, rawMsg := range enc.EncodingsRaw { diff --git a/modules/caddyhttp/encode/gzip/gzip.go b/modules/caddyhttp/encode/gzip/gzip.go index 87e8816a..d6d67f71 100644 --- a/modules/caddyhttp/encode/gzip/gzip.go +++ b/modules/caddyhttp/encode/gzip/gzip.go @@ -20,16 +20,13 @@ import ( "fmt" "strconv" - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "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. @@ -37,6 +34,14 @@ 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() { diff --git a/modules/caddyhttp/encode/zstd/zstd.go b/modules/caddyhttp/encode/zstd/zstd.go index 36226285..f2b4e858 100644 --- a/modules/caddyhttp/encode/zstd/zstd.go +++ b/modules/caddyhttp/encode/zstd/zstd.go @@ -15,22 +15,27 @@ package caddyzstd import ( - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "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 diff --git a/modules/caddyhttp/fileserver/caddyfile.go b/modules/caddyhttp/fileserver/caddyfile.go index 7d9ddd9d..6fa94e7b 100644 --- a/modules/caddyhttp/fileserver/caddyfile.go +++ b/modules/caddyhttp/fileserver/caddyfile.go @@ -15,59 +15,58 @@ package fileserver import ( - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "encoding/json" + + "github.com/caddyserver/caddy/modules/caddyhttp/rewrite" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) -// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: -// -// file_server [] [browse] { -// hide -// index -// browse [] -// root -// } -// -// If browse is given on the first line, it can't be used in the block also. -// The default root is the one given by the root directive. -func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - args := d.RemainingArgs() +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 d.ArgErr() + return nil, h.ArgErr() } fsrv.Browse = new(Browse) default: - return d.ArgErr() + return nil, h.ArgErr() } - for d.NextBlock() { - switch d.Val() { + for h.NextBlock() { + switch h.Val() { case "hide": - fsrv.Hide = d.RemainingArgs() + fsrv.Hide = h.RemainingArgs() if len(fsrv.Hide) == 0 { - return d.ArgErr() + return nil, h.ArgErr() } case "index": - fsrv.IndexNames = d.RemainingArgs() + fsrv.IndexNames = h.RemainingArgs() if len(fsrv.Hide) == 0 { - return d.ArgErr() + return nil, h.ArgErr() } case "root": - if !d.Args(&fsrv.Root) { - return d.ArgErr() + if !h.Args(&fsrv.Root) { + return nil, h.ArgErr() } case "browse": if fsrv.Browse != nil { - return d.Err("browsing is already configured") + return nil, h.Err("browsing is already configured") } fsrv.Browse = new(Browse) - d.Args(&fsrv.Browse.TemplateFile) + h.Args(&fsrv.Browse.TemplateFile) default: - return d.Errf("unknown subdirective '%s'", d.Val()) + return nil, h.Errf("unknown subdirective '%s'", h.Val()) } } } @@ -77,11 +76,29 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { fsrv.Root = "{http.var.root}" } - return nil + return &fsrv, nil } -// Bucket returns the HTTP Caddyfile handler bucket number. -func (fsrv FileServer) Bucket() int { return 7 } +func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { + if !h.Next() { + return nil, h.ArgErr() + } -// Interface guard -var _ httpcaddyfile.HandlerDirective = (*FileServer)(nil) + 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 eca0e8fa..b0912504 100644 --- a/modules/caddyhttp/fileserver/matcher.go +++ b/modules/caddyhttp/fileserver/matcher.go @@ -20,16 +20,13 @@ import ( "os" "time" - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "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 @@ -52,12 +49,20 @@ 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 // try_files -// try_policy +// try_policy first_exist|smallest_size|largest_size|most_recent_modified // } // func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { @@ -82,6 +87,9 @@ func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } } } + if m.Root == "" { + m.Root = "{http.var.root}" + } return nil } @@ -121,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 1b542cf7..cdac453c 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. @@ -50,6 +47,14 @@ type FileServer struct { Browse *Browse `json:"browse,omitempty"` } +// 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. func (fsrv *FileServer) Provision(ctx caddy.Context) error { if fsrv.IndexNames == nil { diff --git a/modules/caddyhttp/headers/caddyfile.go b/modules/caddyhttp/headers/caddyfile.go index 8d320e51..5eaf0648 100644 --- a/modules/caddyhttp/headers/caddyfile.go +++ b/modules/caddyhttp/headers/caddyfile.go @@ -18,11 +18,15 @@ import ( "net/http" "strings" - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) -// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: +func init() { + httpcaddyfile.RegisterHandlerDirective("headers", parseCaddyfile) +} + +// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax: // // headers [] [[+|-] ] { // [+][] [] @@ -31,62 +35,57 @@ import ( // // 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 (h *Headers) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { +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 d.NextArg() { + if h.NextArg() { hasArgs = true - field := d.Val() - d.NextArg() - value := d.Val() - h.processCaddyfileLine(field, value) + field := h.Val() + h.NextArg() + value := h.Val() + processCaddyfileLine(hdr, field, value) } // if not, they should be in a block - for d.NextBlock() { + for h.NextBlock() { if hasArgs { - return d.Err("cannot specify headers in both arguments and block") + return nil, h.Err("cannot specify headers in both arguments and block") } - field := d.Val() + field := h.Val() var value string - if d.NextArg() { - value = d.Val() + if h.NextArg() { + value = h.Val() } - h.processCaddyfileLine(field, value) + processCaddyfileLine(hdr, field, value) } } - return nil + return hdr, nil } -func (h *Headers) processCaddyfileLine(field, value string) { +func processCaddyfileLine(hdr *Headers, field, value string) { if strings.HasPrefix(field, "+") { - if h.Response == nil { - h.Response = &RespHeaderOps{HeaderOps: new(HeaderOps)} + if hdr.Response == nil { + hdr.Response = &RespHeaderOps{HeaderOps: new(HeaderOps)} } - if h.Response.Add == nil { - h.Response.Add = make(http.Header) + if hdr.Response.Add == nil { + hdr.Response.Add = make(http.Header) } - h.Response.Add.Set(field[1:], value) + hdr.Response.Add.Set(field[1:], value) } else if strings.HasPrefix(field, "-") { - if h.Response == nil { - h.Response = &RespHeaderOps{HeaderOps: new(HeaderOps)} + if hdr.Response == nil { + hdr.Response = &RespHeaderOps{HeaderOps: new(HeaderOps)} } - h.Response.Delete = append(h.Response.Delete, field[1:]) - h.Response.Deferred = true + hdr.Response.Delete = append(hdr.Response.Delete, field[1:]) + hdr.Response.Deferred = true } else { - if h.Response == nil { - h.Response = &RespHeaderOps{HeaderOps: new(HeaderOps)} + if hdr.Response == nil { + hdr.Response = &RespHeaderOps{HeaderOps: new(HeaderOps)} } - if h.Response.Set == nil { - h.Response.Set = make(http.Header) + if hdr.Response.Set == nil { + hdr.Response.Set = make(http.Header) } - h.Response.Set.Set(field, value) + hdr.Response.Set.Set(field, value) } } - -// Bucket returns the HTTP Caddyfile handler bucket number. -func (h Headers) Bucket() int { return 3 } - -// Interface guard -var _ httpcaddyfile.HandlerDirective = (*Headers)(nil) diff --git a/modules/caddyhttp/headers/headers.go b/modules/caddyhttp/headers/headers.go index 8f4976a5..e7400041 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 3ba4d029..122aad6e 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 72b54762..0dac1518 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -80,50 +80,25 @@ 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. @@ -165,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 { @@ -186,19 +169,39 @@ func (m MatchPath) Match(r *http.Request) bool { // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchPath) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - *m = d.RemainingArgs() + 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 { - *m = d.RemainingArgs() + for d.Next() { + *m = d.RemainingArgs() + } return nil } @@ -212,6 +215,14 @@ 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() { @@ -237,6 +248,14 @@ 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() { @@ -270,6 +289,14 @@ 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 { @@ -319,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) { @@ -334,14 +369,24 @@ func (m MatchProtocol) Match(r *http.Request) bool { // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchProtocol) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - var proto string - if !d.Args(&proto) { - return d.Err("expected exactly one protocol") + for d.Next() { + var proto string + if !d.Args(&proto) { + return d.Err("expected exactly one protocol") + } + *m = MatchProtocol(proto) } - *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 @@ -375,9 +420,19 @@ 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 { - m.Ranges = d.RemainingArgs() + for d.Next() { + m.Ranges = d.RemainingArgs() + } return nil } @@ -442,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) @@ -513,8 +576,17 @@ func (mre *MatchRegexp) Match(input string, repl caddy.Replacer, scope string) b // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - if !d.Args(&mre.Name, &mre.Pattern) { - return fmt.Errorf("missing arguments") + 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 } diff --git a/modules/caddyhttp/requestbody/requestbody.go b/modules/caddyhttp/requestbody/requestbody.go index 3763cfe6..9b162501 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 ff5786c1..21aca1d4 100755 --- a/modules/caddyhttp/reverseproxy/module.go +++ b/modules/caddyhttp/reverseproxy/module.go @@ -15,39 +15,39 @@ package reverseproxy import ( - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" - "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "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{ - Name: "http.handlers.reverse_proxy", - New: func() interface{} { return new(LoadBalanced) }, - }) + caddy.RegisterModule(new(LoadBalanced)) + httpcaddyfile.RegisterHandlerDirective("reverse_proxy", parseCaddyfile) // TODO: "proxy"? } -// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: +// CaddyModule returns the Caddy module information. +func (*LoadBalanced) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.reverse_proxy", + New: func() caddy.Module { return new(LoadBalanced) }, + } +} + +// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax: // // proxy [] // // TODO: This needs to be finished. It definitely needs to be able to open a block... -func (lb *LoadBalanced) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - allTo := d.RemainingArgs() +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + lb := new(LoadBalanced) + for h.Next() { + allTo := h.RemainingArgs() if len(allTo) == 0 { - return d.ArgErr() + return nil, h.ArgErr() } for _, to := range allTo { lb.Upstreams = append(lb.Upstreams, &UpstreamConfig{Host: to}) } } - return nil + return lb, nil } - -// Bucket returns the HTTP Caddyfile handler bucket number. -func (*LoadBalanced) Bucket() int { return 7 } - -// Interface guard -var _ httpcaddyfile.HandlerDirective = (*LoadBalanced)(nil) diff --git a/modules/caddyhttp/rewrite/caddyfile.go b/modules/caddyhttp/rewrite/caddyfile.go index a067fe14..a977a724 100644 --- a/modules/caddyhttp/rewrite/caddyfile.go +++ b/modules/caddyhttp/rewrite/caddyfile.go @@ -15,24 +15,23 @@ package rewrite import ( - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) -// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: +func init() { + httpcaddyfile.RegisterHandlerDirective("rewrite", parseCaddyfile) +} + +// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax: // // rewrite [] // // The parameter becomes the new URI. -func (rewr *Rewrite) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - rewr.URI = d.Val() +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + var rewr Rewrite + for h.Next() { + rewr.URI = h.Val() } - return nil + return rewr, nil } - -// Bucket returns the HTTP Caddyfile handler bucket number. -func (rewr Rewrite) Bucket() int { return 1 } - -// Interface guard -var _ httpcaddyfile.HandlerDirective = (*Rewrite)(nil) diff --git a/modules/caddyhttp/rewrite/rewrite.go b/modules/caddyhttp/rewrite/rewrite.go index ac113ff5..f434a38a 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 ffa7ce7b..1efbad6f 100644 --- a/modules/caddyhttp/routes.go +++ b/modules/caddyhttp/routes.go @@ -26,33 +26,34 @@ import ( // middlewares, and a responder for handling HTTP // requests. type Route 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"` + 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 (r Route) Empty() bool { - return len(r.MatcherSets) == 0 && - len(r.Handle) == 0 && - len(r.handlers) == 0 && + return len(r.MatcherSetsRaw) == 0 && + len(r.MatcherSets) == 0 && + len(r.HandlersRaw) == 0 && + len(r.Handlers) == 0 && !r.Terminal && r.Group == "" } func (r Route) anyMatcherSetMatches(req *http.Request) bool { - for _, ms := range r.matcherSets { + for _, ms := range r.MatcherSets { if ms.Match(req) { return true } } // if no matchers, always match - return len(r.matcherSets) == 0 + return len(r.MatcherSets) == 0 } // MatcherSet is a set of matchers which @@ -79,7 +80,7 @@ type RouteList []Route 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/staticerror.go b/modules/caddyhttp/staticerror.go index 1834cf7c..3a45366c 100644 --- a/modules/caddyhttp/staticerror.go +++ b/modules/caddyhttp/staticerror.go @@ -23,10 +23,7 @@ import ( ) 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. @@ -35,6 +32,14 @@ type StaticError struct { 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 { repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) diff --git a/modules/caddyhttp/staticresp.go b/modules/caddyhttp/staticresp.go index cafee35b..942459b7 100644 --- a/modules/caddyhttp/staticresp.go +++ b/modules/caddyhttp/staticresp.go @@ -24,10 +24,8 @@ import ( ) 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. @@ -38,6 +36,14 @@ type StaticResponse struct { 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 [] { @@ -71,9 +77,6 @@ func (s *StaticResponse) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil } -// Bucket returns the HTTP Caddyfile handler bucket number. -func (StaticResponse) Bucket() int { return 7 } - func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error { repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) diff --git a/modules/caddyhttp/subroute.go b/modules/caddyhttp/subroute.go index 91721465..3b0d718b 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/templates/caddyfile.go b/modules/caddyhttp/templates/caddyfile.go index d27b8e35..d948da06 100644 --- a/modules/caddyhttp/templates/caddyfile.go +++ b/modules/caddyhttp/templates/caddyfile.go @@ -15,11 +15,15 @@ package templates import ( - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) -// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: +func init() { + httpcaddyfile.RegisterHandlerDirective("templates", parseCaddyfile) +} + +// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax: // // templates [] { // mime @@ -27,23 +31,24 @@ import ( // root // } // -func (t *Templates) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - for d.NextBlock() { - switch d.Val() { +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + t := new(Templates) + for h.Next() { + for h.NextBlock() { + switch h.Val() { case "mime": - t.MIMETypes = d.RemainingArgs() + t.MIMETypes = h.RemainingArgs() if len(t.MIMETypes) == 0 { - return d.ArgErr() + return nil, h.ArgErr() } case "between": - t.Delimiters = d.RemainingArgs() + t.Delimiters = h.RemainingArgs() if len(t.Delimiters) != 2 { - return d.ArgErr() + return nil, h.ArgErr() } case "root": - if !d.Args(&t.IncludeRoot) { - return d.ArgErr() + if !h.Args(&t.IncludeRoot) { + return nil, h.ArgErr() } } } @@ -53,11 +58,5 @@ func (t *Templates) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { t.IncludeRoot = "{http.var.root}" } - return nil + return t, nil } - -// Bucket returns the HTTP Caddyfile handler bucket number. -func (t Templates) Bucket() int { return 5 } - -// Interface guard -var _ httpcaddyfile.HandlerDirective = (*Templates)(nil) diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go index 442e1777..1cd347cf 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 { diff --git a/modules/caddyhttp/vars.go b/modules/caddyhttp/vars.go index f74556a4..bbd45680 100644 --- a/modules/caddyhttp/vars.go +++ b/modules/caddyhttp/vars.go @@ -21,20 +21,22 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.vars", - New: func() interface{} { return new(VarsMiddleware) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.vars", - New: func() interface{} { return new(VarsMiddleware) }, - }) + 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) @@ -50,6 +52,14 @@ func (t VarsMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next H // 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) diff --git a/modules/caddytls/acmemanager.go b/modules/caddytls/acmemanager.go index 578cdb38..36f1c212 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/fileloader.go b/modules/caddytls/fileloader.go index 7a0d14d8..b2cc1325 100644 --- a/modules/caddytls/fileloader.go +++ b/modules/caddytls/fileloader.go @@ -23,15 +23,20 @@ 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 +// 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. type CertKeyFilePair struct { diff --git a/modules/caddytls/folderloader.go b/modules/caddytls/folderloader.go index ae7f0564..da1dff09 100644 --- a/modules/caddytls/folderloader.go +++ b/modules/caddytls/folderloader.go @@ -28,10 +28,7 @@ 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 @@ -39,6 +36,14 @@ func init() { // files which contain both a certificate and a key. 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 diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go index ee146d47..47fb2964 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/standardstek/stek.go b/modules/caddytls/standardstek/stek.go index 6a4b1c82..6d10c76c 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 ec169956..88b77902 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. @@ -53,6 +50,14 @@ type TLS struct { 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? } @@ -237,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 @@ -252,7 +257,7 @@ func (ap AutomationPolicy) makeCertMagicConfig(ctx caddy.Context) certmagic.Conf } return certmagic.Config{ - NewManager: ap.Management.newManager, + NewManager: ap.Management.NewManager, } } @@ -290,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_test.go b/modules_test.go index ecba21ff..ef7edf77 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"}, }, diff --git a/storage.go b/storage.go index 09336e9a..f695a49b 100644 --- a/storage.go +++ b/storage.go @@ -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 }