committed by
GitHub
69 changed files with 5501 additions and 435 deletions
@ -0,0 +1,87 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package caddyfile |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
|
||||
|
"github.com/caddyserver/caddy/v2" |
||||
|
"github.com/caddyserver/caddy/v2/caddyconfig" |
||||
|
) |
||||
|
|
||||
|
// Adapter adapts Caddyfile to Caddy JSON.
|
||||
|
type Adapter struct { |
||||
|
ServerType ServerType |
||||
|
} |
||||
|
|
||||
|
// Adapt converts the Caddyfile config in body to Caddy JSON.
|
||||
|
func (a Adapter) Adapt(body []byte, options map[string]string) ([]byte, []caddyconfig.Warning, error) { |
||||
|
if a.ServerType == nil { |
||||
|
return nil, nil, fmt.Errorf("no server type") |
||||
|
} |
||||
|
if options == nil { |
||||
|
options = make(map[string]string) |
||||
|
} |
||||
|
|
||||
|
filename := options["filename"] |
||||
|
if filename == "" { |
||||
|
filename = "Caddyfile" |
||||
|
} |
||||
|
|
||||
|
serverBlocks, err := Parse(filename, bytes.NewReader(body)) |
||||
|
if err != nil { |
||||
|
return nil, nil, err |
||||
|
} |
||||
|
|
||||
|
cfg, warnings, err := a.ServerType.Setup(serverBlocks, options) |
||||
|
if err != nil { |
||||
|
return nil, warnings, err |
||||
|
} |
||||
|
|
||||
|
marshalFunc := json.Marshal |
||||
|
if options["pretty"] == "true" { |
||||
|
marshalFunc = caddyconfig.JSONIndent |
||||
|
} |
||||
|
result, err := marshalFunc(cfg) |
||||
|
|
||||
|
return result, warnings, err |
||||
|
} |
||||
|
|
||||
|
// Unmarshaler is a type that can unmarshal
|
||||
|
// Caddyfile tokens to set itself up for a
|
||||
|
// JSON encoding. The goal of an unmarshaler
|
||||
|
// is not to set itself up for actual use,
|
||||
|
// but to set itself up for being marshaled
|
||||
|
// into JSON. Caddyfile-unmarshaled values
|
||||
|
// will not be used directly; they will be
|
||||
|
// encoded as JSON and then used from that.
|
||||
|
type Unmarshaler interface { |
||||
|
UnmarshalCaddyfile(d *Dispenser) error |
||||
|
} |
||||
|
|
||||
|
// ServerType is a type that can evaluate a Caddyfile and set up a caddy config.
|
||||
|
type ServerType interface { |
||||
|
// Setup takes the server blocks which
|
||||
|
// contain tokens, as well as options
|
||||
|
// (e.g. CLI flags) and creates a Caddy
|
||||
|
// config, along with any warnings or
|
||||
|
// an error.
|
||||
|
Setup([]ServerBlock, map[string]string) (*caddy.Config, []caddyconfig.Warning, error) |
||||
|
} |
||||
|
|
||||
|
// Interface guard
|
||||
|
var _ caddyconfig.Adapter = (*Adapter)(nil) |
||||
@ -0,0 +1,341 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package caddyfile |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
"strings" |
||||
|
) |
||||
|
|
||||
|
// Dispenser is a type that dispenses tokens, similarly to a lexer,
|
||||
|
// except that it can do so with some notion of structure. An empty
|
||||
|
// Dispenser is invalid; call NewDispenser to make a proper instance.
|
||||
|
type Dispenser struct { |
||||
|
filename string |
||||
|
tokens []Token |
||||
|
cursor int |
||||
|
nesting int |
||||
|
} |
||||
|
|
||||
|
// NewDispenser returns a Dispenser filled with the given tokens.
|
||||
|
// TODO: Get rid of the filename argument; it seems pointless here
|
||||
|
func NewDispenser(filename string, tokens []Token) *Dispenser { |
||||
|
return &Dispenser{ |
||||
|
filename: filename, |
||||
|
tokens: tokens, |
||||
|
cursor: -1, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Next loads the next token. Returns true if a token
|
||||
|
// was loaded; false otherwise. If false, all tokens
|
||||
|
// have been consumed.
|
||||
|
func (d *Dispenser) Next() bool { |
||||
|
if d.cursor < len(d.tokens)-1 { |
||||
|
d.cursor++ |
||||
|
return true |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// Prev moves to the previous token. It does the inverse
|
||||
|
// of Next(), except this function may decrement the cursor
|
||||
|
// to -1 so that the next call to Next() points to the
|
||||
|
// first token; this allows dispensing to "start over". This
|
||||
|
// method returns true if the cursor ends up pointing to a
|
||||
|
// valid token.
|
||||
|
func (d *Dispenser) Prev() bool { |
||||
|
if d.cursor > -1 { |
||||
|
d.cursor-- |
||||
|
return d.cursor > -1 |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// NextArg loads the next token if it is on the same
|
||||
|
// line and if it is not a block opening (open curly
|
||||
|
// brace). Returns true if an argument token was
|
||||
|
// loaded; false otherwise. If false, all tokens on
|
||||
|
// the line have been consumed except for potentially
|
||||
|
// a block opening. It handles imported tokens
|
||||
|
// correctly.
|
||||
|
func (d *Dispenser) NextArg() bool { |
||||
|
if !d.nextOnSameLine() { |
||||
|
return false |
||||
|
} |
||||
|
if d.Val() == "{" { |
||||
|
// roll back; a block opening is not an argument
|
||||
|
d.cursor-- |
||||
|
return false |
||||
|
} |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// nextOnSameLine advances the cursor if the next
|
||||
|
// token is on the same line of the same file.
|
||||
|
func (d *Dispenser) nextOnSameLine() bool { |
||||
|
if d.cursor < 0 { |
||||
|
d.cursor++ |
||||
|
return true |
||||
|
} |
||||
|
if d.cursor >= len(d.tokens) { |
||||
|
return false |
||||
|
} |
||||
|
if d.cursor < len(d.tokens)-1 && |
||||
|
d.tokens[d.cursor].File == d.tokens[d.cursor+1].File && |
||||
|
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line { |
||||
|
d.cursor++ |
||||
|
return true |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// NextLine loads the next token only if it is not on the same
|
||||
|
// line as the current token, and returns true if a token was
|
||||
|
// loaded; false otherwise. If false, there is not another token
|
||||
|
// or it is on the same line. It handles imported tokens correctly.
|
||||
|
func (d *Dispenser) NextLine() bool { |
||||
|
if d.cursor < 0 { |
||||
|
d.cursor++ |
||||
|
return true |
||||
|
} |
||||
|
if d.cursor >= len(d.tokens) { |
||||
|
return false |
||||
|
} |
||||
|
if d.cursor < len(d.tokens)-1 && |
||||
|
(d.tokens[d.cursor].File != d.tokens[d.cursor+1].File || |
||||
|
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) { |
||||
|
d.cursor++ |
||||
|
return true |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// NextBlock can be used as the condition of a for loop
|
||||
|
// to load the next token as long as it opens a block or
|
||||
|
// is already in a block. It returns true if a token was
|
||||
|
// loaded, or false when the block's closing curly brace
|
||||
|
// was loaded and thus the block ended. Nested blocks are
|
||||
|
// not supported.
|
||||
|
func (d *Dispenser) NextBlock() bool { |
||||
|
if d.nesting > 0 { |
||||
|
d.Next() |
||||
|
if d.Val() == "}" { |
||||
|
d.nesting-- |
||||
|
return false |
||||
|
} |
||||
|
return true |
||||
|
} |
||||
|
if !d.nextOnSameLine() { // block must open on same line
|
||||
|
return false |
||||
|
} |
||||
|
if d.Val() != "{" { |
||||
|
d.cursor-- // roll back if not opening brace
|
||||
|
return false |
||||
|
} |
||||
|
d.Next() |
||||
|
if d.Val() == "}" { |
||||
|
// open and then closed right away
|
||||
|
return false |
||||
|
} |
||||
|
d.nesting++ |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// Nested returns true if the token is currently nested
|
||||
|
// inside a block (i.e. an open curly brace was consumed).
|
||||
|
func (d *Dispenser) Nested() bool { |
||||
|
return d.nesting > 0 |
||||
|
} |
||||
|
|
||||
|
// Val gets the text of the current token. If there is no token
|
||||
|
// loaded, it returns empty string.
|
||||
|
func (d *Dispenser) Val() string { |
||||
|
if d.cursor < 0 || d.cursor >= len(d.tokens) { |
||||
|
return "" |
||||
|
} |
||||
|
return d.tokens[d.cursor].Text |
||||
|
} |
||||
|
|
||||
|
// Line gets the line number of the current token. If there is no token
|
||||
|
// loaded, it returns 0.
|
||||
|
func (d *Dispenser) Line() int { |
||||
|
if d.cursor < 0 || d.cursor >= len(d.tokens) { |
||||
|
return 0 |
||||
|
} |
||||
|
return d.tokens[d.cursor].Line |
||||
|
} |
||||
|
|
||||
|
// File gets the filename of the current token. If there is no token loaded,
|
||||
|
// it returns the filename originally given when parsing started.
|
||||
|
func (d *Dispenser) File() string { |
||||
|
if d.cursor < 0 || d.cursor >= len(d.tokens) { |
||||
|
return d.filename |
||||
|
} |
||||
|
if tokenFilename := d.tokens[d.cursor].File; tokenFilename != "" { |
||||
|
return tokenFilename |
||||
|
} |
||||
|
return d.filename |
||||
|
} |
||||
|
|
||||
|
// Args is a convenience function that loads the next arguments
|
||||
|
// (tokens on the same line) into an arbitrary number of strings
|
||||
|
// pointed to in targets. If there are fewer tokens available
|
||||
|
// than string pointers, the remaining strings will not be changed
|
||||
|
// and false will be returned. If there were enough tokens available
|
||||
|
// to fill the arguments, then true will be returned.
|
||||
|
func (d *Dispenser) Args(targets ...*string) bool { |
||||
|
for i := 0; i < len(targets); i++ { |
||||
|
if !d.NextArg() { |
||||
|
return false |
||||
|
} |
||||
|
*targets[i] = d.Val() |
||||
|
} |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// RemainingArgs loads any more arguments (tokens on the same line)
|
||||
|
// into a slice and returns them. Open curly brace tokens also indicate
|
||||
|
// the end of arguments, and the curly brace is not included in
|
||||
|
// the return value nor is it loaded.
|
||||
|
func (d *Dispenser) RemainingArgs() []string { |
||||
|
var args []string |
||||
|
for d.NextArg() { |
||||
|
args = append(args, d.Val()) |
||||
|
} |
||||
|
return args |
||||
|
} |
||||
|
|
||||
|
// NewFromNextTokens returns a new dispenser with a copy of
|
||||
|
// the tokens from the current token until the end of the
|
||||
|
// "directive" whether that be to the end of the line or
|
||||
|
// the end of a block that starts at the end of the line.
|
||||
|
func (d *Dispenser) NewFromNextTokens() *Dispenser { |
||||
|
tkns := []Token{d.Token()} |
||||
|
for d.NextArg() { |
||||
|
tkns = append(tkns, d.Token()) |
||||
|
} |
||||
|
if d.Next() && d.Val() == "{" { |
||||
|
tkns = append(tkns, d.Token()) |
||||
|
for d.NextBlock() { |
||||
|
for d.Nested() { |
||||
|
tkns = append(tkns, d.Token()) |
||||
|
d.NextBlock() |
||||
|
} |
||||
|
} |
||||
|
tkns = append(tkns, d.Token()) |
||||
|
} else { |
||||
|
d.cursor-- |
||||
|
} |
||||
|
return NewDispenser(d.filename, tkns) |
||||
|
} |
||||
|
|
||||
|
// Token returns the current token.
|
||||
|
func (d *Dispenser) Token() Token { |
||||
|
return d.TokenAt(d.cursor) |
||||
|
} |
||||
|
|
||||
|
func (d *Dispenser) TokenAt(cursor int) Token { |
||||
|
if cursor < 0 || cursor >= len(d.tokens) { |
||||
|
return Token{} |
||||
|
} |
||||
|
return d.tokens[cursor] |
||||
|
} |
||||
|
|
||||
|
// Cursor returns the current cursor (token index).
|
||||
|
func (d *Dispenser) Cursor() int { |
||||
|
return d.cursor |
||||
|
} |
||||
|
|
||||
|
func (d *Dispenser) Reset() { |
||||
|
d.cursor = -1 |
||||
|
} |
||||
|
|
||||
|
// ArgErr returns an argument error, meaning that another
|
||||
|
// argument was expected but not found. In other words,
|
||||
|
// a line break or open curly brace was encountered instead of
|
||||
|
// an argument.
|
||||
|
func (d *Dispenser) ArgErr() error { |
||||
|
if d.Val() == "{" { |
||||
|
return d.Err("Unexpected token '{', expecting argument") |
||||
|
} |
||||
|
return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val()) |
||||
|
} |
||||
|
|
||||
|
// SyntaxErr creates a generic syntax error which explains what was
|
||||
|
// found and what was expected.
|
||||
|
func (d *Dispenser) SyntaxErr(expected string) error { |
||||
|
msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected) |
||||
|
return errors.New(msg) |
||||
|
} |
||||
|
|
||||
|
// EOFErr returns an error indicating that the dispenser reached
|
||||
|
// the end of the input when searching for the next token.
|
||||
|
func (d *Dispenser) EOFErr() error { |
||||
|
return d.Errf("Unexpected EOF") |
||||
|
} |
||||
|
|
||||
|
// Err generates a custom parse-time error with a message of msg.
|
||||
|
func (d *Dispenser) Err(msg string) error { |
||||
|
msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg) |
||||
|
return errors.New(msg) |
||||
|
} |
||||
|
|
||||
|
// Errf is like Err, but for formatted error messages
|
||||
|
func (d *Dispenser) Errf(format string, args ...interface{}) error { |
||||
|
return d.Err(fmt.Sprintf(format, args...)) |
||||
|
} |
||||
|
|
||||
|
// Delete deletes the current token and returns the updated slice
|
||||
|
// of tokens. The cursor is not advanced to the next token.
|
||||
|
// Because deletion modifies the underlying slice, this method
|
||||
|
// should only be called if you have access to the original slice
|
||||
|
// of tokens and/or are using the slice of tokens outside this
|
||||
|
// Dispenser instance. If you do not re-assign the slice with the
|
||||
|
// return value of this method, inconsistencies in the token
|
||||
|
// array will become apparent (or worse, hide from you like they
|
||||
|
// did me for 3 and a half freaking hours late one night).
|
||||
|
func (d *Dispenser) Delete() []Token { |
||||
|
if d.cursor >= 0 && d.cursor < len(d.tokens)-1 { |
||||
|
d.tokens = append(d.tokens[:d.cursor], d.tokens[d.cursor+1:]...) |
||||
|
d.cursor-- |
||||
|
} |
||||
|
return d.tokens |
||||
|
} |
||||
|
|
||||
|
// numLineBreaks counts how many line breaks are in the token
|
||||
|
// value given by the token index tknIdx. It returns 0 if the
|
||||
|
// token does not exist or there are no line breaks.
|
||||
|
func (d *Dispenser) numLineBreaks(tknIdx int) int { |
||||
|
if tknIdx < 0 || tknIdx >= len(d.tokens) { |
||||
|
return 0 |
||||
|
} |
||||
|
return strings.Count(d.tokens[tknIdx].Text, "\n") |
||||
|
} |
||||
|
|
||||
|
// isNewLine determines whether the current token is on a different
|
||||
|
// line (higher line number) than the previous token. It handles imported
|
||||
|
// tokens correctly. If there isn't a previous token, it returns true.
|
||||
|
func (d *Dispenser) isNewLine() bool { |
||||
|
if d.cursor < 1 { |
||||
|
return true |
||||
|
} |
||||
|
if d.cursor > len(d.tokens)-1 { |
||||
|
return false |
||||
|
} |
||||
|
return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File || |
||||
|
d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line |
||||
|
} |
||||
@ -0,0 +1,316 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package caddyfile |
||||
|
|
||||
|
import ( |
||||
|
"io" |
||||
|
"log" |
||||
|
"reflect" |
||||
|
"strings" |
||||
|
"testing" |
||||
|
) |
||||
|
|
||||
|
func TestDispenser_Val_Next(t *testing.T) { |
||||
|
input := `host:port |
||||
|
dir1 arg1 |
||||
|
dir2 arg2 arg3 |
||||
|
dir3` |
||||
|
d := newTestDispenser(input) |
||||
|
|
||||
|
if val := d.Val(); val != "" { |
||||
|
t.Fatalf("Val(): Should return empty string when no token loaded; got '%s'", val) |
||||
|
} |
||||
|
|
||||
|
assertNext := func(shouldLoad bool, expectedCursor int, expectedVal string) { |
||||
|
if loaded := d.Next(); loaded != shouldLoad { |
||||
|
t.Errorf("Next(): Expected %v but got %v instead (val '%s')", shouldLoad, loaded, d.Val()) |
||||
|
} |
||||
|
if d.cursor != expectedCursor { |
||||
|
t.Errorf("Expected cursor to be %d, but was %d", expectedCursor, d.cursor) |
||||
|
} |
||||
|
if d.nesting != 0 { |
||||
|
t.Errorf("Nesting should be 0, was %d instead", d.nesting) |
||||
|
} |
||||
|
if val := d.Val(); val != expectedVal { |
||||
|
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
assertNext(true, 0, "host:port") |
||||
|
assertNext(true, 1, "dir1") |
||||
|
assertNext(true, 2, "arg1") |
||||
|
assertNext(true, 3, "dir2") |
||||
|
assertNext(true, 4, "arg2") |
||||
|
assertNext(true, 5, "arg3") |
||||
|
assertNext(true, 6, "dir3") |
||||
|
// Note: This next test simply asserts existing behavior.
|
||||
|
// If desired, we may wish to empty the token value after
|
||||
|
// reading past the EOF. Open an issue if you want this change.
|
||||
|
assertNext(false, 6, "dir3") |
||||
|
} |
||||
|
|
||||
|
func TestDispenser_NextArg(t *testing.T) { |
||||
|
input := `dir1 arg1 |
||||
|
dir2 arg2 arg3 |
||||
|
dir3` |
||||
|
d := newTestDispenser(input) |
||||
|
|
||||
|
assertNext := func(shouldLoad bool, expectedVal string, expectedCursor int) { |
||||
|
if d.Next() != shouldLoad { |
||||
|
t.Errorf("Next(): Should load token but got false instead (val: '%s')", d.Val()) |
||||
|
} |
||||
|
if d.cursor != expectedCursor { |
||||
|
t.Errorf("Next(): Expected cursor to be at %d, but it was %d", expectedCursor, d.cursor) |
||||
|
} |
||||
|
if val := d.Val(); val != expectedVal { |
||||
|
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
assertNextArg := func(expectedVal string, loadAnother bool, expectedCursor int) { |
||||
|
if !d.NextArg() { |
||||
|
t.Error("NextArg(): Should load next argument but got false instead") |
||||
|
} |
||||
|
if d.cursor != expectedCursor { |
||||
|
t.Errorf("NextArg(): Expected cursor to be at %d, but it was %d", expectedCursor, d.cursor) |
||||
|
} |
||||
|
if val := d.Val(); val != expectedVal { |
||||
|
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val) |
||||
|
} |
||||
|
if !loadAnother { |
||||
|
if d.NextArg() { |
||||
|
t.Fatalf("NextArg(): Should NOT load another argument, but got true instead (val: '%s')", d.Val()) |
||||
|
} |
||||
|
if d.cursor != expectedCursor { |
||||
|
t.Errorf("NextArg(): Expected cursor to remain at %d, but it was %d", expectedCursor, d.cursor) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
assertNext(true, "dir1", 0) |
||||
|
assertNextArg("arg1", false, 1) |
||||
|
assertNext(true, "dir2", 2) |
||||
|
assertNextArg("arg2", true, 3) |
||||
|
assertNextArg("arg3", false, 4) |
||||
|
assertNext(true, "dir3", 5) |
||||
|
assertNext(false, "dir3", 5) |
||||
|
} |
||||
|
|
||||
|
func TestDispenser_NextLine(t *testing.T) { |
||||
|
input := `host:port |
||||
|
dir1 arg1 |
||||
|
dir2 arg2 arg3` |
||||
|
d := newTestDispenser(input) |
||||
|
|
||||
|
assertNextLine := func(shouldLoad bool, expectedVal string, expectedCursor int) { |
||||
|
if d.NextLine() != shouldLoad { |
||||
|
t.Errorf("NextLine(): Should load token but got false instead (val: '%s')", d.Val()) |
||||
|
} |
||||
|
if d.cursor != expectedCursor { |
||||
|
t.Errorf("NextLine(): Expected cursor to be %d, instead was %d", expectedCursor, d.cursor) |
||||
|
} |
||||
|
if val := d.Val(); val != expectedVal { |
||||
|
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
assertNextLine(true, "host:port", 0) |
||||
|
assertNextLine(true, "dir1", 1) |
||||
|
assertNextLine(false, "dir1", 1) |
||||
|
d.Next() // arg1
|
||||
|
assertNextLine(true, "dir2", 3) |
||||
|
assertNextLine(false, "dir2", 3) |
||||
|
d.Next() // arg2
|
||||
|
assertNextLine(false, "arg2", 4) |
||||
|
d.Next() // arg3
|
||||
|
assertNextLine(false, "arg3", 5) |
||||
|
} |
||||
|
|
||||
|
func TestDispenser_NextBlock(t *testing.T) { |
||||
|
input := `foobar1 { |
||||
|
sub1 arg1 |
||||
|
sub2 |
||||
|
} |
||||
|
foobar2 { |
||||
|
}` |
||||
|
d := newTestDispenser(input) |
||||
|
|
||||
|
assertNextBlock := func(shouldLoad bool, expectedCursor, expectedNesting int) { |
||||
|
if loaded := d.NextBlock(); loaded != shouldLoad { |
||||
|
t.Errorf("NextBlock(): Should return %v but got %v", shouldLoad, loaded) |
||||
|
} |
||||
|
if d.cursor != expectedCursor { |
||||
|
t.Errorf("NextBlock(): Expected cursor to be %d, was %d", expectedCursor, d.cursor) |
||||
|
} |
||||
|
if d.nesting != expectedNesting { |
||||
|
t.Errorf("NextBlock(): Nesting should be %d, not %d", expectedNesting, d.nesting) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
assertNextBlock(false, -1, 0) |
||||
|
d.Next() // foobar1
|
||||
|
assertNextBlock(true, 2, 1) |
||||
|
assertNextBlock(true, 3, 1) |
||||
|
assertNextBlock(true, 4, 1) |
||||
|
assertNextBlock(false, 5, 0) |
||||
|
d.Next() // foobar2
|
||||
|
assertNextBlock(false, 8, 0) // empty block is as if it didn't exist
|
||||
|
} |
||||
|
|
||||
|
func TestDispenser_Args(t *testing.T) { |
||||
|
var s1, s2, s3 string |
||||
|
input := `dir1 arg1 arg2 arg3 |
||||
|
dir2 arg4 arg5 |
||||
|
dir3 arg6 arg7 |
||||
|
dir4` |
||||
|
d := newTestDispenser(input) |
||||
|
|
||||
|
d.Next() // dir1
|
||||
|
|
||||
|
// As many strings as arguments
|
||||
|
if all := d.Args(&s1, &s2, &s3); !all { |
||||
|
t.Error("Args(): Expected true, got false") |
||||
|
} |
||||
|
if s1 != "arg1" { |
||||
|
t.Errorf("Args(): Expected s1 to be 'arg1', got '%s'", s1) |
||||
|
} |
||||
|
if s2 != "arg2" { |
||||
|
t.Errorf("Args(): Expected s2 to be 'arg2', got '%s'", s2) |
||||
|
} |
||||
|
if s3 != "arg3" { |
||||
|
t.Errorf("Args(): Expected s3 to be 'arg3', got '%s'", s3) |
||||
|
} |
||||
|
|
||||
|
d.Next() // dir2
|
||||
|
|
||||
|
// More strings than arguments
|
||||
|
if all := d.Args(&s1, &s2, &s3); all { |
||||
|
t.Error("Args(): Expected false, got true") |
||||
|
} |
||||
|
if s1 != "arg4" { |
||||
|
t.Errorf("Args(): Expected s1 to be 'arg4', got '%s'", s1) |
||||
|
} |
||||
|
if s2 != "arg5" { |
||||
|
t.Errorf("Args(): Expected s2 to be 'arg5', got '%s'", s2) |
||||
|
} |
||||
|
if s3 != "arg3" { |
||||
|
t.Errorf("Args(): Expected s3 to be unchanged ('arg3'), instead got '%s'", s3) |
||||
|
} |
||||
|
|
||||
|
// (quick cursor check just for kicks and giggles)
|
||||
|
if d.cursor != 6 { |
||||
|
t.Errorf("Cursor should be 6, but is %d", d.cursor) |
||||
|
} |
||||
|
|
||||
|
d.Next() // dir3
|
||||
|
|
||||
|
// More arguments than strings
|
||||
|
if all := d.Args(&s1); !all { |
||||
|
t.Error("Args(): Expected true, got false") |
||||
|
} |
||||
|
if s1 != "arg6" { |
||||
|
t.Errorf("Args(): Expected s1 to be 'arg6', got '%s'", s1) |
||||
|
} |
||||
|
|
||||
|
d.Next() // dir4
|
||||
|
|
||||
|
// No arguments or strings
|
||||
|
if all := d.Args(); !all { |
||||
|
t.Error("Args(): Expected true, got false") |
||||
|
} |
||||
|
|
||||
|
// No arguments but at least one string
|
||||
|
if all := d.Args(&s1); all { |
||||
|
t.Error("Args(): Expected false, got true") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestDispenser_RemainingArgs(t *testing.T) { |
||||
|
input := `dir1 arg1 arg2 arg3 |
||||
|
dir2 arg4 arg5 |
||||
|
dir3 arg6 { arg7 |
||||
|
dir4` |
||||
|
d := newTestDispenser(input) |
||||
|
|
||||
|
d.Next() // dir1
|
||||
|
|
||||
|
args := d.RemainingArgs() |
||||
|
if expected := []string{"arg1", "arg2", "arg3"}; !reflect.DeepEqual(args, expected) { |
||||
|
t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args) |
||||
|
} |
||||
|
|
||||
|
d.Next() // dir2
|
||||
|
|
||||
|
args = d.RemainingArgs() |
||||
|
if expected := []string{"arg4", "arg5"}; !reflect.DeepEqual(args, expected) { |
||||
|
t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args) |
||||
|
} |
||||
|
|
||||
|
d.Next() // dir3
|
||||
|
|
||||
|
args = d.RemainingArgs() |
||||
|
if expected := []string{"arg6"}; !reflect.DeepEqual(args, expected) { |
||||
|
t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args) |
||||
|
} |
||||
|
|
||||
|
d.Next() // {
|
||||
|
d.Next() // arg7
|
||||
|
d.Next() // dir4
|
||||
|
|
||||
|
args = d.RemainingArgs() |
||||
|
if len(args) != 0 { |
||||
|
t.Errorf("RemainingArgs(): Expected %v, got %v", []string{}, args) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestDispenser_ArgErr_Err(t *testing.T) { |
||||
|
input := `dir1 { |
||||
|
} |
||||
|
dir2 arg1 arg2` |
||||
|
d := newTestDispenser(input) |
||||
|
|
||||
|
d.cursor = 1 // {
|
||||
|
|
||||
|
if err := d.ArgErr(); err == nil || !strings.Contains(err.Error(), "{") { |
||||
|
t.Errorf("ArgErr(): Expected an error message with { in it, but got '%v'", err) |
||||
|
} |
||||
|
|
||||
|
d.cursor = 5 // arg2
|
||||
|
|
||||
|
if err := d.ArgErr(); err == nil || !strings.Contains(err.Error(), "arg2") { |
||||
|
t.Errorf("ArgErr(): Expected an error message with 'arg2' in it; got '%v'", err) |
||||
|
} |
||||
|
|
||||
|
err := d.Err("foobar") |
||||
|
if err == nil { |
||||
|
t.Fatalf("Err(): Expected an error, got nil") |
||||
|
} |
||||
|
|
||||
|
if !strings.Contains(err.Error(), "Testfile:3") { |
||||
|
t.Errorf("Expected error message with filename:line in it; got '%v'", err) |
||||
|
} |
||||
|
|
||||
|
if !strings.Contains(err.Error(), "foobar") { |
||||
|
t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func newTestDispenser(input string) *Dispenser { |
||||
|
tokens, err := allTokens(strings.NewReader(input)) |
||||
|
if err != nil && err != io.EOF { |
||||
|
log.Fatalf("getting all tokens from input: %v", err) |
||||
|
} |
||||
|
return NewDispenser("Testfile", tokens) |
||||
|
} |
||||
@ -0,0 +1,150 @@ |
|||||
|
// Copyright 2015 Light Code Labs, LLC
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package caddyfile |
||||
|
|
||||
|
import ( |
||||
|
"bufio" |
||||
|
"io" |
||||
|
"unicode" |
||||
|
) |
||||
|
|
||||
|
type ( |
||||
|
// lexer is a utility which can get values, token by
|
||||
|
// token, from a Reader. A token is a word, and tokens
|
||||
|
// are separated by whitespace. A word can be enclosed
|
||||
|
// in quotes if it contains whitespace.
|
||||
|
lexer struct { |
||||
|
reader *bufio.Reader |
||||
|
token Token |
||||
|
line int |
||||
|
} |
||||
|
|
||||
|
// Token represents a single parsable unit.
|
||||
|
Token struct { |
||||
|
File string |
||||
|
Line int |
||||
|
Text string |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
// load prepares the lexer to scan an input for tokens.
|
||||
|
// It discards any leading byte order mark.
|
||||
|
func (l *lexer) load(input io.Reader) error { |
||||
|
l.reader = bufio.NewReader(input) |
||||
|
l.line = 1 |
||||
|
|
||||
|
// discard byte order mark, if present
|
||||
|
firstCh, _, err := l.reader.ReadRune() |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
if firstCh != 0xFEFF { |
||||
|
err := l.reader.UnreadRune() |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// next loads the next token into the lexer.
|
||||
|
// A token is delimited by whitespace, unless
|
||||
|
// the token starts with a quotes character (")
|
||||
|
// in which case the token goes until the closing
|
||||
|
// quotes (the enclosing quotes are not included).
|
||||
|
// Inside quoted strings, quotes may be escaped
|
||||
|
// with a preceding \ character. No other chars
|
||||
|
// may be escaped. The rest of the line is skipped
|
||||
|
// if a "#" character is read in. Returns true if
|
||||
|
// a token was loaded; false otherwise.
|
||||
|
func (l *lexer) next() bool { |
||||
|
var val []rune |
||||
|
var comment, quoted, escaped bool |
||||
|
|
||||
|
makeToken := func() bool { |
||||
|
l.token.Text = string(val) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
for { |
||||
|
ch, _, err := l.reader.ReadRune() |
||||
|
if err != nil { |
||||
|
if len(val) > 0 { |
||||
|
return makeToken() |
||||
|
} |
||||
|
if err == io.EOF { |
||||
|
return false |
||||
|
} |
||||
|
panic(err) |
||||
|
} |
||||
|
|
||||
|
if quoted { |
||||
|
if !escaped { |
||||
|
if ch == '\\' { |
||||
|
escaped = true |
||||
|
continue |
||||
|
} else if ch == '"' { |
||||
|
quoted = false |
||||
|
return makeToken() |
||||
|
} |
||||
|
} |
||||
|
if ch == '\n' { |
||||
|
l.line++ |
||||
|
} |
||||
|
if escaped { |
||||
|
// only escape quotes and newlines
|
||||
|
if ch != '"' && ch != '\n' { |
||||
|
val = append(val, '\\') |
||||
|
} |
||||
|
} |
||||
|
val = append(val, ch) |
||||
|
escaped = false |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
if unicode.IsSpace(ch) { |
||||
|
if ch == '\r' { |
||||
|
continue |
||||
|
} |
||||
|
if ch == '\n' { |
||||
|
l.line++ |
||||
|
comment = false |
||||
|
} |
||||
|
if len(val) > 0 { |
||||
|
return makeToken() |
||||
|
} |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
if ch == '#' { |
||||
|
comment = true |
||||
|
} |
||||
|
|
||||
|
if comment { |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
if len(val) == 0 { |
||||
|
l.token = Token{Line: l.line} |
||||
|
if ch == '"' { |
||||
|
quoted = true |
||||
|
continue |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
val = append(val, ch) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,196 @@ |
|||||
|
// Copyright 2015 Light Code Labs, LLC
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package caddyfile |
||||
|
|
||||
|
import ( |
||||
|
"log" |
||||
|
"strings" |
||||
|
"testing" |
||||
|
) |
||||
|
|
||||
|
type lexerTestCase struct { |
||||
|
input string |
||||
|
expected []Token |
||||
|
} |
||||
|
|
||||
|
func TestLexer(t *testing.T) { |
||||
|
testCases := []lexerTestCase{ |
||||
|
{ |
||||
|
input: `host:123`, |
||||
|
expected: []Token{ |
||||
|
{Line: 1, Text: "host:123"}, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
input: `host:123 |
||||
|
|
||||
|
directive`, |
||||
|
expected: []Token{ |
||||
|
{Line: 1, Text: "host:123"}, |
||||
|
{Line: 3, Text: "directive"}, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
input: `host:123 { |
||||
|
directive |
||||
|
}`, |
||||
|
expected: []Token{ |
||||
|
{Line: 1, Text: "host:123"}, |
||||
|
{Line: 1, Text: "{"}, |
||||
|
{Line: 2, Text: "directive"}, |
||||
|
{Line: 3, Text: "}"}, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
input: `host:123 { directive }`, |
||||
|
expected: []Token{ |
||||
|
{Line: 1, Text: "host:123"}, |
||||
|
{Line: 1, Text: "{"}, |
||||
|
{Line: 1, Text: "directive"}, |
||||
|
{Line: 1, Text: "}"}, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
input: `host:123 { |
||||
|
#comment |
||||
|
directive |
||||
|
# comment |
||||
|
foobar # another comment |
||||
|
}`, |
||||
|
expected: []Token{ |
||||
|
{Line: 1, Text: "host:123"}, |
||||
|
{Line: 1, Text: "{"}, |
||||
|
{Line: 3, Text: "directive"}, |
||||
|
{Line: 5, Text: "foobar"}, |
||||
|
{Line: 6, Text: "}"}, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
input: `a "quoted value" b |
||||
|
foobar`, |
||||
|
expected: []Token{ |
||||
|
{Line: 1, Text: "a"}, |
||||
|
{Line: 1, Text: "quoted value"}, |
||||
|
{Line: 1, Text: "b"}, |
||||
|
{Line: 2, Text: "foobar"}, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
input: `A "quoted \"value\" inside" B`, |
||||
|
expected: []Token{ |
||||
|
{Line: 1, Text: "A"}, |
||||
|
{Line: 1, Text: `quoted "value" inside`}, |
||||
|
{Line: 1, Text: "B"}, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
input: "A \"newline \\\ninside\" quotes", |
||||
|
expected: []Token{ |
||||
|
{Line: 1, Text: "A"}, |
||||
|
{Line: 1, Text: "newline \ninside"}, |
||||
|
{Line: 2, Text: "quotes"}, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
input: `"don't\escape"`, |
||||
|
expected: []Token{ |
||||
|
{Line: 1, Text: `don't\escape`}, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
input: `"don't\\escape"`, |
||||
|
expected: []Token{ |
||||
|
{Line: 1, Text: `don't\\escape`}, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
input: `A "quoted value with line |
||||
|
break inside" { |
||||
|
foobar |
||||
|
}`, |
||||
|
expected: []Token{ |
||||
|
{Line: 1, Text: "A"}, |
||||
|
{Line: 1, Text: "quoted value with line\n\t\t\t\t\tbreak inside"}, |
||||
|
{Line: 2, Text: "{"}, |
||||
|
{Line: 3, Text: "foobar"}, |
||||
|
{Line: 4, Text: "}"}, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
input: `"C:\php\php-cgi.exe"`, |
||||
|
expected: []Token{ |
||||
|
{Line: 1, Text: `C:\php\php-cgi.exe`}, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
input: `empty "" string`, |
||||
|
expected: []Token{ |
||||
|
{Line: 1, Text: `empty`}, |
||||
|
{Line: 1, Text: ``}, |
||||
|
{Line: 1, Text: `string`}, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
input: "skip those\r\nCR characters", |
||||
|
expected: []Token{ |
||||
|
{Line: 1, Text: "skip"}, |
||||
|
{Line: 1, Text: "those"}, |
||||
|
{Line: 2, Text: "CR"}, |
||||
|
{Line: 2, Text: "characters"}, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
input: "\xEF\xBB\xBF:8080", // test with leading byte order mark
|
||||
|
expected: []Token{ |
||||
|
{Line: 1, Text: ":8080"}, |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
for i, testCase := range testCases { |
||||
|
actual := tokenize(testCase.input) |
||||
|
lexerCompare(t, i, testCase.expected, actual) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func tokenize(input string) (tokens []Token) { |
||||
|
l := lexer{} |
||||
|
if err := l.load(strings.NewReader(input)); err != nil { |
||||
|
log.Printf("[ERROR] load failed: %v", err) |
||||
|
} |
||||
|
for l.next() { |
||||
|
tokens = append(tokens, l.token) |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
func lexerCompare(t *testing.T, n int, expected, actual []Token) { |
||||
|
if len(expected) != len(actual) { |
||||
|
t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual)) |
||||
|
} |
||||
|
|
||||
|
for i := 0; i < len(actual) && i < len(expected); i++ { |
||||
|
if actual[i].Line != expected[i].Line { |
||||
|
t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d", |
||||
|
n, i, expected[i].Text, expected[i].Line, actual[i].Line) |
||||
|
break |
||||
|
} |
||||
|
if actual[i].Text != expected[i].Text { |
||||
|
t.Errorf("Test case %d token %d: expected text '%s' but was '%s'", |
||||
|
n, i, expected[i].Text, actual[i].Text) |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,521 @@ |
|||||
|
// Copyright 2015 Light Code Labs, LLC
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package caddyfile |
||||
|
|
||||
|
import ( |
||||
|
"io" |
||||
|
"log" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
"strings" |
||||
|
) |
||||
|
|
||||
|
// Parse parses the input just enough to group tokens, in
|
||||
|
// order, by server block. No further parsing is performed.
|
||||
|
// Server blocks are returned in the order in which they appear.
|
||||
|
// Directives that do not appear in validDirectives will cause
|
||||
|
// an error. If you do not want to check for valid directives,
|
||||
|
// pass in nil instead.
|
||||
|
func Parse(filename string, input io.Reader) ([]ServerBlock, error) { |
||||
|
tokens, err := allTokens(input) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
p := parser{Dispenser: NewDispenser(filename, tokens)} |
||||
|
return p.parseAll() |
||||
|
} |
||||
|
|
||||
|
// allTokens lexes the entire input, but does not parse it.
|
||||
|
// It returns all the tokens from the input, unstructured
|
||||
|
// and in order.
|
||||
|
func allTokens(input io.Reader) ([]Token, error) { |
||||
|
l := new(lexer) |
||||
|
err := l.load(input) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
var tokens []Token |
||||
|
for l.next() { |
||||
|
tokens = append(tokens, l.token) |
||||
|
} |
||||
|
return tokens, nil |
||||
|
} |
||||
|
|
||||
|
type parser struct { |
||||
|
*Dispenser |
||||
|
block ServerBlock // current server block being parsed
|
||||
|
eof bool // if we encounter a valid EOF in a hard place
|
||||
|
definedSnippets map[string][]Token |
||||
|
nesting int |
||||
|
} |
||||
|
|
||||
|
func (p *parser) parseAll() ([]ServerBlock, error) { |
||||
|
var blocks []ServerBlock |
||||
|
|
||||
|
for p.Next() { |
||||
|
err := p.parseOne() |
||||
|
if err != nil { |
||||
|
return blocks, err |
||||
|
} |
||||
|
if len(p.block.Keys) > 0 { |
||||
|
blocks = append(blocks, p.block) |
||||
|
} |
||||
|
if p.nesting > 0 { |
||||
|
return blocks, p.EOFErr() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return blocks, nil |
||||
|
} |
||||
|
|
||||
|
func (p *parser) parseOne() error { |
||||
|
p.block = ServerBlock{} |
||||
|
return p.begin() |
||||
|
} |
||||
|
|
||||
|
func (p *parser) begin() error { |
||||
|
if len(p.tokens) == 0 { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
err := p.addresses() |
||||
|
|
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
if p.eof { |
||||
|
// this happens if the Caddyfile consists of only
|
||||
|
// a line of addresses and nothing else
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
if ok, name := p.isSnippet(); ok { |
||||
|
if p.definedSnippets == nil { |
||||
|
p.definedSnippets = map[string][]Token{} |
||||
|
} |
||||
|
if _, found := p.definedSnippets[name]; found { |
||||
|
return p.Errf("redeclaration of previously declared snippet %s", name) |
||||
|
} |
||||
|
// consume all tokens til matched close brace
|
||||
|
tokens, err := p.snippetTokens() |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
p.definedSnippets[name] = tokens |
||||
|
// empty block keys so we don't save this block as a real server.
|
||||
|
p.block.Keys = nil |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
return p.blockContents() |
||||
|
} |
||||
|
|
||||
|
func (p *parser) addresses() error { |
||||
|
var expectingAnother bool |
||||
|
|
||||
|
for { |
||||
|
tkn := replaceEnvVars(p.Val()) |
||||
|
|
||||
|
// special case: import directive replaces tokens during parse-time
|
||||
|
if tkn == "import" && p.isNewLine() { |
||||
|
err := p.doImport() |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
// Open brace definitely indicates end of addresses
|
||||
|
if tkn == "{" { |
||||
|
if expectingAnother { |
||||
|
return p.Errf("Expected another address but had '%s' - check for extra comma", tkn) |
||||
|
} |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
if tkn != "" { // empty token possible if user typed ""
|
||||
|
// Trailing comma indicates another address will follow, which
|
||||
|
// may possibly be on the next line
|
||||
|
if tkn[len(tkn)-1] == ',' { |
||||
|
tkn = tkn[:len(tkn)-1] |
||||
|
expectingAnother = true |
||||
|
} else { |
||||
|
expectingAnother = false // but we may still see another one on this line
|
||||
|
} |
||||
|
|
||||
|
p.block.Keys = append(p.block.Keys, tkn) |
||||
|
} |
||||
|
|
||||
|
// Advance token and possibly break out of loop or return error
|
||||
|
hasNext := p.Next() |
||||
|
if expectingAnother && !hasNext { |
||||
|
return p.EOFErr() |
||||
|
} |
||||
|
if !hasNext { |
||||
|
p.eof = true |
||||
|
break // EOF
|
||||
|
} |
||||
|
if !expectingAnother && p.isNewLine() { |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (p *parser) blockContents() error { |
||||
|
errOpenCurlyBrace := p.openCurlyBrace() |
||||
|
if errOpenCurlyBrace != nil { |
||||
|
// single-server configs don't need curly braces
|
||||
|
p.cursor-- |
||||
|
} |
||||
|
|
||||
|
err := p.directives() |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
// only look for close curly brace if there was an opening
|
||||
|
if errOpenCurlyBrace == nil { |
||||
|
err = p.closeCurlyBrace() |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// directives parses through all the lines for directives
|
||||
|
// and it expects the next token to be the first
|
||||
|
// directive. It goes until EOF or closing curly brace
|
||||
|
// which ends the server block.
|
||||
|
func (p *parser) directives() error { |
||||
|
for p.Next() { |
||||
|
// end of server block
|
||||
|
if p.Val() == "}" { |
||||
|
// p.nesting has already been decremented
|
||||
|
break |
||||
|
} |
||||
|
|
||||
|
// special case: import directive replaces tokens during parse-time
|
||||
|
if p.Val() == "import" { |
||||
|
err := p.doImport() |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
p.cursor-- // cursor is advanced when we continue, so roll back one more
|
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
// normal case: parse a directive as a new segment
|
||||
|
// (a "segment" is a line which starts with a directive
|
||||
|
// and which ends at the end of the line or at the end of
|
||||
|
// the block that is opened at the end of the line)
|
||||
|
if err := p.directive(); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// doImport swaps out the import directive and its argument
|
||||
|
// (a total of 2 tokens) with the tokens in the specified file
|
||||
|
// or globbing pattern. When the function returns, the cursor
|
||||
|
// is on the token before where the import directive was. In
|
||||
|
// other words, call Next() to access the first token that was
|
||||
|
// imported.
|
||||
|
func (p *parser) doImport() error { |
||||
|
// syntax checks
|
||||
|
if !p.NextArg() { |
||||
|
return p.ArgErr() |
||||
|
} |
||||
|
importPattern := replaceEnvVars(p.Val()) |
||||
|
if importPattern == "" { |
||||
|
return p.Err("Import requires a non-empty filepath") |
||||
|
} |
||||
|
if p.NextArg() { |
||||
|
return p.Err("Import takes only one argument (glob pattern or file)") |
||||
|
} |
||||
|
// splice out the import directive and its argument (2 tokens total)
|
||||
|
tokensBefore := p.tokens[:p.cursor-1] |
||||
|
tokensAfter := p.tokens[p.cursor+1:] |
||||
|
var importedTokens []Token |
||||
|
|
||||
|
// first check snippets. That is a simple, non-recursive replacement
|
||||
|
if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil { |
||||
|
importedTokens = p.definedSnippets[importPattern] |
||||
|
} else { |
||||
|
// make path relative to the file of the _token_ being processed rather
|
||||
|
// than current working directory (issue #867) and then use glob to get
|
||||
|
// list of matching filenames
|
||||
|
absFile, err := filepath.Abs(p.Dispenser.File()) |
||||
|
if err != nil { |
||||
|
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err) |
||||
|
} |
||||
|
|
||||
|
var matches []string |
||||
|
var globPattern string |
||||
|
if !filepath.IsAbs(importPattern) { |
||||
|
globPattern = filepath.Join(filepath.Dir(absFile), importPattern) |
||||
|
} else { |
||||
|
globPattern = importPattern |
||||
|
} |
||||
|
if strings.Count(globPattern, "*") > 1 || strings.Count(globPattern, "?") > 1 || |
||||
|
(strings.Contains(globPattern, "[") && strings.Contains(globPattern, "]")) { |
||||
|
// See issue #2096 - a pattern with many glob expansions can hang for too long
|
||||
|
return p.Errf("Glob pattern may only contain one wildcard (*), but has others: %s", globPattern) |
||||
|
} |
||||
|
matches, err = filepath.Glob(globPattern) |
||||
|
|
||||
|
if err != nil { |
||||
|
return p.Errf("Failed to use import pattern %s: %v", importPattern, err) |
||||
|
} |
||||
|
if len(matches) == 0 { |
||||
|
if strings.ContainsAny(globPattern, "*?[]") { |
||||
|
log.Printf("[WARNING] No files matching import glob pattern: %s", importPattern) |
||||
|
} else { |
||||
|
return p.Errf("File to import not found: %s", importPattern) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// collect all the imported tokens
|
||||
|
|
||||
|
for _, importFile := range matches { |
||||
|
newTokens, err := p.doSingleImport(importFile) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
importedTokens = append(importedTokens, newTokens...) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// splice the imported tokens in the place of the import statement
|
||||
|
// and rewind cursor so Next() will land on first imported token
|
||||
|
p.tokens = append(tokensBefore, append(importedTokens, tokensAfter...)...) |
||||
|
p.cursor-- |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// doSingleImport lexes the individual file at importFile and returns
|
||||
|
// its tokens or an error, if any.
|
||||
|
func (p *parser) doSingleImport(importFile string) ([]Token, error) { |
||||
|
file, err := os.Open(importFile) |
||||
|
if err != nil { |
||||
|
return nil, p.Errf("Could not import %s: %v", importFile, err) |
||||
|
} |
||||
|
defer file.Close() |
||||
|
|
||||
|
if info, err := file.Stat(); err != nil { |
||||
|
return nil, p.Errf("Could not import %s: %v", importFile, err) |
||||
|
} else if info.IsDir() { |
||||
|
return nil, p.Errf("Could not import %s: is a directory", importFile) |
||||
|
} |
||||
|
|
||||
|
importedTokens, err := allTokens(file) |
||||
|
if err != nil { |
||||
|
return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err) |
||||
|
} |
||||
|
|
||||
|
// Tack the file path onto these tokens so errors show the imported file's name
|
||||
|
// (we use full, absolute path to avoid bugs: issue #1892)
|
||||
|
filename, err := filepath.Abs(importFile) |
||||
|
if err != nil { |
||||
|
return nil, p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err) |
||||
|
} |
||||
|
for i := 0; i < len(importedTokens); i++ { |
||||
|
importedTokens[i].File = filename |
||||
|
} |
||||
|
|
||||
|
return importedTokens, nil |
||||
|
} |
||||
|
|
||||
|
// directive collects tokens until the directive's scope
|
||||
|
// closes (either end of line or end of curly brace block).
|
||||
|
// It expects the currently-loaded token to be a directive
|
||||
|
// (or } that ends a server block). The collected tokens
|
||||
|
// are loaded into the current server block for later use
|
||||
|
// by directive setup functions.
|
||||
|
func (p *parser) directive() error { |
||||
|
// evaluate any env vars in directive token
|
||||
|
p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text) |
||||
|
|
||||
|
// a segment is a list of tokens associated with this directive
|
||||
|
var segment Segment |
||||
|
|
||||
|
// the directive itself is appended as a relevant token
|
||||
|
segment = append(segment, p.Token()) |
||||
|
|
||||
|
for p.Next() { |
||||
|
if p.Val() == "{" { |
||||
|
p.nesting++ |
||||
|
} else if p.isNewLine() && p.nesting == 0 { |
||||
|
p.cursor-- // read too far
|
||||
|
break |
||||
|
} else if p.Val() == "}" && p.nesting > 0 { |
||||
|
p.nesting-- |
||||
|
} else if p.Val() == "}" && p.nesting == 0 { |
||||
|
return p.Err("Unexpected '}' because no matching opening brace") |
||||
|
} else if p.Val() == "import" && p.isNewLine() { |
||||
|
if err := p.doImport(); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
p.cursor-- // cursor is advanced when we continue, so roll back one more
|
||||
|
continue |
||||
|
} |
||||
|
p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text) |
||||
|
segment = append(segment, p.Token()) |
||||
|
} |
||||
|
|
||||
|
p.block.Segments = append(p.block.Segments, segment) |
||||
|
|
||||
|
if p.nesting > 0 { |
||||
|
return p.EOFErr() |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// openCurlyBrace expects the current token to be an
|
||||
|
// opening curly brace. This acts like an assertion
|
||||
|
// because it returns an error if the token is not
|
||||
|
// a opening curly brace. It does NOT advance the token.
|
||||
|
func (p *parser) openCurlyBrace() error { |
||||
|
if p.Val() != "{" { |
||||
|
return p.SyntaxErr("{") |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// closeCurlyBrace expects the current token to be
|
||||
|
// a closing curly brace. This acts like an assertion
|
||||
|
// because it returns an error if the token is not
|
||||
|
// a closing curly brace. It does NOT advance the token.
|
||||
|
func (p *parser) closeCurlyBrace() error { |
||||
|
if p.Val() != "}" { |
||||
|
return p.SyntaxErr("}") |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// replaceEnvVars replaces environment variables that appear in the token
|
||||
|
// and understands both the $UNIX and %WINDOWS% syntaxes.
|
||||
|
func replaceEnvVars(s string) string { |
||||
|
s = replaceEnvReferences(s, "{%", "%}") |
||||
|
s = replaceEnvReferences(s, "{$", "}") |
||||
|
return s |
||||
|
} |
||||
|
|
||||
|
// replaceEnvReferences performs the actual replacement of env variables
|
||||
|
// in s, given the placeholder start and placeholder end strings.
|
||||
|
func replaceEnvReferences(s, refStart, refEnd string) string { |
||||
|
index := strings.Index(s, refStart) |
||||
|
for index != -1 { |
||||
|
endIndex := strings.Index(s[index:], refEnd) |
||||
|
if endIndex == -1 { |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
endIndex += index |
||||
|
if endIndex > index+len(refStart) { |
||||
|
ref := s[index : endIndex+len(refEnd)] |
||||
|
s = strings.Replace(s, ref, os.Getenv(ref[len(refStart):len(ref)-len(refEnd)]), -1) |
||||
|
} else { |
||||
|
return s |
||||
|
} |
||||
|
index = strings.Index(s, refStart) |
||||
|
} |
||||
|
return s |
||||
|
} |
||||
|
|
||||
|
func (p *parser) isSnippet() (bool, string) { |
||||
|
keys := p.block.Keys |
||||
|
// A snippet block is a single key with parens. Nothing else qualifies.
|
||||
|
if len(keys) == 1 && strings.HasPrefix(keys[0], "(") && strings.HasSuffix(keys[0], ")") { |
||||
|
return true, strings.TrimSuffix(keys[0][1:], ")") |
||||
|
} |
||||
|
return false, "" |
||||
|
} |
||||
|
|
||||
|
// read and store everything in a block for later replay.
|
||||
|
func (p *parser) snippetTokens() ([]Token, error) { |
||||
|
// snippet must have curlies.
|
||||
|
err := p.openCurlyBrace() |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
nesting := 1 // count our own nesting in snippets
|
||||
|
tokens := []Token{} |
||||
|
for p.Next() { |
||||
|
if p.Val() == "}" { |
||||
|
nesting-- |
||||
|
if nesting == 0 { |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
if p.Val() == "{" { |
||||
|
nesting++ |
||||
|
} |
||||
|
tokens = append(tokens, p.tokens[p.cursor]) |
||||
|
} |
||||
|
// make sure we're matched up
|
||||
|
if nesting != 0 { |
||||
|
return nil, p.SyntaxErr("}") |
||||
|
} |
||||
|
return tokens, nil |
||||
|
} |
||||
|
|
||||
|
// ServerBlock associates any number of keys from the
|
||||
|
// head of the server block with tokens, which are
|
||||
|
// grouped by segments.
|
||||
|
type ServerBlock struct { |
||||
|
Keys []string |
||||
|
Segments []Segment |
||||
|
} |
||||
|
|
||||
|
// DispenseDirective returns a dispenser that contains
|
||||
|
// all the tokens in the server block.
|
||||
|
func (sb ServerBlock) DispenseDirective(dir string) *Dispenser { |
||||
|
var tokens []Token |
||||
|
for _, seg := range sb.Segments { |
||||
|
if len(seg) > 0 && seg[0].Text == dir { |
||||
|
tokens = append(tokens, seg...) |
||||
|
} |
||||
|
} |
||||
|
return NewDispenser("", tokens) |
||||
|
} |
||||
|
|
||||
|
// Segment is a list of tokens which begins with a directive
|
||||
|
// and ends at the end of the directive (either at the end of
|
||||
|
// the line, or at the end of a block it opens).
|
||||
|
type Segment []Token |
||||
|
|
||||
|
// Directive returns the directive name for the segment.
|
||||
|
// The directive name is the text of the first token.
|
||||
|
func (s Segment) Directive() string { |
||||
|
if len(s) > 0 { |
||||
|
return s[0].Text |
||||
|
} |
||||
|
return "" |
||||
|
} |
||||
|
|
||||
|
// NewDispenser returns a dispenser for this
|
||||
|
// segment's tokens.
|
||||
|
func (s Segment) NewDispenser() *Dispenser { |
||||
|
return NewDispenser("", s) |
||||
|
} |
||||
@ -0,0 +1,681 @@ |
|||||
|
// Copyright 2015 Light Code Labs, LLC
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package caddyfile |
||||
|
|
||||
|
import ( |
||||
|
"io/ioutil" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
"strings" |
||||
|
"testing" |
||||
|
) |
||||
|
|
||||
|
// TODO: re-enable all tests
|
||||
|
|
||||
|
func TestAllTokens(t *testing.T) { |
||||
|
input := strings.NewReader("a b c\nd e") |
||||
|
expected := []string{"a", "b", "c", "d", "e"} |
||||
|
tokens, err := allTokens(input) |
||||
|
|
||||
|
if err != nil { |
||||
|
t.Fatalf("Expected no error, got %v", err) |
||||
|
} |
||||
|
if len(tokens) != len(expected) { |
||||
|
t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens)) |
||||
|
} |
||||
|
|
||||
|
for i, val := range expected { |
||||
|
if tokens[i].Text != val { |
||||
|
t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].Text) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestParseOneAndImport(t *testing.T) { |
||||
|
testParseOne := func(input string) (ServerBlock, error) { |
||||
|
p := testParser(input) |
||||
|
p.Next() // parseOne doesn't call Next() to start, so we must
|
||||
|
err := p.parseOne() |
||||
|
return p.block, err |
||||
|
} |
||||
|
|
||||
|
for i, test := range []struct { |
||||
|
input string |
||||
|
shouldErr bool |
||||
|
keys []string |
||||
|
numTokens []int // number of tokens to expect in each segment
|
||||
|
}{ |
||||
|
{`localhost`, false, []string{ |
||||
|
"localhost", |
||||
|
}, []int{}}, |
||||
|
|
||||
|
{`localhost |
||||
|
dir1`, false, []string{ |
||||
|
"localhost", |
||||
|
}, []int{1}}, |
||||
|
|
||||
|
{`localhost:1234 |
||||
|
dir1 foo bar`, false, []string{ |
||||
|
"localhost:1234", |
||||
|
}, []int{3}, |
||||
|
}, |
||||
|
|
||||
|
{`localhost { |
||||
|
dir1 |
||||
|
}`, false, []string{ |
||||
|
"localhost", |
||||
|
}, []int{1}}, |
||||
|
|
||||
|
{`localhost:1234 { |
||||
|
dir1 foo bar |
||||
|
dir2 |
||||
|
}`, false, []string{ |
||||
|
"localhost:1234", |
||||
|
}, []int{3, 1}}, |
||||
|
|
||||
|
{`http://localhost https://localhost
|
||||
|
dir1 foo bar`, false, []string{ |
||||
|
"http://localhost", |
||||
|
"https://localhost", |
||||
|
}, []int{3}}, |
||||
|
|
||||
|
{`http://localhost https://localhost {
|
||||
|
dir1 foo bar |
||||
|
}`, false, []string{ |
||||
|
"http://localhost", |
||||
|
"https://localhost", |
||||
|
}, []int{3}}, |
||||
|
|
||||
|
{`http://localhost, https://localhost {
|
||||
|
dir1 foo bar |
||||
|
}`, false, []string{ |
||||
|
"http://localhost", |
||||
|
"https://localhost", |
||||
|
}, []int{3}}, |
||||
|
|
||||
|
{`http://localhost, {
|
||||
|
}`, true, []string{ |
||||
|
"http://localhost", |
||||
|
}, []int{}}, |
||||
|
|
||||
|
{`host1:80, http://host2.com
|
||||
|
dir1 foo bar |
||||
|
dir2 baz`, false, []string{ |
||||
|
"host1:80", |
||||
|
"http://host2.com", |
||||
|
}, []int{3, 2}}, |
||||
|
|
||||
|
{`http://host1.com,
|
||||
|
http://host2.com,
|
||||
|
https://host3.com`, false, []string{
|
||||
|
"http://host1.com", |
||||
|
"http://host2.com", |
||||
|
"https://host3.com", |
||||
|
}, []int{}}, |
||||
|
|
||||
|
{`http://host1.com:1234, https://host2.com
|
||||
|
dir1 foo { |
||||
|
bar baz |
||||
|
} |
||||
|
dir2`, false, []string{ |
||||
|
"http://host1.com:1234", |
||||
|
"https://host2.com", |
||||
|
}, []int{6, 1}}, |
||||
|
|
||||
|
{`127.0.0.1 |
||||
|
dir1 { |
||||
|
bar baz |
||||
|
} |
||||
|
dir2 { |
||||
|
foo bar |
||||
|
}`, false, []string{ |
||||
|
"127.0.0.1", |
||||
|
}, []int{5, 5}}, |
||||
|
|
||||
|
{`localhost |
||||
|
dir1 { |
||||
|
foo`, true, []string{ |
||||
|
"localhost", |
||||
|
}, []int{3}}, |
||||
|
|
||||
|
{`localhost |
||||
|
dir1 { |
||||
|
}`, false, []string{ |
||||
|
"localhost", |
||||
|
}, []int{3}}, |
||||
|
|
||||
|
{`localhost |
||||
|
dir1 { |
||||
|
} }`, true, []string{ |
||||
|
"localhost", |
||||
|
}, []int{}}, |
||||
|
|
||||
|
{`localhost |
||||
|
dir1 { |
||||
|
nested { |
||||
|
foo |
||||
|
} |
||||
|
} |
||||
|
dir2 foo bar`, false, []string{ |
||||
|
"localhost", |
||||
|
}, []int{7, 3}}, |
||||
|
|
||||
|
{``, false, []string{}, []int{}}, |
||||
|
|
||||
|
{`localhost |
||||
|
dir1 arg1 |
||||
|
import testdata/import_test1.txt`, false, []string{ |
||||
|
"localhost", |
||||
|
}, []int{2, 3, 1}}, |
||||
|
|
||||
|
{`import testdata/import_test2.txt`, false, []string{ |
||||
|
"host1", |
||||
|
}, []int{1, 2}}, |
||||
|
|
||||
|
{`import testdata/import_test1.txt testdata/import_test2.txt`, true, []string{}, []int{}}, |
||||
|
|
||||
|
{`import testdata/not_found.txt`, true, []string{}, []int{}}, |
||||
|
|
||||
|
{`""`, false, []string{}, []int{}}, |
||||
|
|
||||
|
{``, false, []string{}, []int{}}, |
||||
|
|
||||
|
// test cases found by fuzzing!
|
||||
|
{`import }{$"`, true, []string{}, []int{}}, |
||||
|
{`import /*/*.txt`, true, []string{}, []int{}}, |
||||
|
{`import /???/?*?o`, true, []string{}, []int{}}, |
||||
|
{`import /??`, true, []string{}, []int{}}, |
||||
|
{`import /[a-z]`, true, []string{}, []int{}}, |
||||
|
{`import {$}`, true, []string{}, []int{}}, |
||||
|
{`import {%}`, true, []string{}, []int{}}, |
||||
|
{`import {$$}`, true, []string{}, []int{}}, |
||||
|
{`import {%%}`, true, []string{}, []int{}}, |
||||
|
} { |
||||
|
result, err := testParseOne(test.input) |
||||
|
|
||||
|
if test.shouldErr && err == nil { |
||||
|
t.Errorf("Test %d: Expected an error, but didn't get one", i) |
||||
|
} |
||||
|
if !test.shouldErr && err != nil { |
||||
|
t.Errorf("Test %d: Expected no error, but got: %v", i, err) |
||||
|
} |
||||
|
|
||||
|
if len(result.Keys) != len(test.keys) { |
||||
|
t.Errorf("Test %d: Expected %d keys, got %d", |
||||
|
i, len(test.keys), len(result.Keys)) |
||||
|
continue |
||||
|
} |
||||
|
for j, addr := range result.Keys { |
||||
|
if addr != test.keys[j] { |
||||
|
t.Errorf("Test %d, key %d: Expected '%s', but was '%s'", |
||||
|
i, j, test.keys[j], addr) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if len(result.Segments) != len(test.numTokens) { |
||||
|
t.Errorf("Test %d: Expected %d segments, had %d", |
||||
|
i, len(test.numTokens), len(result.Segments)) |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
for j, seg := range result.Segments { |
||||
|
if len(seg) != test.numTokens[j] { |
||||
|
t.Errorf("Test %d, segment %d: Expected %d tokens, counted %d", |
||||
|
i, j, test.numTokens[j], len(seg)) |
||||
|
continue |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestRecursiveImport(t *testing.T) { |
||||
|
testParseOne := func(input string) (ServerBlock, error) { |
||||
|
p := testParser(input) |
||||
|
p.Next() // parseOne doesn't call Next() to start, so we must
|
||||
|
err := p.parseOne() |
||||
|
return p.block, err |
||||
|
} |
||||
|
|
||||
|
isExpected := func(got ServerBlock) bool { |
||||
|
if len(got.Keys) != 1 || got.Keys[0] != "localhost" { |
||||
|
t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys) |
||||
|
return false |
||||
|
} |
||||
|
if len(got.Segments) != 2 { |
||||
|
t.Errorf("got wrong number of segments: expect 2, got %d", len(got.Segments)) |
||||
|
return false |
||||
|
} |
||||
|
if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 2 { |
||||
|
t.Errorf("got unexpect tokens: %v", got.Segments) |
||||
|
return false |
||||
|
} |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
recursiveFile1, err := filepath.Abs("testdata/recursive_import_test1") |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
recursiveFile2, err := filepath.Abs("testdata/recursive_import_test2") |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
|
||||
|
// test relative recursive import
|
||||
|
err = ioutil.WriteFile(recursiveFile1, []byte( |
||||
|
`localhost |
||||
|
dir1 |
||||
|
import recursive_import_test2`), 0644) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
defer os.Remove(recursiveFile1) |
||||
|
|
||||
|
err = ioutil.WriteFile(recursiveFile2, []byte("dir2 1"), 0644) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
defer os.Remove(recursiveFile2) |
||||
|
|
||||
|
// import absolute path
|
||||
|
result, err := testParseOne("import " + recursiveFile1) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
if !isExpected(result) { |
||||
|
t.Error("absolute+relative import failed") |
||||
|
} |
||||
|
|
||||
|
// import relative path
|
||||
|
result, err = testParseOne("import testdata/recursive_import_test1") |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
if !isExpected(result) { |
||||
|
t.Error("relative+relative import failed") |
||||
|
} |
||||
|
|
||||
|
// test absolute recursive import
|
||||
|
err = ioutil.WriteFile(recursiveFile1, []byte( |
||||
|
`localhost |
||||
|
dir1 |
||||
|
import `+recursiveFile2), 0644) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
|
||||
|
// import absolute path
|
||||
|
result, err = testParseOne("import " + recursiveFile1) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
if !isExpected(result) { |
||||
|
t.Error("absolute+absolute import failed") |
||||
|
} |
||||
|
|
||||
|
// import relative path
|
||||
|
result, err = testParseOne("import testdata/recursive_import_test1") |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
if !isExpected(result) { |
||||
|
t.Error("relative+absolute import failed") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestDirectiveImport(t *testing.T) { |
||||
|
testParseOne := func(input string) (ServerBlock, error) { |
||||
|
p := testParser(input) |
||||
|
p.Next() // parseOne doesn't call Next() to start, so we must
|
||||
|
err := p.parseOne() |
||||
|
return p.block, err |
||||
|
} |
||||
|
|
||||
|
isExpected := func(got ServerBlock) bool { |
||||
|
if len(got.Keys) != 1 || got.Keys[0] != "localhost" { |
||||
|
t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys) |
||||
|
return false |
||||
|
} |
||||
|
if len(got.Segments) != 2 { |
||||
|
t.Errorf("got wrong number of segments: expect 2, got %d", len(got.Segments)) |
||||
|
return false |
||||
|
} |
||||
|
if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 8 { |
||||
|
t.Errorf("got unexpect tokens: %v", got.Segments) |
||||
|
return false |
||||
|
} |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
directiveFile, err := filepath.Abs("testdata/directive_import_test") |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
|
||||
|
err = ioutil.WriteFile(directiveFile, []byte(`prop1 1 |
||||
|
prop2 2`), 0644) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
defer os.Remove(directiveFile) |
||||
|
|
||||
|
// import from existing file
|
||||
|
result, err := testParseOne(`localhost |
||||
|
dir1 |
||||
|
proxy { |
||||
|
import testdata/directive_import_test |
||||
|
transparent |
||||
|
}`) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
if !isExpected(result) { |
||||
|
t.Error("directive import failed") |
||||
|
} |
||||
|
|
||||
|
// import from nonexistent file
|
||||
|
_, err = testParseOne(`localhost |
||||
|
dir1 |
||||
|
proxy { |
||||
|
import testdata/nonexistent_file |
||||
|
transparent |
||||
|
}`) |
||||
|
if err == nil { |
||||
|
t.Fatal("expected error when importing a nonexistent file") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestParseAll(t *testing.T) { |
||||
|
for i, test := range []struct { |
||||
|
input string |
||||
|
shouldErr bool |
||||
|
keys [][]string // keys per server block, in order
|
||||
|
}{ |
||||
|
{`localhost`, false, [][]string{ |
||||
|
{"localhost"}, |
||||
|
}}, |
||||
|
|
||||
|
{`localhost:1234`, false, [][]string{ |
||||
|
{"localhost:1234"}, |
||||
|
}}, |
||||
|
|
||||
|
{`localhost:1234 { |
||||
|
} |
||||
|
localhost:2015 { |
||||
|
}`, false, [][]string{ |
||||
|
{"localhost:1234"}, |
||||
|
{"localhost:2015"}, |
||||
|
}}, |
||||
|
|
||||
|
{`localhost:1234, http://host2`, false, [][]string{ |
||||
|
{"localhost:1234", "http://host2"}, |
||||
|
}}, |
||||
|
|
||||
|
{`localhost:1234, http://host2,`, true, [][]string{}}, |
||||
|
|
||||
|
{`http://host1.com, http://host2.com {
|
||||
|
} |
||||
|
https://host3.com, https://host4.com {
|
||||
|
}`, false, [][]string{ |
||||
|
{"http://host1.com", "http://host2.com"}, |
||||
|
{"https://host3.com", "https://host4.com"}, |
||||
|
}}, |
||||
|
|
||||
|
{`import testdata/import_glob*.txt`, false, [][]string{ |
||||
|
{"glob0.host0"}, |
||||
|
{"glob0.host1"}, |
||||
|
{"glob1.host0"}, |
||||
|
{"glob2.host0"}, |
||||
|
}}, |
||||
|
|
||||
|
{`import notfound/*`, false, [][]string{}}, // glob needn't error with no matches
|
||||
|
{`import notfound/file.conf`, true, [][]string{}}, // but a specific file should
|
||||
|
} { |
||||
|
p := testParser(test.input) |
||||
|
blocks, err := p.parseAll() |
||||
|
|
||||
|
if test.shouldErr && err == nil { |
||||
|
t.Errorf("Test %d: Expected an error, but didn't get one", i) |
||||
|
} |
||||
|
if !test.shouldErr && err != nil { |
||||
|
t.Errorf("Test %d: Expected no error, but got: %v", i, err) |
||||
|
} |
||||
|
|
||||
|
if len(blocks) != len(test.keys) { |
||||
|
t.Errorf("Test %d: Expected %d server blocks, got %d", |
||||
|
i, len(test.keys), len(blocks)) |
||||
|
continue |
||||
|
} |
||||
|
for j, block := range blocks { |
||||
|
if len(block.Keys) != len(test.keys[j]) { |
||||
|
t.Errorf("Test %d: Expected %d keys in block %d, got %d", |
||||
|
i, len(test.keys[j]), j, len(block.Keys)) |
||||
|
continue |
||||
|
} |
||||
|
for k, addr := range block.Keys { |
||||
|
if addr != test.keys[j][k] { |
||||
|
t.Errorf("Test %d, block %d, key %d: Expected '%s', but got '%s'", |
||||
|
i, j, k, test.keys[j][k], addr) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestEnvironmentReplacement(t *testing.T) { |
||||
|
os.Setenv("PORT", "8080") |
||||
|
os.Setenv("ADDRESS", "servername.com") |
||||
|
os.Setenv("FOOBAR", "foobar") |
||||
|
os.Setenv("PARTIAL_DIR", "r1") |
||||
|
|
||||
|
// basic test; unix-style env vars
|
||||
|
p := testParser(`{$ADDRESS}`) |
||||
|
blocks, _ := p.parseAll() |
||||
|
if actual, expected := blocks[0].Keys[0], "servername.com"; expected != actual { |
||||
|
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) |
||||
|
} |
||||
|
|
||||
|
// basic test; unix-style env vars
|
||||
|
p = testParser(`di{$PARTIAL_DIR}`) |
||||
|
blocks, _ = p.parseAll() |
||||
|
if actual, expected := blocks[0].Keys[0], "dir1"; expected != actual { |
||||
|
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) |
||||
|
} |
||||
|
|
||||
|
// multiple vars per token
|
||||
|
p = testParser(`{$ADDRESS}:{$PORT}`) |
||||
|
blocks, _ = p.parseAll() |
||||
|
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual { |
||||
|
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) |
||||
|
} |
||||
|
|
||||
|
// windows-style var and unix style in same token
|
||||
|
p = testParser(`{%ADDRESS%}:{$PORT}`) |
||||
|
blocks, _ = p.parseAll() |
||||
|
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual { |
||||
|
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) |
||||
|
} |
||||
|
|
||||
|
// reverse order
|
||||
|
p = testParser(`{$ADDRESS}:{%PORT%}`) |
||||
|
blocks, _ = p.parseAll() |
||||
|
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual { |
||||
|
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) |
||||
|
} |
||||
|
|
||||
|
// env var in server block body as argument
|
||||
|
p = testParser(":{%PORT%}\ndir1 {$FOOBAR}") |
||||
|
blocks, _ = p.parseAll() |
||||
|
if actual, expected := blocks[0].Keys[0], ":8080"; expected != actual { |
||||
|
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) |
||||
|
} |
||||
|
if actual, expected := blocks[0].Segments[0][1].Text, "foobar"; expected != actual { |
||||
|
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) |
||||
|
} |
||||
|
|
||||
|
// combined windows env vars in argument
|
||||
|
p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}") |
||||
|
blocks, _ = p.parseAll() |
||||
|
if actual, expected := blocks[0].Segments[0][1].Text, "servername.com/foobar"; expected != actual { |
||||
|
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) |
||||
|
} |
||||
|
|
||||
|
// malformed env var (windows)
|
||||
|
p = testParser(":1234\ndir1 {%ADDRESS}") |
||||
|
blocks, _ = p.parseAll() |
||||
|
if actual, expected := blocks[0].Segments[0][1].Text, "{%ADDRESS}"; expected != actual { |
||||
|
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual) |
||||
|
} |
||||
|
|
||||
|
// malformed (non-existent) env var (unix)
|
||||
|
p = testParser(`:{$PORT$}`) |
||||
|
blocks, _ = p.parseAll() |
||||
|
if actual, expected := blocks[0].Keys[0], ":"; expected != actual { |
||||
|
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual) |
||||
|
} |
||||
|
|
||||
|
// in quoted field
|
||||
|
p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"") |
||||
|
blocks, _ = p.parseAll() |
||||
|
if actual, expected := blocks[0].Segments[0][1].Text, "Test foobar test"; expected != actual { |
||||
|
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) |
||||
|
} |
||||
|
|
||||
|
// after end token
|
||||
|
p = testParser(":1234\nanswer \"{{ .Name }} {$FOOBAR}\"") |
||||
|
blocks, _ = p.parseAll() |
||||
|
if actual, expected := blocks[0].Segments[0][1].Text, "{{ .Name }} foobar"; expected != actual { |
||||
|
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestSnippets(t *testing.T) { |
||||
|
p := testParser(` |
||||
|
(common) { |
||||
|
gzip foo |
||||
|
errors stderr |
||||
|
} |
||||
|
http://example.com {
|
||||
|
import common |
||||
|
} |
||||
|
`) |
||||
|
blocks, err := p.parseAll() |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
for _, b := range blocks { |
||||
|
t.Log(b.Keys) |
||||
|
t.Log(b.Segments) |
||||
|
} |
||||
|
if len(blocks) != 1 { |
||||
|
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks)) |
||||
|
} |
||||
|
if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual { |
||||
|
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual) |
||||
|
} |
||||
|
if len(blocks[0].Segments) != 2 { |
||||
|
t.Fatalf("Server block should have tokens from import, got: %+v", blocks[0]) |
||||
|
} |
||||
|
if actual, expected := blocks[0].Segments[0][0].Text, "gzip"; expected != actual { |
||||
|
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) |
||||
|
} |
||||
|
if actual, expected := blocks[0].Segments[1][1].Text, "stderr"; expected != actual { |
||||
|
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) { |
||||
|
file, err := ioutil.TempFile("", t.Name()) |
||||
|
if err != nil { |
||||
|
panic(err) // get a stack trace so we know where this was called from.
|
||||
|
} |
||||
|
if _, err := file.WriteString(str); err != nil { |
||||
|
panic(err) |
||||
|
} |
||||
|
if err := file.Close(); err != nil { |
||||
|
panic(err) |
||||
|
} |
||||
|
return file.Name() |
||||
|
} |
||||
|
|
||||
|
func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) { |
||||
|
fileName := writeStringToTempFileOrDie(t, ` |
||||
|
http://example.com {
|
||||
|
# This isn't an import directive, it's just an arg with value 'import' |
||||
|
basicauth / import password |
||||
|
} |
||||
|
`) |
||||
|
// Parse the root file that imports the other one.
|
||||
|
p := testParser(`import ` + fileName) |
||||
|
blocks, err := p.parseAll() |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
for _, b := range blocks { |
||||
|
t.Log(b.Keys) |
||||
|
t.Log(b.Segments) |
||||
|
} |
||||
|
auth := blocks[0].Segments[0] |
||||
|
line := auth[0].Text + " " + auth[1].Text + " " + auth[2].Text + " " + auth[3].Text |
||||
|
if line != "basicauth / import password" { |
||||
|
// Previously, it would be changed to:
|
||||
|
// basicauth / import /path/to/test/dir/password
|
||||
|
// referencing a file that (probably) doesn't exist and changing the
|
||||
|
// password!
|
||||
|
t.Errorf("Expected basicauth tokens to be 'basicauth / import password' but got %#q", line) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestSnippetAcrossMultipleFiles(t *testing.T) { |
||||
|
// Make the derived Caddyfile that expects (common) to be defined.
|
||||
|
fileName := writeStringToTempFileOrDie(t, ` |
||||
|
http://example.com {
|
||||
|
import common |
||||
|
} |
||||
|
`) |
||||
|
|
||||
|
// Parse the root file that defines (common) and then imports the other one.
|
||||
|
p := testParser(` |
||||
|
(common) { |
||||
|
gzip foo |
||||
|
} |
||||
|
import ` + fileName + ` |
||||
|
`) |
||||
|
|
||||
|
blocks, err := p.parseAll() |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
for _, b := range blocks { |
||||
|
t.Log(b.Keys) |
||||
|
t.Log(b.Segments) |
||||
|
} |
||||
|
if len(blocks) != 1 { |
||||
|
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks)) |
||||
|
} |
||||
|
if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual { |
||||
|
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual) |
||||
|
} |
||||
|
if len(blocks[0].Segments) != 1 { |
||||
|
t.Fatalf("Server block should have tokens from import") |
||||
|
} |
||||
|
if actual, expected := blocks[0].Segments[0][0].Text, "gzip"; expected != actual { |
||||
|
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func testParser(input string) parser { |
||||
|
return parser{Dispenser: newTestDispenser(input)} |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
glob0.host0 { |
||||
|
dir2 arg1 |
||||
|
} |
||||
|
|
||||
|
glob0.host1 { |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
glob1.host0 { |
||||
|
dir1 |
||||
|
dir2 arg1 |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
glob2.host0 { |
||||
|
dir2 arg1 |
||||
|
} |
||||
@ -0,0 +1,2 @@ |
|||||
|
dir2 arg1 arg2 |
||||
|
dir3 |
||||
@ -0,0 +1,4 @@ |
|||||
|
host1 { |
||||
|
dir1 |
||||
|
dir2 arg1 |
||||
|
} |
||||
@ -0,0 +1,113 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package caddyconfig |
||||
|
|
||||
|
import ( |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
) |
||||
|
|
||||
|
// Adapter is a type which can adapt a configuration to Caddy JSON.
|
||||
|
// It returns the results and any warnings, or an error.
|
||||
|
type Adapter interface { |
||||
|
Adapt(body []byte, options map[string]string) ([]byte, []Warning, error) |
||||
|
} |
||||
|
|
||||
|
// Warning represents a warning or notice related to conversion.
|
||||
|
type Warning struct { |
||||
|
File string |
||||
|
Line int |
||||
|
Directive string |
||||
|
Message string |
||||
|
} |
||||
|
|
||||
|
// JSON encodes val as JSON, returning it as a json.RawMessage. Any
|
||||
|
// marshaling errors (which are highly unlikely with correct code)
|
||||
|
// are converted to warnings. This is convenient when filling config
|
||||
|
// structs that require a json.RawMessage, without having to worry
|
||||
|
// about errors.
|
||||
|
func JSON(val interface{}, warnings *[]Warning) json.RawMessage { |
||||
|
b, err := json.Marshal(val) |
||||
|
if err != nil { |
||||
|
if warnings != nil { |
||||
|
*warnings = append(*warnings, Warning{Message: err.Error()}) |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
return b |
||||
|
} |
||||
|
|
||||
|
// JSONModuleObject is like JSON, except it marshals val into a JSON object
|
||||
|
// and then adds a key to that object named fieldName with the value fieldVal.
|
||||
|
// This is useful for JSON-encoding module values where the module name has to
|
||||
|
// be described within the object by a certain key; for example,
|
||||
|
// "responder": "file_server" for a file server HTTP responder. The val must
|
||||
|
// encode into a map[string]interface{} (i.e. it must be a struct or map),
|
||||
|
// and any errors are converted into warnings, so this can be conveniently
|
||||
|
// used when filling a struct. For correct code, there should be no errors.
|
||||
|
func JSONModuleObject(val interface{}, fieldName, fieldVal string, warnings *[]Warning) json.RawMessage { |
||||
|
// encode to a JSON object first
|
||||
|
enc, err := json.Marshal(val) |
||||
|
if err != nil { |
||||
|
if warnings != nil { |
||||
|
*warnings = append(*warnings, Warning{Message: err.Error()}) |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// then decode the object
|
||||
|
var tmp map[string]interface{} |
||||
|
err = json.Unmarshal(enc, &tmp) |
||||
|
if err != nil { |
||||
|
if warnings != nil { |
||||
|
*warnings = append(*warnings, Warning{Message: err.Error()}) |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// so we can easily add the module's field with its appointed value
|
||||
|
tmp[fieldName] = fieldVal |
||||
|
|
||||
|
// then re-marshal as JSON
|
||||
|
result, err := json.Marshal(tmp) |
||||
|
if err != nil { |
||||
|
if warnings != nil { |
||||
|
*warnings = append(*warnings, Warning{Message: err.Error()}) |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
return result |
||||
|
} |
||||
|
|
||||
|
// JSONIndent is used to JSON-marshal the final resulting Caddy
|
||||
|
// configuration in a consistent, human-readable way.
|
||||
|
func JSONIndent(val interface{}) ([]byte, error) { |
||||
|
return json.MarshalIndent(val, "", "\t") |
||||
|
} |
||||
|
|
||||
|
func RegisterAdapter(name string, adapter Adapter) error { |
||||
|
if _, ok := configAdapters[name]; ok { |
||||
|
return fmt.Errorf("%s: already registered", name) |
||||
|
} |
||||
|
configAdapters[name] = adapter |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func GetAdapter(name string) Adapter { |
||||
|
return configAdapters[name] |
||||
|
} |
||||
|
|
||||
|
var configAdapters = make(map[string]Adapter) |
||||
@ -0,0 +1,332 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package httpcaddyfile |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"net" |
||||
|
"net/url" |
||||
|
"reflect" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
|
||||
|
"github.com/caddyserver/caddy/caddyconfig/caddyfile" |
||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp" |
||||
|
"github.com/mholt/certmagic" |
||||
|
) |
||||
|
|
||||
|
// mapAddressToServerBlocks returns a map of listener address to list of server
|
||||
|
// blocks that will be served on that address. To do this, each server block is
|
||||
|
// expanded so that each one is considered individually, although keys of a
|
||||
|
// server block that share the same address stay grouped together so the config
|
||||
|
// isn't repeated unnecessarily. For example, this Caddyfile:
|
||||
|
//
|
||||
|
// example.com {
|
||||
|
// bind 127.0.0.1
|
||||
|
// }
|
||||
|
// www.example.com, example.net/path, localhost:9999 {
|
||||
|
// bind 127.0.0.1 1.2.3.4
|
||||
|
// }
|
||||
|
//
|
||||
|
// has two server blocks to start with. But expressed in this Caddyfile are
|
||||
|
// actually 4 listener addresses: 127.0.0.1:443, 1.2.3.4:443, 127.0.0.1:9999,
|
||||
|
// and 127.0.0.1:9999. This is because the bind directive is applied to each
|
||||
|
// key of its server block (specifying the host part), and each key may have
|
||||
|
// a different port. And we definitely need to be sure that a site which is
|
||||
|
// bound to be served on a specific interface is not served on others just
|
||||
|
// beceause that is more convenient: it would be a potential security risk
|
||||
|
// if the difference between interfaces means private vs. public.
|
||||
|
//
|
||||
|
// So what this function does for the example above is iterate each server
|
||||
|
// block, and for each server block, iterate its keys. For the first, it
|
||||
|
// finds one key (example.com) and determines its listener address
|
||||
|
// (127.0.0.1:443 - because of 'bind' and automatic HTTPS). It then adds
|
||||
|
// the listener address to the map value returned by this function, with
|
||||
|
// the first server block as one of its associations.
|
||||
|
//
|
||||
|
// It then iterates each key on the second server block and associates them
|
||||
|
// with one or more listener addresses. Indeed, each key in this block has
|
||||
|
// two listener addresses because of the 'bind' directive. Once we know
|
||||
|
// which addresses serve which keys, we can create a new server block for
|
||||
|
// each address containing the contents of the server block and only those
|
||||
|
// specific keys of the server block which use that address.
|
||||
|
//
|
||||
|
// It is possible and even likely that some keys in the returned map have
|
||||
|
// the exact same list of server blocks (i.e. they are identical). This
|
||||
|
// happens when multiple hosts are declared with a 'bind' directive and
|
||||
|
// the resulting listener addresses are not shared by any other server
|
||||
|
// block (or the other server blocks are exactly identical in their token
|
||||
|
// contents). This happens with our example above because 1.2.3.4:443
|
||||
|
// and 1.2.3.4:9999 are used exclusively with the second server block. This
|
||||
|
// repetition may be undesirable, so call consolidateAddrMappings() to map
|
||||
|
// multiple addresses to the same lists of server blocks (a many:many mapping).
|
||||
|
// (Doing this is essentially a map-reduce technique.)
|
||||
|
func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock) (map[string][]serverBlock, error) { |
||||
|
sbmap := make(map[string][]serverBlock) |
||||
|
|
||||
|
for i, sblock := range originalServerBlocks { |
||||
|
// within a server block, we need to map all the listener addresses
|
||||
|
// implied by the server block to the keys of the server block which
|
||||
|
// will be served by them; this has the effect of treating each
|
||||
|
// key of a server block as its own, but without having to repeat its
|
||||
|
// contents in cases where multiple keys really can be served together
|
||||
|
addrToKeys := make(map[string][]string) |
||||
|
for j, key := range sblock.block.Keys { |
||||
|
// a key can have multiple listener addresses if there are multiple
|
||||
|
// arguments to the 'bind' directive (although they will all have
|
||||
|
// the same port, since the port is defined by the key or is implicit
|
||||
|
// through automatic HTTPS)
|
||||
|
addrs, err := st.listenerAddrsForServerBlockKey(sblock, key) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("server block %d, key %d (%s): determining listener address: %v", i, j, key, err) |
||||
|
} |
||||
|
|
||||
|
// associate this key with each listener address it is served on
|
||||
|
for _, addr := range addrs { |
||||
|
addrToKeys[addr] = append(addrToKeys[addr], key) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// now that we know which addresses serve which keys of this
|
||||
|
// server block, we iterate that mapping and create a list of
|
||||
|
// new server blocks for each address where the keys of the
|
||||
|
// server block are only the ones which use the address; but
|
||||
|
// the contents (tokens) are of course the same
|
||||
|
for addr, keys := range addrToKeys { |
||||
|
sbmap[addr] = append(sbmap[addr], serverBlock{ |
||||
|
block: caddyfile.ServerBlock{ |
||||
|
Keys: keys, |
||||
|
Segments: sblock.block.Segments, |
||||
|
}, |
||||
|
pile: sblock.pile, |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return sbmap, nil |
||||
|
} |
||||
|
|
||||
|
// consolidateAddrMappings eliminates repetition of identical server blocks in a mapping of
|
||||
|
// single listener addresses to lists of server blocks. Since multiple addresses may serve
|
||||
|
// identical sites (server block contents), this function turns a 1:many mapping into a
|
||||
|
// many:many mapping. Server block contents (tokens) must be exactly identical so that
|
||||
|
// reflect.DeepEqual returns true in order for the addresses to be combined. Identical
|
||||
|
// entries are deleted from the addrToServerBlocks map. Essentially, each pairing (each
|
||||
|
// association from multiple addresses to multiple server blocks; i.e. each element of
|
||||
|
// the returned slice) becomes a server definition in the output JSON.
|
||||
|
func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]serverBlock) []sbAddrAssociation { |
||||
|
var sbaddrs []sbAddrAssociation |
||||
|
for addr, sblocks := range addrToServerBlocks { |
||||
|
// we start with knowing that at least this address
|
||||
|
// maps to these server blocks
|
||||
|
a := sbAddrAssociation{ |
||||
|
addresses: []string{addr}, |
||||
|
serverBlocks: sblocks, |
||||
|
} |
||||
|
|
||||
|
// now find other addresses that map to identical
|
||||
|
// server blocks and add them to our list of
|
||||
|
// addresses, while removing them from the map
|
||||
|
for otherAddr, otherSblocks := range addrToServerBlocks { |
||||
|
if addr == otherAddr { |
||||
|
continue |
||||
|
} |
||||
|
if reflect.DeepEqual(sblocks, otherSblocks) { |
||||
|
a.addresses = append(a.addresses, otherAddr) |
||||
|
delete(addrToServerBlocks, otherAddr) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
sbaddrs = append(sbaddrs, a) |
||||
|
} |
||||
|
return sbaddrs |
||||
|
} |
||||
|
|
||||
|
func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string) ([]string, error) { |
||||
|
addr, err := ParseAddress(key) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("parsing key: %v", err) |
||||
|
} |
||||
|
addr = addr.Normalize() |
||||
|
|
||||
|
lnPort := defaultPort |
||||
|
if addr.Port != "" { |
||||
|
// port explicitly defined
|
||||
|
lnPort = addr.Port |
||||
|
} else if certmagic.HostQualifies(addr.Host) { |
||||
|
// automatic HTTPS
|
||||
|
lnPort = strconv.Itoa(certmagic.HTTPSPort) |
||||
|
} |
||||
|
|
||||
|
// the bind directive specifies hosts, but is optional
|
||||
|
var lnHosts []string |
||||
|
for _, cfgVal := range sblock.pile["bind"] { |
||||
|
lnHosts = append(lnHosts, cfgVal.Value.([]string)...) |
||||
|
} |
||||
|
if len(lnHosts) == 0 { |
||||
|
lnHosts = []string{""} |
||||
|
} |
||||
|
|
||||
|
// use a map to prevent duplication
|
||||
|
listeners := make(map[string]struct{}) |
||||
|
for _, host := range lnHosts { |
||||
|
listeners[net.JoinHostPort(host, lnPort)] = struct{}{} |
||||
|
} |
||||
|
|
||||
|
// now turn map into list
|
||||
|
var listenersList []string |
||||
|
for lnStr := range listeners { |
||||
|
listenersList = append(listenersList, lnStr) |
||||
|
} |
||||
|
// sort.Strings(listenersList) // TODO: is sorting necessary?
|
||||
|
|
||||
|
return listenersList, nil |
||||
|
} |
||||
|
|
||||
|
// Address represents a site address. It contains
|
||||
|
// the original input value, and the component
|
||||
|
// parts of an address. The component parts may be
|
||||
|
// updated to the correct values as setup proceeds,
|
||||
|
// but the original value should never be changed.
|
||||
|
//
|
||||
|
// The Host field must be in a normalized form.
|
||||
|
type Address struct { |
||||
|
Original, Scheme, Host, Port, Path string |
||||
|
} |
||||
|
|
||||
|
// ParseAddress parses an address string into a structured format with separate
|
||||
|
// scheme, host, port, and path portions, as well as the original input string.
|
||||
|
func ParseAddress(str string) (Address, error) { |
||||
|
httpPort, httpsPort := strconv.Itoa(certmagic.HTTPPort), strconv.Itoa(certmagic.HTTPSPort) |
||||
|
|
||||
|
input := str |
||||
|
|
||||
|
// Split input into components (prepend with // to force host portion by default)
|
||||
|
if !strings.Contains(str, "//") && !strings.HasPrefix(str, "/") { |
||||
|
str = "//" + str |
||||
|
} |
||||
|
|
||||
|
u, err := url.Parse(str) |
||||
|
if err != nil { |
||||
|
return Address{}, err |
||||
|
} |
||||
|
|
||||
|
// separate host and port
|
||||
|
host, port, err := net.SplitHostPort(u.Host) |
||||
|
if err != nil { |
||||
|
host, port, err = net.SplitHostPort(u.Host + ":") |
||||
|
if err != nil { |
||||
|
host = u.Host |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// see if we can set port based off scheme
|
||||
|
if port == "" { |
||||
|
if u.Scheme == "http" { |
||||
|
port = httpPort |
||||
|
} else if u.Scheme == "https" { |
||||
|
port = httpsPort |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// error if scheme and port combination violate convention
|
||||
|
if (u.Scheme == "http" && port == httpsPort) || (u.Scheme == "https" && port == httpPort) { |
||||
|
return Address{}, fmt.Errorf("[%s] scheme and port violate convention", input) |
||||
|
} |
||||
|
|
||||
|
return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err |
||||
|
} |
||||
|
|
||||
|
// TODO: which of the methods on Address are even used?
|
||||
|
|
||||
|
// String returns a human-readable form of a. It will
|
||||
|
// be a cleaned-up and filled-out URL string.
|
||||
|
func (a Address) String() string { |
||||
|
if a.Host == "" && a.Port == "" { |
||||
|
return "" |
||||
|
} |
||||
|
scheme := a.Scheme |
||||
|
if scheme == "" { |
||||
|
if a.Port == strconv.Itoa(certmagic.HTTPSPort) { |
||||
|
scheme = "https" |
||||
|
} else { |
||||
|
scheme = "http" |
||||
|
} |
||||
|
} |
||||
|
s := scheme |
||||
|
if s != "" { |
||||
|
s += "://" |
||||
|
} |
||||
|
if a.Port != "" && |
||||
|
((scheme == "https" && a.Port != strconv.Itoa(caddyhttp.DefaultHTTPSPort)) || |
||||
|
(scheme == "http" && a.Port != strconv.Itoa(caddyhttp.DefaultHTTPPort))) { |
||||
|
s += net.JoinHostPort(a.Host, a.Port) |
||||
|
} else { |
||||
|
s += a.Host |
||||
|
} |
||||
|
if a.Path != "" { |
||||
|
s += a.Path |
||||
|
} |
||||
|
return s |
||||
|
} |
||||
|
|
||||
|
// Normalize returns a normalized version of a.
|
||||
|
func (a Address) Normalize() Address { |
||||
|
path := a.Path |
||||
|
if !caseSensitivePath { |
||||
|
path = strings.ToLower(path) |
||||
|
} |
||||
|
|
||||
|
// ensure host is normalized if it's an IP address
|
||||
|
host := a.Host |
||||
|
if ip := net.ParseIP(host); ip != nil { |
||||
|
host = ip.String() |
||||
|
} |
||||
|
|
||||
|
return Address{ |
||||
|
Original: a.Original, |
||||
|
Scheme: strings.ToLower(a.Scheme), |
||||
|
Host: strings.ToLower(host), |
||||
|
Port: a.Port, |
||||
|
Path: path, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Key returns a string form of a, much like String() does, but this
|
||||
|
// method doesn't add anything default that wasn't in the original.
|
||||
|
func (a Address) Key() string { |
||||
|
res := "" |
||||
|
if a.Scheme != "" { |
||||
|
res += a.Scheme + "://" |
||||
|
} |
||||
|
if a.Host != "" { |
||||
|
res += a.Host |
||||
|
} |
||||
|
// insert port only if the original has its own explicit port
|
||||
|
if a.Port != "" && |
||||
|
len(a.Original) >= len(res) && |
||||
|
strings.HasPrefix(a.Original[len(res):], ":"+a.Port) { |
||||
|
res += ":" + a.Port |
||||
|
} |
||||
|
if a.Path != "" { |
||||
|
res += a.Path |
||||
|
} |
||||
|
return res |
||||
|
} |
||||
|
|
||||
|
const ( |
||||
|
defaultPort = "2015" |
||||
|
caseSensitivePath = false // TODO: Used?
|
||||
|
) |
||||
@ -0,0 +1,166 @@ |
|||||
|
package httpcaddyfile |
||||
|
|
||||
|
import ( |
||||
|
"strings" |
||||
|
"testing" |
||||
|
) |
||||
|
|
||||
|
func TestParseAddress(t *testing.T) { |
||||
|
for i, test := range []struct { |
||||
|
input string |
||||
|
scheme, host, port, path string |
||||
|
shouldErr bool |
||||
|
}{ |
||||
|
{`localhost`, "", "localhost", "", "", false}, |
||||
|
{`localhost:1234`, "", "localhost", "1234", "", false}, |
||||
|
{`localhost:`, "", "localhost", "", "", false}, |
||||
|
{`0.0.0.0`, "", "0.0.0.0", "", "", false}, |
||||
|
{`127.0.0.1:1234`, "", "127.0.0.1", "1234", "", false}, |
||||
|
{`:1234`, "", "", "1234", "", false}, |
||||
|
{`[::1]`, "", "::1", "", "", false}, |
||||
|
{`[::1]:1234`, "", "::1", "1234", "", false}, |
||||
|
{`:`, "", "", "", "", false}, |
||||
|
{`:http`, "", "", "", "", true}, |
||||
|
{`:https`, "", "", "", "", true}, |
||||
|
{`localhost:http`, "", "", "", "", true}, // using service name in port is verboten, as of Go 1.12.8
|
||||
|
{`localhost:https`, "", "", "", "", true}, |
||||
|
{`http://localhost:https`, "", "", "", "", true}, // conflict
|
||||
|
{`http://localhost:http`, "", "", "", "", true}, // repeated scheme
|
||||
|
{`host:https/path`, "", "", "", "", true}, |
||||
|
{`http://localhost:443`, "", "", "", "", true}, // not conventional
|
||||
|
{`https://localhost:80`, "", "", "", "", true}, // not conventional
|
||||
|
{`http://localhost`, "http", "localhost", "80", "", false}, |
||||
|
{`https://localhost`, "https", "localhost", "443", "", false}, |
||||
|
{`http://127.0.0.1`, "http", "127.0.0.1", "80", "", false}, |
||||
|
{`https://127.0.0.1`, "https", "127.0.0.1", "443", "", false}, |
||||
|
{`http://[::1]`, "http", "::1", "80", "", false}, |
||||
|
{`http://localhost:1234`, "http", "localhost", "1234", "", false}, |
||||
|
{`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", "", false}, |
||||
|
{`http://[::1]:1234`, "http", "::1", "1234", "", false}, |
||||
|
{``, "", "", "", "", false}, |
||||
|
{`::1`, "", "::1", "", "", true}, |
||||
|
{`localhost::`, "", "localhost::", "", "", true}, |
||||
|
{`#$%@`, "", "", "", "", true}, |
||||
|
{`host/path`, "", "host", "", "/path", false}, |
||||
|
{`http://host/`, "http", "host", "80", "/", false}, |
||||
|
{`//asdf`, "", "asdf", "", "", false}, |
||||
|
{`:1234/asdf`, "", "", "1234", "/asdf", false}, |
||||
|
{`http://host/path`, "http", "host", "80", "/path", false}, |
||||
|
{`https://host:443/path/foo`, "https", "host", "443", "/path/foo", false}, |
||||
|
{`host:80/path`, "", "host", "80", "/path", false}, |
||||
|
{`/path`, "", "", "", "/path", false}, |
||||
|
} { |
||||
|
actual, err := ParseAddress(test.input) |
||||
|
|
||||
|
if err != nil && !test.shouldErr { |
||||
|
t.Errorf("Test %d (%s): Expected no error, but had error: %v", i, test.input, err) |
||||
|
} |
||||
|
if err == nil && test.shouldErr { |
||||
|
t.Errorf("Test %d (%s): Expected error, but had none", i, test.input) |
||||
|
} |
||||
|
|
||||
|
if !test.shouldErr && actual.Original != test.input { |
||||
|
t.Errorf("Test %d (%s): Expected original '%s', got '%s'", i, test.input, test.input, actual.Original) |
||||
|
} |
||||
|
if actual.Scheme != test.scheme { |
||||
|
t.Errorf("Test %d (%s): Expected scheme '%s', got '%s'", i, test.input, test.scheme, actual.Scheme) |
||||
|
} |
||||
|
if actual.Host != test.host { |
||||
|
t.Errorf("Test %d (%s): Expected host '%s', got '%s'", i, test.input, test.host, actual.Host) |
||||
|
} |
||||
|
if actual.Port != test.port { |
||||
|
t.Errorf("Test %d (%s): Expected port '%s', got '%s'", i, test.input, test.port, actual.Port) |
||||
|
} |
||||
|
if actual.Path != test.path { |
||||
|
t.Errorf("Test %d (%s): Expected path '%s', got '%s'", i, test.input, test.path, actual.Path) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestAddressString(t *testing.T) { |
||||
|
for i, test := range []struct { |
||||
|
addr Address |
||||
|
expected string |
||||
|
}{ |
||||
|
{Address{Scheme: "http", Host: "host", Port: "1234", Path: "/path"}, "http://host:1234/path"}, |
||||
|
{Address{Scheme: "", Host: "host", Port: "", Path: ""}, "http://host"}, |
||||
|
{Address{Scheme: "", Host: "host", Port: "80", Path: ""}, "http://host"}, |
||||
|
{Address{Scheme: "", Host: "host", Port: "443", Path: ""}, "https://host"}, |
||||
|
{Address{Scheme: "https", Host: "host", Port: "443", Path: ""}, "https://host"}, |
||||
|
{Address{Scheme: "https", Host: "host", Port: "", Path: ""}, "https://host"}, |
||||
|
{Address{Scheme: "", Host: "host", Port: "80", Path: "/path"}, "http://host/path"}, |
||||
|
{Address{Scheme: "http", Host: "", Port: "1234", Path: ""}, "http://:1234"}, |
||||
|
{Address{Scheme: "", Host: "", Port: "", Path: ""}, ""}, |
||||
|
} { |
||||
|
actual := test.addr.String() |
||||
|
if actual != test.expected { |
||||
|
t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expected, actual) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestKeyNormalization(t *testing.T) { |
||||
|
testCases := []struct { |
||||
|
input string |
||||
|
expect string |
||||
|
}{ |
||||
|
{ |
||||
|
input: "http://host:1234/path", |
||||
|
expect: "http://host:1234/path", |
||||
|
}, |
||||
|
{ |
||||
|
input: "HTTP://A/ABCDEF", |
||||
|
expect: "http://a/ABCDEF", |
||||
|
}, |
||||
|
{ |
||||
|
input: "A/ABCDEF", |
||||
|
expect: "a/ABCDEF", |
||||
|
}, |
||||
|
{ |
||||
|
input: "A:2015/Path", |
||||
|
expect: "a:2015/Path", |
||||
|
}, |
||||
|
{ |
||||
|
input: ":80", |
||||
|
expect: ":80", |
||||
|
}, |
||||
|
{ |
||||
|
input: ":443", |
||||
|
expect: ":443", |
||||
|
}, |
||||
|
{ |
||||
|
input: ":1234", |
||||
|
expect: ":1234", |
||||
|
}, |
||||
|
{ |
||||
|
input: "", |
||||
|
expect: "", |
||||
|
}, |
||||
|
{ |
||||
|
input: ":", |
||||
|
expect: "", |
||||
|
}, |
||||
|
{ |
||||
|
input: "[::]", |
||||
|
expect: "::", |
||||
|
}, |
||||
|
} |
||||
|
for i, tc := range testCases { |
||||
|
addr, err := ParseAddress(tc.input) |
||||
|
if err != nil { |
||||
|
t.Errorf("Test %d: Parsing address '%s': %v", i, tc.input, err) |
||||
|
continue |
||||
|
} |
||||
|
expect := tc.expect |
||||
|
if !caseSensitivePath { |
||||
|
// every other part of the address should be lowercased when normalized,
|
||||
|
// so simply lower-case the whole thing to do case-insensitive comparison
|
||||
|
// of the path as well
|
||||
|
expect = strings.ToLower(expect) |
||||
|
} |
||||
|
if actual := addr.Normalize().Key(); actual != expect { |
||||
|
t.Errorf("Test %d: Normalized key for address '%s' was '%s' but expected '%s'", i, tc.input, actual, expect) |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,255 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package httpcaddyfile |
||||
|
|
||||
|
import ( |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
"html" |
||||
|
"net/http" |
||||
|
"reflect" |
||||
|
|
||||
|
"github.com/caddyserver/caddy/caddyconfig" |
||||
|
"github.com/caddyserver/caddy/modules/caddyhttp" |
||||
|
"github.com/caddyserver/caddy/v2/modules/caddytls" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
RegisterDirective("bind", parseBind) |
||||
|
RegisterDirective("root", parseRoot) |
||||
|
RegisterDirective("tls", parseTLS) |
||||
|
RegisterHandlerDirective("redir", parseRedir) |
||||
|
} |
||||
|
|
||||
|
func parseBind(h Helper) ([]ConfigValue, error) { |
||||
|
var lnHosts []string |
||||
|
for h.Next() { |
||||
|
lnHosts = append(lnHosts, h.RemainingArgs()...) |
||||
|
} |
||||
|
return h.NewBindAddresses(lnHosts), nil |
||||
|
} |
||||
|
|
||||
|
func parseRoot(h Helper) ([]ConfigValue, error) { |
||||
|
if !h.Next() { |
||||
|
return nil, h.ArgErr() |
||||
|
} |
||||
|
|
||||
|
matcherSet, ok, err := h.MatcherToken() |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
if !ok { |
||||
|
// no matcher token; oops
|
||||
|
h.Dispenser.Prev() |
||||
|
} |
||||
|
|
||||
|
if !h.NextArg() { |
||||
|
return nil, h.ArgErr() |
||||
|
} |
||||
|
root := h.Val() |
||||
|
if h.NextArg() { |
||||
|
return nil, h.ArgErr() |
||||
|
} |
||||
|
|
||||
|
varsHandler := caddyhttp.VarsMiddleware{"root": root} |
||||
|
route := caddyhttp.Route{ |
||||
|
HandlersRaw: []json.RawMessage{ |
||||
|
caddyconfig.JSONModuleObject(varsHandler, "handler", "vars", nil), |
||||
|
}, |
||||
|
} |
||||
|
if matcherSet != nil { |
||||
|
route.MatcherSetsRaw = []map[string]json.RawMessage{matcherSet} |
||||
|
} |
||||
|
|
||||
|
return h.NewVarsRoute(route), nil |
||||
|
} |
||||
|
|
||||
|
func parseTLS(h Helper) ([]ConfigValue, error) { |
||||
|
var configVals []ConfigValue |
||||
|
|
||||
|
cp := new(caddytls.ConnectionPolicy) |
||||
|
var fileLoader caddytls.FileLoader |
||||
|
var folderLoader caddytls.FolderLoader |
||||
|
var mgr caddytls.ACMEManagerMaker |
||||
|
var off bool |
||||
|
|
||||
|
for h.Next() { |
||||
|
// file certificate loader
|
||||
|
firstLine := h.RemainingArgs() |
||||
|
switch len(firstLine) { |
||||
|
case 0: |
||||
|
case 1: |
||||
|
if firstLine[0] == "off" { |
||||
|
off = true |
||||
|
} else { |
||||
|
mgr.Email = firstLine[0] |
||||
|
} |
||||
|
case 2: |
||||
|
fileLoader = append(fileLoader, caddytls.CertKeyFilePair{ |
||||
|
Certificate: firstLine[0], |
||||
|
Key: firstLine[1], |
||||
|
// TODO: add tags, for enterprise module's certificate selection
|
||||
|
}) |
||||
|
default: |
||||
|
return nil, h.ArgErr() |
||||
|
} |
||||
|
|
||||
|
var hasBlock bool |
||||
|
for h.NextBlock() { |
||||
|
hasBlock = true |
||||
|
|
||||
|
switch h.Val() { |
||||
|
|
||||
|
// connection policy
|
||||
|
case "protocols": |
||||
|
args := h.RemainingArgs() |
||||
|
if len(args) == 0 { |
||||
|
return nil, h.SyntaxErr("one or two protocols") |
||||
|
} |
||||
|
if len(args) > 0 { |
||||
|
if _, ok := caddytls.SupportedProtocols[args[0]]; !ok { |
||||
|
return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[0]) |
||||
|
} |
||||
|
cp.ProtocolMin = args[0] |
||||
|
} |
||||
|
if len(args) > 1 { |
||||
|
if _, ok := caddytls.SupportedProtocols[args[1]]; !ok { |
||||
|
return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[1]) |
||||
|
} |
||||
|
cp.ProtocolMax = args[1] |
||||
|
} |
||||
|
case "ciphers": |
||||
|
for h.NextArg() { |
||||
|
if _, ok := caddytls.SupportedCipherSuites[h.Val()]; !ok { |
||||
|
return nil, h.Errf("Wrong cipher suite name or cipher suite not supported: '%s'", h.Val()) |
||||
|
} |
||||
|
cp.CipherSuites = append(cp.CipherSuites, h.Val()) |
||||
|
} |
||||
|
case "curves": |
||||
|
for h.NextArg() { |
||||
|
if _, ok := caddytls.SupportedCurves[h.Val()]; !ok { |
||||
|
return nil, h.Errf("Wrong curve name or curve not supported: '%s'", h.Val()) |
||||
|
} |
||||
|
cp.Curves = append(cp.Curves, h.Val()) |
||||
|
} |
||||
|
case "alpn": |
||||
|
args := h.RemainingArgs() |
||||
|
if len(args) == 0 { |
||||
|
return nil, h.ArgErr() |
||||
|
} |
||||
|
cp.ALPN = args |
||||
|
|
||||
|
// certificate folder loader
|
||||
|
case "load": |
||||
|
folderLoader = append(folderLoader, h.RemainingArgs()...) |
||||
|
|
||||
|
// automation policy
|
||||
|
case "ca": |
||||
|
arg := h.RemainingArgs() |
||||
|
if len(arg) != 1 { |
||||
|
return nil, h.ArgErr() |
||||
|
} |
||||
|
mgr.CA = arg[0] |
||||
|
|
||||
|
// TODO: other properties for automation manager
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// a naked tls directive is not allowed
|
||||
|
if len(firstLine) == 0 && !hasBlock { |
||||
|
return nil, h.ArgErr() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// connection policy
|
||||
|
configVals = append(configVals, ConfigValue{ |
||||
|
Class: "tls.connection_policy", |
||||
|
Value: cp, |
||||
|
}) |
||||
|
|
||||
|
// certificate loaders
|
||||
|
if len(fileLoader) > 0 { |
||||
|
configVals = append(configVals, ConfigValue{ |
||||
|
Class: "tls.certificate_loader", |
||||
|
Value: fileLoader, |
||||
|
}) |
||||
|
} |
||||
|
if len(folderLoader) > 0 { |
||||
|
configVals = append(configVals, ConfigValue{ |
||||
|
Class: "tls.certificate_loader", |
||||
|
Value: folderLoader, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// automation policy
|
||||
|
if off { |
||||
|
configVals = append(configVals, ConfigValue{ |
||||
|
Class: "tls.off", |
||||
|
Value: true, |
||||
|
}) |
||||
|
} else if !reflect.DeepEqual(mgr, caddytls.ACMEManagerMaker{}) { |
||||
|
configVals = append(configVals, ConfigValue{ |
||||
|
Class: "tls.automation_manager", |
||||
|
Value: mgr, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
return configVals, nil |
||||
|
} |
||||
|
|
||||
|
func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) { |
||||
|
if !h.Next() { |
||||
|
return nil, h.ArgErr() |
||||
|
} |
||||
|
|
||||
|
if !h.NextArg() { |
||||
|
return nil, h.ArgErr() |
||||
|
} |
||||
|
to := h.Val() |
||||
|
|
||||
|
var code string |
||||
|
if h.NextArg() { |
||||
|
code = h.Val() |
||||
|
} |
||||
|
if code == "permanent" { |
||||
|
code = "301" |
||||
|
} |
||||
|
if code == "temporary" || code == "" { |
||||
|
code = "307" |
||||
|
} |
||||
|
var body string |
||||
|
if code == "meta" { |
||||
|
// Script tag comes first since that will better imitate a redirect in the browser's
|
||||
|
// history, but the meta tag is a fallback for most non-JS clients.
|
||||
|
const metaRedir = `<!DOCTYPE html> |
||||
|
<html> |
||||
|
<head> |
||||
|
<title>Redirecting...</title> |
||||
|
<script>window.location.replace("%s");</script> |
||||
|
<meta http-equiv="refresh" content="0; URL='%s'"> |
||||
|
</head> |
||||
|
<body>Redirecting to <a href="%s">%s</a>...</body> |
||||
|
</html> |
||||
|
` |
||||
|
safeTo := html.EscapeString(to) |
||||
|
body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo) |
||||
|
} |
||||
|
|
||||
|
return caddyhttp.StaticResponse{ |
||||
|
StatusCode: caddyhttp.WeakString(code), |
||||
|
Headers: http.Header{"Location": []string{to}}, |
||||
|
Body: body, |
||||
|
}, nil |
||||
|
} |
||||
@ -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) |
||||
@ -0,0 +1,56 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package httpcaddyfile |
||||
|
|
||||
|
import ( |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
|
||||
|
"github.com/caddyserver/caddy/v2" |
||||
|
"github.com/caddyserver/caddy/v2/caddyconfig" |
||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" |
||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp" |
||||
|
) |
||||
|
|
||||
|
func (st *ServerType) parseMatcherDefinitions(d *caddyfile.Dispenser) (map[string]map[string]json.RawMessage, error) { |
||||
|
matchers := make(map[string]map[string]json.RawMessage) |
||||
|
for d.Next() { |
||||
|
definitionName := d.Val() |
||||
|
for d.NextBlock() { |
||||
|
matcherName := d.Val() |
||||
|
mod, err := caddy.GetModule("http.matchers." + matcherName) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("getting matcher module '%s': %v", matcherName, err) |
||||
|
} |
||||
|
unm, ok := mod.New().(caddyfile.Unmarshaler) |
||||
|
if !ok { |
||||
|
return nil, fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName) |
||||
|
} |
||||
|
err = unm.UnmarshalCaddyfile(d.NewFromNextTokens()) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
rm, ok := unm.(caddyhttp.RequestMatcher) |
||||
|
if !ok { |
||||
|
return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName) |
||||
|
} |
||||
|
if _, ok := matchers[definitionName]; !ok { |
||||
|
matchers[definitionName] = make(map[string]json.RawMessage) |
||||
|
} |
||||
|
matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil) |
||||
|
} |
||||
|
} |
||||
|
return matchers, nil |
||||
|
} |
||||
@ -0,0 +1,519 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package httpcaddyfile |
||||
|
|
||||
|
import ( |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
"reflect" |
||||
|
"sort" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
|
||||
|
"github.com/caddyserver/caddy/v2" |
||||
|
"github.com/caddyserver/caddy/v2/caddyconfig" |
||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" |
||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp" |
||||
|
"github.com/caddyserver/caddy/v2/modules/caddytls" |
||||
|
"github.com/mholt/certmagic" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
caddyconfig.RegisterAdapter("caddyfile", caddyfile.Adapter{ServerType: ServerType{}}) |
||||
|
} |
||||
|
|
||||
|
// ServerType can set up a config from an HTTP Caddyfile.
|
||||
|
type ServerType struct { |
||||
|
} |
||||
|
|
||||
|
// Setup makes a config from the tokens.
|
||||
|
func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock, |
||||
|
options map[string]string) (*caddy.Config, []caddyconfig.Warning, error) { |
||||
|
var warnings []caddyconfig.Warning |
||||
|
|
||||
|
var serverBlocks []serverBlock |
||||
|
for _, sblock := range originalServerBlocks { |
||||
|
serverBlocks = append(serverBlocks, serverBlock{ |
||||
|
block: sblock, |
||||
|
pile: make(map[string][]ConfigValue), |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
for _, sb := range serverBlocks { |
||||
|
// replace shorthand placeholders (which are
|
||||
|
// convenient when writing a Caddyfile) with
|
||||
|
// their actual placeholder identifiers or
|
||||
|
// variable names
|
||||
|
replacer := strings.NewReplacer( |
||||
|
"{uri}", "{http.request.uri}", |
||||
|
"{path}", "{http.request.uri.path}", |
||||
|
"{host}", "{http.request.host}", |
||||
|
"{hostport}", "{http.request.hostport}", |
||||
|
"{method}", "{http.request.method}", |
||||
|
"{scheme}", "{http.request.scheme}", |
||||
|
"{file}", "{http.request.uri.path.file}", |
||||
|
"{dir}", "{http.request.uri.path.dir}", |
||||
|
"{query}", "{http.request.uri.query}", |
||||
|
) |
||||
|
for _, segment := range sb.block.Segments { |
||||
|
for i := 0; i < len(segment); i++ { |
||||
|
segment[i].Text = replacer.Replace(segment[i].Text) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// extract matcher definitions
|
||||
|
d := sb.block.DispenseDirective("matcher") |
||||
|
matcherDefs, err := st.parseMatcherDefinitions(d) |
||||
|
if err != nil { |
||||
|
return nil, nil, err |
||||
|
} |
||||
|
|
||||
|
for _, segment := range sb.block.Segments { |
||||
|
dir := segment.Directive() |
||||
|
if dir == "matcher" { |
||||
|
// TODO: This is a special case because we pre-processed it; handle this better
|
||||
|
continue |
||||
|
} |
||||
|
if dirFunc, ok := registeredDirectives[dir]; ok { |
||||
|
results, err := dirFunc(Helper{ |
||||
|
Dispenser: segment.NewDispenser(), |
||||
|
warnings: &warnings, |
||||
|
matcherDefs: matcherDefs, |
||||
|
}) |
||||
|
if err != nil { |
||||
|
return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err) |
||||
|
} |
||||
|
for _, result := range results { |
||||
|
result.directive = dir |
||||
|
sb.pile[result.Class] = append(sb.pile[result.Class], result) |
||||
|
} |
||||
|
} else { |
||||
|
tkn := segment[0] |
||||
|
return nil, warnings, fmt.Errorf("%s:%d: unrecognized directive: %s", tkn.File, tkn.Line, dir) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// map
|
||||
|
sbmap, err := st.mapAddressToServerBlocks(serverBlocks) |
||||
|
if err != nil { |
||||
|
return nil, warnings, err |
||||
|
} |
||||
|
|
||||
|
// reduce
|
||||
|
pairings := st.consolidateAddrMappings(sbmap) |
||||
|
|
||||
|
// each pairing of listener addresses to list of server
|
||||
|
// blocks is basically a server definition
|
||||
|
servers, err := st.serversFromPairings(pairings, &warnings) |
||||
|
if err != nil { |
||||
|
return nil, warnings, err |
||||
|
} |
||||
|
|
||||
|
// now that each server is configured, make the HTTP app
|
||||
|
httpApp := caddyhttp.App{ |
||||
|
HTTPPort: tryInt(options["http-port"], &warnings), |
||||
|
HTTPSPort: tryInt(options["https-port"], &warnings), |
||||
|
Servers: servers, |
||||
|
} |
||||
|
|
||||
|
// now for the TLS app! (TODO: refactor into own func)
|
||||
|
tlsApp := caddytls.TLS{Certificates: make(map[string]json.RawMessage)} |
||||
|
for _, p := range pairings { |
||||
|
for _, sblock := range p.serverBlocks { |
||||
|
// tls automation policies
|
||||
|
if mmVals, ok := sblock.pile["tls.automation_manager"]; ok { |
||||
|
for _, mmVal := range mmVals { |
||||
|
mm := mmVal.Value.(caddytls.ManagerMaker) |
||||
|
sblockHosts, err := st.autoHTTPSHosts(sblock) |
||||
|
if err != nil { |
||||
|
return nil, warnings, err |
||||
|
} |
||||
|
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, caddytls.AutomationPolicy{ |
||||
|
Hosts: sblockHosts, |
||||
|
ManagementRaw: caddyconfig.JSONModuleObject(mm, "module", mm.(caddy.Module).CaddyModule().ID(), &warnings), |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// tls certificate loaders
|
||||
|
if clVals, ok := sblock.pile["tls.certificate_loader"]; ok { |
||||
|
for _, clVal := range clVals { |
||||
|
loader := clVal.Value.(caddytls.CertificateLoader) |
||||
|
loaderName := caddy.GetModuleName(loader) |
||||
|
tlsApp.Certificates[loaderName] = caddyconfig.JSON(loader, &warnings) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
// consolidate automation policies that are the exact same
|
||||
|
tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies) |
||||
|
|
||||
|
// annnd the top-level config, then we're done!
|
||||
|
cfg := &caddy.Config{AppsRaw: make(map[string]json.RawMessage)} |
||||
|
if !reflect.DeepEqual(httpApp, caddyhttp.App{}) { |
||||
|
cfg.AppsRaw["http"] = caddyconfig.JSON(httpApp, &warnings) |
||||
|
} |
||||
|
if !reflect.DeepEqual(tlsApp, caddytls.TLS{}) { |
||||
|
cfg.AppsRaw["tls"] = caddyconfig.JSON(tlsApp, &warnings) |
||||
|
} |
||||
|
|
||||
|
return cfg, warnings, nil |
||||
|
} |
||||
|
|
||||
|
// hostsFromServerBlockKeys returns a list of all the
|
||||
|
// hostnames found in the keys of the server block sb.
|
||||
|
// The list may not be in a consistent order.
|
||||
|
func (st *ServerType) hostsFromServerBlockKeys(sb caddyfile.ServerBlock) ([]string, error) { |
||||
|
// first get each unique hostname
|
||||
|
hostMap := make(map[string]struct{}) |
||||
|
for _, sblockKey := range sb.Keys { |
||||
|
addr, err := ParseAddress(sblockKey) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("parsing server block key: %v", err) |
||||
|
} |
||||
|
addr = addr.Normalize() |
||||
|
hostMap[addr.Host] = struct{}{} |
||||
|
} |
||||
|
|
||||
|
// convert map to slice
|
||||
|
sblockHosts := make([]string, 0, len(hostMap)) |
||||
|
for host := range hostMap { |
||||
|
sblockHosts = append(sblockHosts, host) |
||||
|
} |
||||
|
|
||||
|
return sblockHosts, nil |
||||
|
} |
||||
|
|
||||
|
// serversFromPairings creates the servers for each pairing of addresses
|
||||
|
// to server blocks. Each pairing is essentially a server definition.
|
||||
|
func (st *ServerType) serversFromPairings(pairings []sbAddrAssociation, warnings *[]caddyconfig.Warning) (map[string]*caddyhttp.Server, error) { |
||||
|
servers := make(map[string]*caddyhttp.Server) |
||||
|
|
||||
|
for i, p := range pairings { |
||||
|
srv := &caddyhttp.Server{ |
||||
|
Listen: p.addresses, |
||||
|
} |
||||
|
|
||||
|
for _, sblock := range p.serverBlocks { |
||||
|
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock.block) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("server block %v: compiling matcher sets: %v", sblock.block.Keys, err) |
||||
|
} |
||||
|
|
||||
|
// if there are user-defined variables, then siteVarSubroute will
|
||||
|
// wrap the handlerSubroute; otherwise handlerSubroute will be the
|
||||
|
// site's primary subroute.
|
||||
|
siteVarSubroute, handlerSubroute := new(caddyhttp.Subroute), new(caddyhttp.Subroute) |
||||
|
|
||||
|
// tls: connection policies and toggle auto HTTPS
|
||||
|
|
||||
|
autoHTTPSQualifiedHosts, err := st.autoHTTPSHosts(sblock) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
if _, ok := sblock.pile["tls.off"]; ok { |
||||
|
// tls off: disable TLS (and automatic HTTPS) for server block's names
|
||||
|
if srv.AutoHTTPS == nil { |
||||
|
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig) |
||||
|
} |
||||
|
srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, autoHTTPSQualifiedHosts...) |
||||
|
} else if cpVals, ok := sblock.pile["tls.connection_policy"]; ok { |
||||
|
// tls connection policies
|
||||
|
for _, cpVal := range cpVals { |
||||
|
cp := cpVal.Value.(*caddytls.ConnectionPolicy) |
||||
|
// only create if there is a non-empty policy
|
||||
|
if !reflect.DeepEqual(cp, new(caddytls.ConnectionPolicy)) { |
||||
|
// make sure the policy covers all hostnames from the block
|
||||
|
hosts, err := st.hostsFromServerBlockKeys(sblock.block) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
// TODO: are matchers needed if every hostname of the config is matched?
|
||||
|
cp.Matchers = map[string]json.RawMessage{ |
||||
|
"sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones
|
||||
|
} |
||||
|
srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp) |
||||
|
} |
||||
|
} |
||||
|
// TODO: consolidate equal conn policies
|
||||
|
} |
||||
|
|
||||
|
// vars: special routes that will have to wrap the normal handlers
|
||||
|
// so that these variables can be used across their matchers too
|
||||
|
for _, cfgVal := range sblock.pile["var"] { |
||||
|
siteVarSubroute.Routes = append(siteVarSubroute.Routes, cfgVal.Value.(caddyhttp.Route)) |
||||
|
} |
||||
|
|
||||
|
// set up each handler directive
|
||||
|
dirRoutes := sblock.pile["route"] |
||||
|
// TODO: The ordering here depends on... if there is a list of
|
||||
|
// directives to use, then sort by that, otherwise just use in
|
||||
|
// the order they appear in the slice (which is the order they
|
||||
|
// appeared in the Caddyfile)
|
||||
|
sortByList := true |
||||
|
if sortByList { |
||||
|
dirPositions := make(map[string]int) |
||||
|
for i, dir := range defaultDirectiveOrder { |
||||
|
dirPositions[dir] = i |
||||
|
} |
||||
|
sort.SliceStable(dirRoutes, func(i, j int) bool { |
||||
|
iDir, jDir := dirRoutes[i].directive, dirRoutes[j].directive |
||||
|
return dirPositions[iDir] < dirPositions[jDir] |
||||
|
}) |
||||
|
} |
||||
|
for _, r := range dirRoutes { |
||||
|
handlerSubroute.Routes = append(handlerSubroute.Routes, r.Value.(caddyhttp.Route)) |
||||
|
} |
||||
|
|
||||
|
// the route that contains the site's handlers will
|
||||
|
// be assumed to be the sub-route for this site...
|
||||
|
siteSubroute := handlerSubroute |
||||
|
|
||||
|
// ... unless, of course, there are variables that might
|
||||
|
// be used by the site's matchers or handlers, in which
|
||||
|
// case we need to nest the handlers in a sub-sub-route,
|
||||
|
// and the variables go in the sub-route so the variables
|
||||
|
// get evaluated first
|
||||
|
if len(siteVarSubroute.Routes) > 0 { |
||||
|
subSubRoute := caddyhttp.Subroute{Routes: siteSubroute.Routes} |
||||
|
siteSubroute.Routes = append( |
||||
|
siteVarSubroute.Routes, |
||||
|
caddyhttp.Route{ |
||||
|
HandlersRaw: []json.RawMessage{ |
||||
|
caddyconfig.JSONModuleObject(subSubRoute, "handler", "subroute", warnings), |
||||
|
}, |
||||
|
}, |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
siteSubroute.Routes = consolidateRoutes(siteSubroute.Routes) |
||||
|
|
||||
|
srv.Routes = append(srv.Routes, caddyhttp.Route{ |
||||
|
MatcherSetsRaw: matcherSetsEnc, |
||||
|
HandlersRaw: []json.RawMessage{ |
||||
|
caddyconfig.JSONModuleObject(siteSubroute, "handler", "subroute", warnings), |
||||
|
}, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
srv.Routes = consolidateRoutes(srv.Routes) |
||||
|
|
||||
|
servers[fmt.Sprintf("srv%d", i)] = srv |
||||
|
} |
||||
|
|
||||
|
return servers, nil |
||||
|
} |
||||
|
|
||||
|
func (st ServerType) autoHTTPSHosts(sb serverBlock) ([]string, error) { |
||||
|
// get the hosts for this server block...
|
||||
|
hosts, err := st.hostsFromServerBlockKeys(sb.block) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
// ...and of those, which ones qualify for auto HTTPS
|
||||
|
var autoHTTPSQualifiedHosts []string |
||||
|
for _, h := range hosts { |
||||
|
if certmagic.HostQualifies(h) { |
||||
|
autoHTTPSQualifiedHosts = append(autoHTTPSQualifiedHosts, h) |
||||
|
} |
||||
|
} |
||||
|
return autoHTTPSQualifiedHosts, nil |
||||
|
} |
||||
|
|
||||
|
// consolidateRoutes combines routes with the same properties
|
||||
|
// (same matchers, same Terminal and Group settings) for a
|
||||
|
// cleaner overall output.
|
||||
|
func consolidateRoutes(routes caddyhttp.RouteList) caddyhttp.RouteList { |
||||
|
for i := 0; i < len(routes)-1; i++ { |
||||
|
if reflect.DeepEqual(routes[i].MatcherSetsRaw, routes[i+1].MatcherSetsRaw) && |
||||
|
routes[i].Terminal == routes[i+1].Terminal && |
||||
|
routes[i].Group == routes[i+1].Group { |
||||
|
// keep the handlers in the same order, then splice out repetitive route
|
||||
|
routes[i].HandlersRaw = append(routes[i].HandlersRaw, routes[i+1].HandlersRaw...) |
||||
|
routes = append(routes[:i+1], routes[i+2:]...) |
||||
|
i-- |
||||
|
} |
||||
|
} |
||||
|
return routes |
||||
|
} |
||||
|
|
||||
|
// consolidateAutomationPolicies combines automation policies that are the same,
|
||||
|
// for a cleaner overall output.
|
||||
|
func consolidateAutomationPolicies(aps []caddytls.AutomationPolicy) []caddytls.AutomationPolicy { |
||||
|
for i := 0; i < len(aps); i++ { |
||||
|
for j := 0; j < len(aps); j++ { |
||||
|
if j == i { |
||||
|
continue |
||||
|
} |
||||
|
if reflect.DeepEqual(aps[i].ManagementRaw, aps[j].ManagementRaw) { |
||||
|
aps[i].Hosts = append(aps[i].Hosts, aps[j].Hosts...) |
||||
|
} |
||||
|
aps = append(aps[:j], aps[j+1:]...) |
||||
|
i-- |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
return aps |
||||
|
} |
||||
|
|
||||
|
func matcherSetFromMatcherToken( |
||||
|
tkn caddyfile.Token, |
||||
|
matcherDefs map[string]map[string]json.RawMessage, |
||||
|
warnings *[]caddyconfig.Warning, |
||||
|
) (map[string]json.RawMessage, bool, error) { |
||||
|
// matcher tokens can be wildcards, simple path matchers,
|
||||
|
// or refer to a pre-defined matcher by some name
|
||||
|
if tkn.Text == "*" { |
||||
|
// match all requests == no matchers, so nothing to do
|
||||
|
return nil, true, nil |
||||
|
} else if strings.HasPrefix(tkn.Text, "/") { |
||||
|
// convenient way to specify a single path match
|
||||
|
return map[string]json.RawMessage{ |
||||
|
"path": caddyconfig.JSON(caddyhttp.MatchPath{tkn.Text}, warnings), |
||||
|
}, true, nil |
||||
|
} else if strings.HasPrefix(tkn.Text, "match:") { |
||||
|
// pre-defined matcher
|
||||
|
matcherName := strings.TrimPrefix(tkn.Text, "match:") |
||||
|
m, ok := matcherDefs[matcherName] |
||||
|
if !ok { |
||||
|
return nil, false, fmt.Errorf("unrecognized matcher name: %+v", matcherName) |
||||
|
} |
||||
|
return m, true, nil |
||||
|
} |
||||
|
|
||||
|
return nil, false, nil |
||||
|
} |
||||
|
|
||||
|
func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([]map[string]json.RawMessage, error) { |
||||
|
type hostPathPair struct { |
||||
|
hostm caddyhttp.MatchHost |
||||
|
pathm caddyhttp.MatchPath |
||||
|
} |
||||
|
|
||||
|
// keep routes with common host and path matchers together
|
||||
|
var matcherPairs []*hostPathPair |
||||
|
|
||||
|
for _, key := range sblock.Keys { |
||||
|
addr, err := ParseAddress(key) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("server block %v: parsing and standardizing address '%s': %v", sblock.Keys, key, err) |
||||
|
} |
||||
|
addr = addr.Normalize() |
||||
|
|
||||
|
// choose a matcher pair that should be shared by this
|
||||
|
// server block; if none exists yet, create one
|
||||
|
var chosenMatcherPair *hostPathPair |
||||
|
for _, mp := range matcherPairs { |
||||
|
if (len(mp.pathm) == 0 && addr.Path == "") || |
||||
|
(len(mp.pathm) == 1 && mp.pathm[0] == addr.Path) { |
||||
|
chosenMatcherPair = mp |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
if chosenMatcherPair == nil { |
||||
|
chosenMatcherPair = new(hostPathPair) |
||||
|
if addr.Path != "" { |
||||
|
chosenMatcherPair.pathm = []string{addr.Path} |
||||
|
} |
||||
|
matcherPairs = append(matcherPairs, chosenMatcherPair) |
||||
|
} |
||||
|
|
||||
|
// add this server block's keys to the matcher
|
||||
|
// pair if it doesn't already exist
|
||||
|
if addr.Host != "" { |
||||
|
var found bool |
||||
|
for _, h := range chosenMatcherPair.hostm { |
||||
|
if h == addr.Host { |
||||
|
found = true |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
if !found { |
||||
|
chosenMatcherPair.hostm = append(chosenMatcherPair.hostm, addr.Host) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// iterate each pairing of host and path matchers and
|
||||
|
// put them into a map for JSON encoding
|
||||
|
var matcherSets []map[string]caddyhttp.RequestMatcher |
||||
|
for _, mp := range matcherPairs { |
||||
|
matcherSet := make(map[string]caddyhttp.RequestMatcher) |
||||
|
if len(mp.hostm) > 0 { |
||||
|
matcherSet["host"] = mp.hostm |
||||
|
} |
||||
|
if len(mp.pathm) > 0 { |
||||
|
matcherSet["path"] = mp.pathm |
||||
|
} |
||||
|
if len(matcherSet) > 0 { |
||||
|
matcherSets = append(matcherSets, matcherSet) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// finally, encode each of the matcher sets
|
||||
|
var matcherSetsEnc []map[string]json.RawMessage |
||||
|
for _, ms := range matcherSets { |
||||
|
msEncoded, err := encodeMatcherSet(ms) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("server block %v: %v", sblock.Keys, err) |
||||
|
} |
||||
|
matcherSetsEnc = append(matcherSetsEnc, msEncoded) |
||||
|
} |
||||
|
|
||||
|
return matcherSetsEnc, nil |
||||
|
} |
||||
|
|
||||
|
func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (map[string]json.RawMessage, error) { |
||||
|
msEncoded := make(map[string]json.RawMessage) |
||||
|
for matcherName, val := range matchers { |
||||
|
jsonBytes, err := json.Marshal(val) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("marshaling matcher set %#v: %v", matchers, err) |
||||
|
} |
||||
|
msEncoded[matcherName] = jsonBytes |
||||
|
} |
||||
|
return msEncoded, nil |
||||
|
} |
||||
|
|
||||
|
// tryInt tries to convert str to an integer. If it fails, it downgrades
|
||||
|
// the error to a warning and returns 0.
|
||||
|
func tryInt(str string, warnings *[]caddyconfig.Warning) int { |
||||
|
if str == "" { |
||||
|
return 0 |
||||
|
} |
||||
|
val, err := strconv.Atoi(str) |
||||
|
if err != nil && warnings != nil { |
||||
|
*warnings = append(*warnings, caddyconfig.Warning{Message: err.Error()}) |
||||
|
} |
||||
|
return val |
||||
|
} |
||||
|
|
||||
|
type matcherSetAndTokens struct { |
||||
|
matcherSet map[string]json.RawMessage |
||||
|
tokens []caddyfile.Token |
||||
|
} |
||||
|
|
||||
|
// sbAddrAssocation is a mapping from a list of
|
||||
|
// addresses to a list of server blocks that are
|
||||
|
// served on those addresses.
|
||||
|
type sbAddrAssociation struct { |
||||
|
addresses []string |
||||
|
serverBlocks []serverBlock |
||||
|
} |
||||
|
|
||||
|
// Interface guard
|
||||
|
var _ caddyfile.ServerType = (*ServerType)(nil) |
||||
@ -0,0 +1,99 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package encode |
||||
|
|
||||
|
import ( |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
|
||||
|
"github.com/caddyserver/caddy/v2" |
||||
|
"github.com/caddyserver/caddy/v2/caddyconfig" |
||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" |
||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" |
||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
httpcaddyfile.RegisterHandlerDirective("encode", parseCaddyfile) |
||||
|
} |
||||
|
|
||||
|
// TODO: This is a good example of why UnmarshalCaddyfile is still a good idea... hmm.
|
||||
|
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { |
||||
|
enc := new(Encode) |
||||
|
err := enc.UnmarshalCaddyfile(h.Dispenser) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
return enc, nil |
||||
|
} |
||||
|
|
||||
|
// TODO: Keep UnmarshalCaddyfile pattern?
|
||||
|
|
||||
|
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
|
||||
|
//
|
||||
|
// encode [<matcher>] <formats...> {
|
||||
|
// gzip [<level>]
|
||||
|
// zstd
|
||||
|
// brotli [<quality>]
|
||||
|
// }
|
||||
|
//
|
||||
|
// Specifying the formats on the first line will use those formats' defaults.
|
||||
|
func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { |
||||
|
for d.Next() { |
||||
|
for _, arg := range d.RemainingArgs() { |
||||
|
mod, err := caddy.GetModule("http.encoders." + arg) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("finding encoder module '%s': %v", mod.Name, err) |
||||
|
} |
||||
|
encoding, ok := mod.New().(Encoding) |
||||
|
if !ok { |
||||
|
return fmt.Errorf("module %s is not an HTTP encoding", mod.Name) |
||||
|
} |
||||
|
if enc.EncodingsRaw == nil { |
||||
|
enc.EncodingsRaw = make(map[string]json.RawMessage) |
||||
|
} |
||||
|
enc.EncodingsRaw[arg] = caddyconfig.JSON(encoding, nil) |
||||
|
} |
||||
|
|
||||
|
for d.NextBlock() { |
||||
|
name := d.Val() |
||||
|
mod, err := caddy.GetModule("http.encoders." + name) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("getting encoder module '%s': %v", mod.Name, err) |
||||
|
} |
||||
|
unm, ok := mod.New().(caddyfile.Unmarshaler) |
||||
|
if !ok { |
||||
|
return fmt.Errorf("encoder module '%s' is not a Caddyfile unmarshaler", mod.Name) |
||||
|
} |
||||
|
err = unm.UnmarshalCaddyfile(d.NewFromNextTokens()) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
encoding, ok := unm.(Encoding) |
||||
|
if !ok { |
||||
|
return fmt.Errorf("module %s is not an HTTP encoding", mod.Name) |
||||
|
} |
||||
|
if enc.EncodingsRaw == nil { |
||||
|
enc.EncodingsRaw = make(map[string]json.RawMessage) |
||||
|
} |
||||
|
enc.EncodingsRaw[name] = caddyconfig.JSON(encoding, nil) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Interface guard
|
||||
|
var _ caddyfile.Unmarshaler = (*Encode)(nil) |
||||
@ -0,0 +1,104 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package fileserver |
||||
|
|
||||
|
import ( |
||||
|
"encoding/json" |
||||
|
|
||||
|
"github.com/caddyserver/caddy/modules/caddyhttp/rewrite" |
||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" |
||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
httpcaddyfile.RegisterHandlerDirective("file_server", parseCaddyfile) |
||||
|
httpcaddyfile.RegisterDirective("try_files", parseTryFiles) |
||||
|
} |
||||
|
|
||||
|
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { |
||||
|
var fsrv FileServer |
||||
|
|
||||
|
for h.Next() { |
||||
|
args := h.RemainingArgs() |
||||
|
switch len(args) { |
||||
|
case 0: |
||||
|
case 1: |
||||
|
if args[0] != "browse" { |
||||
|
return nil, h.ArgErr() |
||||
|
} |
||||
|
fsrv.Browse = new(Browse) |
||||
|
default: |
||||
|
return nil, h.ArgErr() |
||||
|
} |
||||
|
|
||||
|
for h.NextBlock() { |
||||
|
switch h.Val() { |
||||
|
case "hide": |
||||
|
fsrv.Hide = h.RemainingArgs() |
||||
|
if len(fsrv.Hide) == 0 { |
||||
|
return nil, h.ArgErr() |
||||
|
} |
||||
|
case "index": |
||||
|
fsrv.IndexNames = h.RemainingArgs() |
||||
|
if len(fsrv.Hide) == 0 { |
||||
|
return nil, h.ArgErr() |
||||
|
} |
||||
|
case "root": |
||||
|
if !h.Args(&fsrv.Root) { |
||||
|
return nil, h.ArgErr() |
||||
|
} |
||||
|
case "browse": |
||||
|
if fsrv.Browse != nil { |
||||
|
return nil, h.Err("browsing is already configured") |
||||
|
} |
||||
|
fsrv.Browse = new(Browse) |
||||
|
h.Args(&fsrv.Browse.TemplateFile) |
||||
|
default: |
||||
|
return nil, h.Errf("unknown subdirective '%s'", h.Val()) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// if no root was configured explicitly, use site root
|
||||
|
if fsrv.Root == "" { |
||||
|
fsrv.Root = "{http.var.root}" |
||||
|
} |
||||
|
|
||||
|
return &fsrv, nil |
||||
|
} |
||||
|
|
||||
|
func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { |
||||
|
if !h.Next() { |
||||
|
return nil, h.ArgErr() |
||||
|
} |
||||
|
|
||||
|
try := h.RemainingArgs() |
||||
|
if len(try) == 0 { |
||||
|
return nil, h.ArgErr() |
||||
|
} |
||||
|
|
||||
|
handler := rewrite.Rewrite{ |
||||
|
URI: "{http.matchers.file.relative}{http.request.uri.query}", |
||||
|
} |
||||
|
|
||||
|
matcherSet := map[string]json.RawMessage{ |
||||
|
"file": h.JSON(MatchFile{ |
||||
|
Root: "{http.var.root}", |
||||
|
TryFiles: try, |
||||
|
}, nil), |
||||
|
} |
||||
|
|
||||
|
return h.NewRoute(matcherSet, handler), nil |
||||
|
} |
||||
@ -0,0 +1,91 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package headers |
||||
|
|
||||
|
import ( |
||||
|
"net/http" |
||||
|
"strings" |
||||
|
|
||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" |
||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
httpcaddyfile.RegisterHandlerDirective("headers", parseCaddyfile) |
||||
|
} |
||||
|
|
||||
|
// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax:
|
||||
|
//
|
||||
|
// headers [<matcher>] [[+|-]<field> <value>] {
|
||||
|
// [+][<field>] [<value>]
|
||||
|
// [-<field>]
|
||||
|
// }
|
||||
|
//
|
||||
|
// Either a block can be opened or a single header field can be configured
|
||||
|
// in the first line, but not both in the same directive.
|
||||
|
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { |
||||
|
hdr := new(Headers) |
||||
|
for h.Next() { |
||||
|
// first see if headers are in the initial line
|
||||
|
var hasArgs bool |
||||
|
if h.NextArg() { |
||||
|
hasArgs = true |
||||
|
field := h.Val() |
||||
|
h.NextArg() |
||||
|
value := h.Val() |
||||
|
processCaddyfileLine(hdr, field, value) |
||||
|
} |
||||
|
|
||||
|
// if not, they should be in a block
|
||||
|
for h.NextBlock() { |
||||
|
if hasArgs { |
||||
|
return nil, h.Err("cannot specify headers in both arguments and block") |
||||
|
} |
||||
|
field := h.Val() |
||||
|
var value string |
||||
|
if h.NextArg() { |
||||
|
value = h.Val() |
||||
|
} |
||||
|
processCaddyfileLine(hdr, field, value) |
||||
|
} |
||||
|
} |
||||
|
return hdr, nil |
||||
|
} |
||||
|
|
||||
|
func processCaddyfileLine(hdr *Headers, field, value string) { |
||||
|
if strings.HasPrefix(field, "+") { |
||||
|
if hdr.Response == nil { |
||||
|
hdr.Response = &RespHeaderOps{HeaderOps: new(HeaderOps)} |
||||
|
} |
||||
|
if hdr.Response.Add == nil { |
||||
|
hdr.Response.Add = make(http.Header) |
||||
|
} |
||||
|
hdr.Response.Add.Set(field[1:], value) |
||||
|
} else if strings.HasPrefix(field, "-") { |
||||
|
if hdr.Response == nil { |
||||
|
hdr.Response = &RespHeaderOps{HeaderOps: new(HeaderOps)} |
||||
|
} |
||||
|
hdr.Response.Delete = append(hdr.Response.Delete, field[1:]) |
||||
|
hdr.Response.Deferred = true |
||||
|
} else { |
||||
|
if hdr.Response == nil { |
||||
|
hdr.Response = &RespHeaderOps{HeaderOps: new(HeaderOps)} |
||||
|
} |
||||
|
if hdr.Response.Set == nil { |
||||
|
hdr.Response.Set = make(http.Header) |
||||
|
} |
||||
|
hdr.Response.Set.Set(field, value) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package rewrite |
||||
|
|
||||
|
import ( |
||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" |
||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
httpcaddyfile.RegisterHandlerDirective("rewrite", parseCaddyfile) |
||||
|
} |
||||
|
|
||||
|
// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax:
|
||||
|
//
|
||||
|
// rewrite [<matcher>] <to>
|
||||
|
//
|
||||
|
// The <to> parameter becomes the new URI.
|
||||
|
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { |
||||
|
var rewr Rewrite |
||||
|
for h.Next() { |
||||
|
rewr.URI = h.Val() |
||||
|
} |
||||
|
return rewr, nil |
||||
|
} |
||||
@ -1,55 +0,0 @@ |
|||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||
//
|
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||
// you may not use this file except in compliance with the License.
|
|
||||
// You may obtain a copy of the License at
|
|
||||
//
|
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||
//
|
|
||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||
// See the License for the specific language governing permissions and
|
|
||||
// limitations under the License.
|
|
||||
|
|
||||
package caddyhttp |
|
||||
|
|
||||
import ( |
|
||||
"net/http" |
|
||||
|
|
||||
"github.com/caddyserver/caddy/v2" |
|
||||
) |
|
||||
|
|
||||
func init() { |
|
||||
caddy.RegisterModule(caddy.Module{ |
|
||||
Name: "http.handlers.table", |
|
||||
New: func() interface{} { return new(tableMiddleware) }, |
|
||||
}) |
|
||||
|
|
||||
caddy.RegisterModule(caddy.Module{ |
|
||||
Name: "http.matchers.table", |
|
||||
New: func() interface{} { return new(tableMatcher) }, |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
type tableMiddleware struct { |
|
||||
} |
|
||||
|
|
||||
func (t tableMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error { |
|
||||
// tbl := r.Context().Value(TableCtxKey).(map[string]interface{})
|
|
||||
|
|
||||
// TODO: implement this...
|
|
||||
|
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
type tableMatcher struct { |
|
||||
} |
|
||||
|
|
||||
func (m tableMatcher) Match(r *http.Request) bool { |
|
||||
return false // TODO: implement
|
|
||||
} |
|
||||
|
|
||||
// Interface guards
|
|
||||
var _ MiddlewareHandler = (*tableMiddleware)(nil) |
|
||||
var _ RequestMatcher = (*tableMatcher)(nil) |
|
||||
@ -0,0 +1,62 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package templates |
||||
|
|
||||
|
import ( |
||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" |
||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
httpcaddyfile.RegisterHandlerDirective("templates", parseCaddyfile) |
||||
|
} |
||||
|
|
||||
|
// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax:
|
||||
|
//
|
||||
|
// templates [<matcher>] {
|
||||
|
// mime <types...>
|
||||
|
// between <open_delim> <close_delim>
|
||||
|
// root <path>
|
||||
|
// }
|
||||
|
//
|
||||
|
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { |
||||
|
t := new(Templates) |
||||
|
for h.Next() { |
||||
|
for h.NextBlock() { |
||||
|
switch h.Val() { |
||||
|
case "mime": |
||||
|
t.MIMETypes = h.RemainingArgs() |
||||
|
if len(t.MIMETypes) == 0 { |
||||
|
return nil, h.ArgErr() |
||||
|
} |
||||
|
case "between": |
||||
|
t.Delimiters = h.RemainingArgs() |
||||
|
if len(t.Delimiters) != 2 { |
||||
|
return nil, h.ArgErr() |
||||
|
} |
||||
|
case "root": |
||||
|
if !h.Args(&t.IncludeRoot) { |
||||
|
return nil, h.ArgErr() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if t.IncludeRoot == "" { |
||||
|
t.IncludeRoot = "{http.var.root}" |
||||
|
} |
||||
|
|
||||
|
return t, nil |
||||
|
} |
||||
@ -0,0 +1,81 @@ |
|||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
|
//
|
||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
// you may not use this file except in compliance with the License.
|
||||
|
// You may obtain a copy of the License at
|
||||
|
//
|
||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
//
|
||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
// See the License for the specific language governing permissions and
|
||||
|
// limitations under the License.
|
||||
|
|
||||
|
package caddyhttp |
||||
|
|
||||
|
import ( |
||||
|
"net/http" |
||||
|
|
||||
|
"github.com/caddyserver/caddy/v2" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
caddy.RegisterModule(VarsMiddleware{}) |
||||
|
caddy.RegisterModule(VarsMatcher{}) |
||||
|
} |
||||
|
|
||||
|
// VarsMiddleware is an HTTP middleware which sets variables
|
||||
|
// in the context, mainly for use by placeholders.
|
||||
|
type VarsMiddleware map[string]string |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (VarsMiddleware) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "http.handlers.vars", |
||||
|
New: func() caddy.Module { return new(VarsMiddleware) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (t VarsMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error { |
||||
|
vars := r.Context().Value(VarCtxKey).(map[string]interface{}) |
||||
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) |
||||
|
for k, v := range t { |
||||
|
keyExpanded := repl.ReplaceAll(k, "") |
||||
|
valExpanded := repl.ReplaceAll(v, "") |
||||
|
vars[keyExpanded] = valExpanded |
||||
|
} |
||||
|
return next.ServeHTTP(w, r) |
||||
|
} |
||||
|
|
||||
|
// VarsMatcher is an HTTP request matcher which can match
|
||||
|
// requests based on variables in the context.
|
||||
|
type VarsMatcher map[string]string |
||||
|
|
||||
|
// CaddyModule returns the Caddy module information.
|
||||
|
func (VarsMatcher) CaddyModule() caddy.ModuleInfo { |
||||
|
return caddy.ModuleInfo{ |
||||
|
Name: "http.matchers.vars", |
||||
|
New: func() caddy.Module { return new(VarsMatcher) }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Match matches a request based on variables in the context.
|
||||
|
func (m VarsMatcher) Match(r *http.Request) bool { |
||||
|
vars := r.Context().Value(VarCtxKey).(map[string]string) |
||||
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) |
||||
|
for k, v := range m { |
||||
|
keyExpanded := repl.ReplaceAll(k, "") |
||||
|
valExpanded := repl.ReplaceAll(v, "") |
||||
|
if vars[keyExpanded] != valExpanded { |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// Interface guards
|
||||
|
var ( |
||||
|
_ MiddlewareHandler = (*VarsMiddleware)(nil) |
||||
|
_ RequestMatcher = (*VarsMatcher)(nil) |
||||
|
) |
||||
Loading…
Reference in new issue