mirror of
https://github.com/caddyserver/caddy.git
synced 2025-05-31 20:24:18 -04:00
Merge pull request #2699 from caddyserver/cfadapter
v2: Implement config adapters and WIP Caddyfile adapter
This commit is contained in:
commit
0544f0266a
13
admin.go
13
admin.go
@ -95,9 +95,11 @@ func StartAdmin(initialConfigJSON []byte) error {
|
|||||||
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||||
///// END PPROF STUFF //////
|
///// END PPROF STUFF //////
|
||||||
|
|
||||||
for _, m := range GetModules("admin") {
|
for _, m := range GetModules("admin.routers") {
|
||||||
route := m.New().(AdminRoute)
|
adminrtr := m.New().(AdminRouter)
|
||||||
mux.Handle(route.Pattern, route)
|
for _, route := range adminrtr.Routes() {
|
||||||
|
mux.Handle(route.Pattern, route)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := cors.Default().Handler(mux)
|
handler := cors.Default().Handler(mux)
|
||||||
@ -144,6 +146,11 @@ func StopAdmin() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminRouter is a type which can return routes for the admin API.
|
||||||
|
type AdminRouter interface {
|
||||||
|
Routes() []AdminRoute
|
||||||
|
}
|
||||||
|
|
||||||
// AdminRoute represents a route for the admin endpoint.
|
// AdminRoute represents a route for the admin endpoint.
|
||||||
type AdminRoute struct {
|
type AdminRoute struct {
|
||||||
http.Handler
|
http.Handler
|
||||||
|
87
caddyconfig/caddyfile/adapter.go
Normal file
87
caddyconfig/caddyfile/adapter.go
Normal file
@ -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)
|
341
caddyconfig/caddyfile/dispenser.go
Executable file
341
caddyconfig/caddyfile/dispenser.go
Executable file
@ -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
|
||||||
|
}
|
316
caddyconfig/caddyfile/dispenser_test.go
Executable file
316
caddyconfig/caddyfile/dispenser_test.go
Executable file
@ -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)
|
||||||
|
}
|
150
caddyconfig/caddyfile/lexer.go
Executable file
150
caddyconfig/caddyfile/lexer.go
Executable file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
196
caddyconfig/caddyfile/lexer_test.go
Executable file
196
caddyconfig/caddyfile/lexer_test.go
Executable file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
521
caddyconfig/caddyfile/parse.go
Executable file
521
caddyconfig/caddyfile/parse.go
Executable file
@ -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)
|
||||||
|
}
|
681
caddyconfig/caddyfile/parse_test.go
Executable file
681
caddyconfig/caddyfile/parse_test.go
Executable file
@ -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)}
|
||||||
|
}
|
6
caddyconfig/caddyfile/testdata/import_glob0.txt
vendored
Executable file
6
caddyconfig/caddyfile/testdata/import_glob0.txt
vendored
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
glob0.host0 {
|
||||||
|
dir2 arg1
|
||||||
|
}
|
||||||
|
|
||||||
|
glob0.host1 {
|
||||||
|
}
|
4
caddyconfig/caddyfile/testdata/import_glob1.txt
vendored
Executable file
4
caddyconfig/caddyfile/testdata/import_glob1.txt
vendored
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
glob1.host0 {
|
||||||
|
dir1
|
||||||
|
dir2 arg1
|
||||||
|
}
|
3
caddyconfig/caddyfile/testdata/import_glob2.txt
vendored
Executable file
3
caddyconfig/caddyfile/testdata/import_glob2.txt
vendored
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
glob2.host0 {
|
||||||
|
dir2 arg1
|
||||||
|
}
|
2
caddyconfig/caddyfile/testdata/import_test1.txt
vendored
Executable file
2
caddyconfig/caddyfile/testdata/import_test1.txt
vendored
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
dir2 arg1 arg2
|
||||||
|
dir3
|
4
caddyconfig/caddyfile/testdata/import_test2.txt
vendored
Executable file
4
caddyconfig/caddyfile/testdata/import_test2.txt
vendored
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
host1 {
|
||||||
|
dir1
|
||||||
|
dir2 arg1
|
||||||
|
}
|
113
caddyconfig/configadapters.go
Normal file
113
caddyconfig/configadapters.go
Normal file
@ -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)
|
332
caddyconfig/httpcaddyfile/addresses.go
Normal file
332
caddyconfig/httpcaddyfile/addresses.go
Normal file
@ -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?
|
||||||
|
)
|
166
caddyconfig/httpcaddyfile/addresses_test.go
Normal file
166
caddyconfig/httpcaddyfile/addresses_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
255
caddyconfig/httpcaddyfile/builtins.go
Normal file
255
caddyconfig/httpcaddyfile/builtins.go
Normal file
@ -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
|
||||||
|
}
|
182
caddyconfig/httpcaddyfile/directives.go
Normal file
182
caddyconfig/httpcaddyfile/directives.go
Normal file
@ -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)
|
56
caddyconfig/httpcaddyfile/handlers.go
Normal file
56
caddyconfig/httpcaddyfile/handlers.go
Normal file
@ -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
|
||||||
|
}
|
519
caddyconfig/httpcaddyfile/httptype.go
Normal file
519
caddyconfig/httpcaddyfile/httptype.go
Normal file
@ -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)
|
@ -18,6 +18,7 @@ import (
|
|||||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||||
|
|
||||||
// this is where modules get plugged in
|
// this is where modules get plugged in
|
||||||
|
_ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
||||||
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/brotli"
|
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/brotli"
|
||||||
|
@ -31,6 +31,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/mholt/certmagic"
|
"github.com/mholt/certmagic"
|
||||||
"github.com/mitchellh/go-ps"
|
"github.com/mitchellh/go-ps"
|
||||||
)
|
)
|
||||||
@ -38,6 +39,7 @@ import (
|
|||||||
func cmdStart() (int, error) {
|
func cmdStart() (int, error) {
|
||||||
startCmd := flag.NewFlagSet("start", flag.ExitOnError)
|
startCmd := flag.NewFlagSet("start", flag.ExitOnError)
|
||||||
startCmdConfigFlag := startCmd.String("config", "", "Configuration file")
|
startCmdConfigFlag := startCmd.String("config", "", "Configuration file")
|
||||||
|
startCmdConfigAdapterFlag := startCmd.String("config-adapter", "", "Name of config adapter to apply")
|
||||||
startCmd.Parse(os.Args[2:])
|
startCmd.Parse(os.Args[2:])
|
||||||
|
|
||||||
// open a listener to which the child process will connect when
|
// open a listener to which the child process will connect when
|
||||||
@ -62,6 +64,9 @@ func cmdStart() (int, error) {
|
|||||||
if *startCmdConfigFlag != "" {
|
if *startCmdConfigFlag != "" {
|
||||||
cmd.Args = append(cmd.Args, "--config", *startCmdConfigFlag)
|
cmd.Args = append(cmd.Args, "--config", *startCmdConfigFlag)
|
||||||
}
|
}
|
||||||
|
if *startCmdConfigAdapterFlag != "" {
|
||||||
|
cmd.Args = append(cmd.Args, "--config-adapter", *startCmdConfigAdapterFlag)
|
||||||
|
}
|
||||||
stdinpipe, err := cmd.StdinPipe()
|
stdinpipe, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup,
|
return caddy.ExitCodeFailedStartup,
|
||||||
@ -137,7 +142,8 @@ func cmdStart() (int, error) {
|
|||||||
func cmdRun() (int, error) {
|
func cmdRun() (int, error) {
|
||||||
runCmd := flag.NewFlagSet("run", flag.ExitOnError)
|
runCmd := flag.NewFlagSet("run", flag.ExitOnError)
|
||||||
runCmdConfigFlag := runCmd.String("config", "", "Configuration file")
|
runCmdConfigFlag := runCmd.String("config", "", "Configuration file")
|
||||||
runCmdPrintEnvFlag := runCmd.Bool("print-env", false, "Print environment (useful for debugging)")
|
runCmdConfigAdapterFlag := runCmd.String("config-adapter", "", "Name of config adapter to apply")
|
||||||
|
runCmdPrintEnvFlag := runCmd.Bool("print-env", false, "Print environment")
|
||||||
runCmdPingbackFlag := runCmd.String("pingback", "", "Echo confirmation bytes to this address on success")
|
runCmdPingbackFlag := runCmd.String("pingback", "", "Echo confirmation bytes to this address on success")
|
||||||
runCmd.Parse(os.Args[2:])
|
runCmd.Parse(os.Args[2:])
|
||||||
|
|
||||||
@ -149,16 +155,10 @@ func cmdRun() (int, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if a config file was specified for bootstrapping
|
// get the config in caddy's native format
|
||||||
// the server instance, load it now
|
config, err := loadConfig(*runCmdConfigFlag, *runCmdConfigAdapterFlag)
|
||||||
var config []byte
|
if err != nil {
|
||||||
if *runCmdConfigFlag != "" {
|
return caddy.ExitCodeFailedStartup, err
|
||||||
var err error
|
|
||||||
config, err = ioutil.ReadFile(*runCmdConfigFlag)
|
|
||||||
if err != nil {
|
|
||||||
return caddy.ExitCodeFailedStartup,
|
|
||||||
fmt.Errorf("reading config file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// set a fitting User-Agent for ACME requests
|
// set a fitting User-Agent for ACME requests
|
||||||
@ -167,7 +167,7 @@ func cmdRun() (int, error) {
|
|||||||
certmagic.UserAgent = "Caddy/" + cleanModVersion
|
certmagic.UserAgent = "Caddy/" + cleanModVersion
|
||||||
|
|
||||||
// start the admin endpoint along with any initial config
|
// start the admin endpoint along with any initial config
|
||||||
err := caddy.StartAdmin(config)
|
err = caddy.StartAdmin(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup,
|
return caddy.ExitCodeFailedStartup,
|
||||||
fmt.Errorf("starting caddy administration endpoint: %v", err)
|
fmt.Errorf("starting caddy administration endpoint: %v", err)
|
||||||
@ -226,6 +226,7 @@ func cmdStop() (int, error) {
|
|||||||
func cmdReload() (int, error) {
|
func cmdReload() (int, error) {
|
||||||
reloadCmd := flag.NewFlagSet("load", flag.ExitOnError)
|
reloadCmd := flag.NewFlagSet("load", flag.ExitOnError)
|
||||||
reloadCmdConfigFlag := reloadCmd.String("config", "", "Configuration file")
|
reloadCmdConfigFlag := reloadCmd.String("config", "", "Configuration file")
|
||||||
|
reloadCmdConfigAdapterFlag := reloadCmd.String("config-adapter", "", "Name of config adapter to apply")
|
||||||
reloadCmdAddrFlag := reloadCmd.String("address", "", "Address of the administration listener, if different from config")
|
reloadCmdAddrFlag := reloadCmd.String("address", "", "Address of the administration listener, if different from config")
|
||||||
reloadCmd.Parse(os.Args[2:])
|
reloadCmd.Parse(os.Args[2:])
|
||||||
|
|
||||||
@ -235,11 +236,10 @@ func cmdReload() (int, error) {
|
|||||||
fmt.Errorf("no configuration to load (use --config)")
|
fmt.Errorf("no configuration to load (use --config)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// load the configuration file
|
// get the config in caddy's native format
|
||||||
config, err := ioutil.ReadFile(*reloadCmdConfigFlag)
|
config, err := loadConfig(*reloadCmdConfigFlag, *reloadCmdConfigAdapterFlag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup,
|
return caddy.ExitCodeFailedStartup, err
|
||||||
fmt.Errorf("reading config file: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the address of the admin listener and craft endpoint URL
|
// get the address of the admin listener and craft endpoint URL
|
||||||
@ -306,3 +306,52 @@ func cmdEnviron() (int, error) {
|
|||||||
}
|
}
|
||||||
return caddy.ExitCodeSuccess, nil
|
return caddy.ExitCodeSuccess, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cmdAdaptConfig() (int, error) {
|
||||||
|
adaptCmd := flag.NewFlagSet("adapt", flag.ExitOnError)
|
||||||
|
adaptCmdAdapterFlag := adaptCmd.String("adapter", "", "Name of config adapter")
|
||||||
|
adaptCmdInputFlag := adaptCmd.String("input", "", "Configuration file to adapt")
|
||||||
|
adaptCmdPrettyFlag := adaptCmd.Bool("pretty", false, "Format the output for human readability")
|
||||||
|
adaptCmd.Parse(os.Args[2:])
|
||||||
|
|
||||||
|
if *adaptCmdAdapterFlag == "" || *adaptCmdInputFlag == "" {
|
||||||
|
return caddy.ExitCodeFailedStartup,
|
||||||
|
fmt.Errorf("usage: caddy adapt-config --adapter <name> --input <file>")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgAdapter := caddyconfig.GetAdapter(*adaptCmdAdapterFlag)
|
||||||
|
if cfgAdapter == nil {
|
||||||
|
return caddy.ExitCodeFailedStartup,
|
||||||
|
fmt.Errorf("unrecognized config adapter: %s", *adaptCmdAdapterFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
input, err := ioutil.ReadFile(*adaptCmdInputFlag)
|
||||||
|
if err != nil {
|
||||||
|
return caddy.ExitCodeFailedStartup,
|
||||||
|
fmt.Errorf("reading input file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := make(map[string]string)
|
||||||
|
if *adaptCmdPrettyFlag {
|
||||||
|
opts["pretty"] = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
|
||||||
|
if err != nil {
|
||||||
|
return caddy.ExitCodeFailedStartup, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// print warnings to stderr
|
||||||
|
for _, warn := range warnings {
|
||||||
|
msg := warn.Message
|
||||||
|
if warn.Directive != "" {
|
||||||
|
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
|
||||||
|
}
|
||||||
|
log.Printf("[WARNING][%s] %s:%d: %s", *adaptCmdAdapterFlag, warn.File, warn.Line, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// print result to stdout
|
||||||
|
fmt.Println(string(adaptedConfig))
|
||||||
|
|
||||||
|
return caddy.ExitCodeSuccess, nil
|
||||||
|
}
|
||||||
|
65
cmd/main.go
65
cmd/main.go
@ -25,6 +25,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Main implements the main function of the caddy command.
|
// Main implements the main function of the caddy command.
|
||||||
@ -62,6 +63,7 @@ var commands = map[string]commandFunc{
|
|||||||
"version": cmdVersion,
|
"version": cmdVersion,
|
||||||
"list-modules": cmdListModules,
|
"list-modules": cmdListModules,
|
||||||
"environ": cmdEnviron,
|
"environ": cmdEnviron,
|
||||||
|
"adapt-config": cmdAdaptConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
func usageString() string {
|
func usageString() string {
|
||||||
@ -85,3 +87,66 @@ func handlePingbackConn(conn net.Conn, expect []byte) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadConfig loads the config from configFile and adapts it
|
||||||
|
// using adapterName. If adapterName is specified, configFile
|
||||||
|
// must be also. It prints any warnings to stderr, and returns
|
||||||
|
// the resulting JSON config bytes.
|
||||||
|
func loadConfig(configFile, adapterName string) ([]byte, error) {
|
||||||
|
// specifying an adapter without a config file is ambiguous
|
||||||
|
if configFile == "" && adapterName != "" {
|
||||||
|
return nil, fmt.Errorf("cannot adapt config without config file (use --config)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// load initial config and adapter
|
||||||
|
var config []byte
|
||||||
|
var cfgAdapter caddyconfig.Adapter
|
||||||
|
var err error
|
||||||
|
if configFile != "" {
|
||||||
|
config, err = ioutil.ReadFile(configFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading config file: %v", err)
|
||||||
|
}
|
||||||
|
} else if adapterName == "" {
|
||||||
|
// as a special case when no config file or adapter
|
||||||
|
// is specified, see if the Caddyfile adapter is
|
||||||
|
// plugged in, and if so, try using a default Caddyfile
|
||||||
|
cfgAdapter = caddyconfig.GetAdapter("caddyfile")
|
||||||
|
if cfgAdapter != nil {
|
||||||
|
config, err = ioutil.ReadFile("Caddyfile")
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("reading default Caddyfile: %v", err)
|
||||||
|
}
|
||||||
|
configFile = "Caddyfile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// load config adapter
|
||||||
|
if adapterName != "" {
|
||||||
|
cfgAdapter = caddyconfig.GetAdapter(adapterName)
|
||||||
|
if cfgAdapter == nil {
|
||||||
|
return nil, fmt.Errorf("unrecognized config adapter: %s", adapterName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// adapt config
|
||||||
|
if cfgAdapter != nil {
|
||||||
|
adaptedConfig, warnings, err := cfgAdapter.Adapt(config, map[string]string{
|
||||||
|
"filename": configFile,
|
||||||
|
// TODO: all other options... (http-port, etc...)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("adapting config using %s: %v", adapterName, err)
|
||||||
|
}
|
||||||
|
for _, warn := range warnings {
|
||||||
|
msg := warn.Message
|
||||||
|
if warn.Directive != "" {
|
||||||
|
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
|
||||||
|
}
|
||||||
|
fmt.Printf("[WARNING][%s] %s:%d: %s", adapterName, warn.File, warn.Line, msg)
|
||||||
|
}
|
||||||
|
config = adaptedConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
11
context.go
11
context.go
@ -99,11 +99,16 @@ func (ctx Context) LoadModule(name string, rawMsg json.RawMessage) (interface{},
|
|||||||
return nil, fmt.Errorf("module '%s' has no constructor", mod.Name)
|
return nil, fmt.Errorf("module '%s' has no constructor", mod.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
val := mod.New()
|
val := mod.New().(interface{})
|
||||||
|
|
||||||
// value must be a pointer for unmarshaling into concrete type
|
// value must be a pointer for unmarshaling into concrete type, even if
|
||||||
|
// the module's concrete type is a slice or map; New() *should* return
|
||||||
|
// a pointer, otherwise unmarshaling errors or panics will occur
|
||||||
if rv := reflect.ValueOf(val); rv.Kind() != reflect.Ptr {
|
if rv := reflect.ValueOf(val); rv.Kind() != reflect.Ptr {
|
||||||
val = reflect.New(rv.Type()).Elem().Addr().Interface()
|
log.Printf("[WARNING] ModuleInfo.New() for module '%s' did not return a pointer,"+
|
||||||
|
" so we are using reflection to make a pointer instead; please fix this by"+
|
||||||
|
" using new(Type) or &Type notation in your module's New() function.", name)
|
||||||
|
val = reflect.New(rv.Type()).Elem().Addr().Interface().(Module)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fill in its config only if there is a config to fill in
|
// fill in its config only if there is a config to fill in
|
||||||
|
10
go.mod
10
go.mod
@ -3,20 +3,22 @@ module github.com/caddyserver/caddy/v2
|
|||||||
go 1.12
|
go 1.12
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/DataDog/zstd v1.4.0 // indirect
|
github.com/DataDog/zstd v1.4.1 // indirect
|
||||||
github.com/Masterminds/goutils v1.1.0 // indirect
|
github.com/Masterminds/goutils v1.1.0 // indirect
|
||||||
github.com/Masterminds/semver v1.4.2 // indirect
|
github.com/Masterminds/semver v1.4.2 // indirect
|
||||||
github.com/Masterminds/sprig v2.20.0+incompatible
|
github.com/Masterminds/sprig v2.20.0+incompatible
|
||||||
github.com/andybalholm/brotli v0.0.0-20190704151324-71eb68cc467c
|
github.com/andybalholm/brotli v0.0.0-20190704151324-71eb68cc467c
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.0
|
github.com/dustin/go-humanize v1.0.0
|
||||||
github.com/go-acme/lego v2.6.0+incompatible
|
github.com/go-acme/lego v2.6.0+incompatible
|
||||||
github.com/google/go-cmp v0.3.0 // indirect
|
github.com/google/go-cmp v0.3.1 // indirect
|
||||||
github.com/google/uuid v1.1.1 // indirect
|
github.com/google/uuid v1.1.1 // indirect
|
||||||
github.com/huandu/xstrings v1.2.0 // indirect
|
github.com/huandu/xstrings v1.2.0 // indirect
|
||||||
github.com/imdario/mergo v0.3.7 // indirect
|
github.com/imdario/mergo v0.3.7 // indirect
|
||||||
github.com/klauspost/compress v1.7.1-0.20190613161414-0b31f265a57b
|
github.com/klauspost/compress v1.7.1-0.20190613161414-0b31f265a57b
|
||||||
github.com/klauspost/cpuid v1.2.1
|
github.com/klauspost/cpuid v1.2.1
|
||||||
github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2
|
github.com/kr/pretty v0.1.0 // indirect
|
||||||
|
github.com/mholt/certmagic v0.6.2
|
||||||
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936
|
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936
|
||||||
github.com/rs/cors v1.6.0
|
github.com/rs/cors v1.6.0
|
||||||
github.com/russross/blackfriday/v2 v2.0.1
|
github.com/russross/blackfriday/v2 v2.0.1
|
||||||
@ -24,6 +26,8 @@ require (
|
|||||||
github.com/starlight-go/starlight v0.0.0-20181207205707-b06f321544f3
|
github.com/starlight-go/starlight v0.0.0-20181207205707-b06f321544f3
|
||||||
go.starlark.net v0.0.0-20190604130855-6ddc71c0ba77
|
go.starlark.net v0.0.0-20190604130855-6ddc71c0ba77
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 // indirect
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||||
gopkg.in/yaml.v2 v2.2.2 // indirect
|
gopkg.in/yaml.v2 v2.2.2 // indirect
|
||||||
)
|
)
|
||||||
|
23
go.sum
23
go.sum
@ -1,5 +1,5 @@
|
|||||||
github.com/DataDog/zstd v1.4.0 h1:vhoV+DUHnRZdKW1i5UMjAk2G4JY8wN4ayRfYDNdEhwo=
|
github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
|
||||||
github.com/DataDog/zstd v1.4.0/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||||
github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=
|
github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=
|
||||||
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||||
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
|
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
|
||||||
@ -12,6 +12,8 @@ github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1q
|
|||||||
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/go-acme/lego v2.5.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M=
|
github.com/go-acme/lego v2.5.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M=
|
||||||
@ -19,8 +21,8 @@ github.com/go-acme/lego v2.6.0+incompatible h1:KxcEWOF5hKtgou4xIqPaXSRF9DoO4OJ90
|
|||||||
github.com/go-acme/lego v2.6.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M=
|
github.com/go-acme/lego v2.6.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M=
|
||||||
github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721 h1:KRMr9A3qfbVM7iV/WcLY/rL5LICqwMHLhwRXKu99fXw=
|
github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721 h1:KRMr9A3qfbVM7iV/WcLY/rL5LICqwMHLhwRXKu99fXw=
|
||||||
github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
|
github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
|
||||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0=
|
github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0=
|
||||||
@ -32,8 +34,13 @@ github.com/klauspost/compress v1.7.1-0.20190613161414-0b31f265a57b/go.mod h1:RyI
|
|||||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
|
github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
|
||||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2 h1:xKE9kZ5C8gelJC3+BNM6LJs1x21rivK7yxfTZMAuY2s=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/mholt/certmagic v0.6.2 h1:yy9cKm3rtxdh12SW4E51lzG3Eo6N59LEOfBQ0CTnMms=
|
||||||
|
github.com/mholt/certmagic v0.6.2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY=
|
||||||
github.com/miekg/dns v1.1.3 h1:1g0r1IvskvgL8rR+AcHzUA+oFmGcQlaIm4IqakufeMM=
|
github.com/miekg/dns v1.1.3 h1:1g0r1IvskvgL8rR+AcHzUA+oFmGcQlaIm4IqakufeMM=
|
||||||
github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||||
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936 h1:kw1v0NlnN+GZcU8Ma8CLF2Zzgjfx95gs3/GN3vYAPpo=
|
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936 h1:kw1v0NlnN+GZcU8Ma8CLF2Zzgjfx95gs3/GN3vYAPpo=
|
||||||
@ -64,12 +71,16 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/square/go-jose.v2 v2.2.2 h1:orlkJ3myw8CN1nVQHBFfloD+L3egixIa4FvUP6RosSA=
|
gopkg.in/square/go-jose.v2 v2.2.2 h1:orlkJ3myw8CN1nVQHBFfloD+L3egixIa4FvUP6RosSA=
|
||||||
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
|
101
modules.go
101
modules.go
@ -23,28 +23,76 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Module represents a Caddy module.
|
// Module is a type that is used as a Caddy module.
|
||||||
type Module struct {
|
type Module interface {
|
||||||
|
// This method indicates the type is a Caddy
|
||||||
|
// module. The returned ModuleInfo must have
|
||||||
|
// both a name and a constructor function.
|
||||||
|
// This method must not have any side-effects.
|
||||||
|
CaddyModule() ModuleInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModuleInfo represents a registered Caddy module.
|
||||||
|
type ModuleInfo struct {
|
||||||
// Name is the full name of the module. It
|
// Name is the full name of the module. It
|
||||||
// must be unique and properly namespaced.
|
// must be unique and properly namespaced.
|
||||||
Name string
|
Name string
|
||||||
|
|
||||||
// New returns a new, empty instance of
|
// New returns a pointer to a new, empty
|
||||||
// the module's type. The host module
|
// instance of the module's type. The host
|
||||||
// which loads this module will likely
|
// module which instantiates this module will
|
||||||
// invoke methods on the returned value.
|
// likely type-assert and invoke methods on
|
||||||
// It must return a pointer; if not, it
|
// the returned value. This function must not
|
||||||
// is converted into one.
|
// have any side-effects.
|
||||||
New func() interface{}
|
New func() Module
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Module) String() string { return m.Name }
|
// Namespace returns the module's namespace (scope)
|
||||||
|
// which is all but the last element of its name.
|
||||||
|
// If there is no explicit namespace in the name,
|
||||||
|
// the whole name is considered the namespace.
|
||||||
|
func (mi ModuleInfo) Namespace() string {
|
||||||
|
lastDot := strings.LastIndex(mi.Name, ".")
|
||||||
|
if lastDot < 0 {
|
||||||
|
return mi.Name
|
||||||
|
}
|
||||||
|
return mi.Name[:lastDot]
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterModule registers a module. Modules must call
|
// ID returns a module's ID, which is the
|
||||||
// this function in the init phase of runtime.
|
// last element of its name.
|
||||||
func RegisterModule(mod Module) error {
|
func (mi ModuleInfo) ID() string {
|
||||||
if mod.Name == "caddy" {
|
if mi.Name == "" {
|
||||||
return fmt.Errorf("modules cannot be named 'caddy'")
|
return ""
|
||||||
|
}
|
||||||
|
parts := strings.Split(mi.Name, ".")
|
||||||
|
return parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mi ModuleInfo) String() string { return mi.Name }
|
||||||
|
|
||||||
|
// RegisterModule registers a module by receiving a
|
||||||
|
// plain/empty value of the module. For registration to
|
||||||
|
// be properly recorded, this should be called in the
|
||||||
|
// init phase of runtime. Typically, the module package
|
||||||
|
// will do this as a side-effect of being imported.
|
||||||
|
// This function returns an error if the module's info
|
||||||
|
// is incomplete or invalid, or if the module is
|
||||||
|
// already registered.
|
||||||
|
func RegisterModule(instance Module) error {
|
||||||
|
mod := instance.CaddyModule()
|
||||||
|
|
||||||
|
if mod.Name == "" {
|
||||||
|
return fmt.Errorf("missing ModuleInfo.Name")
|
||||||
|
}
|
||||||
|
if mod.Name == "caddy" || mod.Name == "admin" {
|
||||||
|
return fmt.Errorf("module name '%s' is reserved", mod.Name)
|
||||||
|
}
|
||||||
|
if mod.New == nil {
|
||||||
|
return fmt.Errorf("missing ModuleInfo.New")
|
||||||
|
}
|
||||||
|
if val := mod.New(); val == nil {
|
||||||
|
return fmt.Errorf("ModuleInfo.New must return a non-nil module instance")
|
||||||
}
|
}
|
||||||
|
|
||||||
modulesMu.Lock()
|
modulesMu.Lock()
|
||||||
@ -57,18 +105,27 @@ func RegisterModule(mod Module) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetModule returns a module by its full name.
|
// GetModule returns module information from its full name.
|
||||||
func GetModule(name string) (Module, error) {
|
func GetModule(name string) (ModuleInfo, error) {
|
||||||
modulesMu.Lock()
|
modulesMu.Lock()
|
||||||
defer modulesMu.Unlock()
|
defer modulesMu.Unlock()
|
||||||
|
|
||||||
m, ok := modules[name]
|
m, ok := modules[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return Module{}, fmt.Errorf("module not registered: %s", name)
|
return ModuleInfo{}, fmt.Errorf("module not registered: %s", name)
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetModuleName returns a module's name from an instance of its value.
|
||||||
|
// If the value is not a module, an empty name will be returned.
|
||||||
|
func GetModuleName(instance interface{}) string {
|
||||||
|
var name string
|
||||||
|
if mod, ok := instance.(Module); ok {
|
||||||
|
name = mod.CaddyModule().Name
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
// GetModules returns all modules in the given scope/namespace.
|
// GetModules returns all modules in the given scope/namespace.
|
||||||
// For example, a scope of "foo" returns modules named "foo.bar",
|
// For example, a scope of "foo" returns modules named "foo.bar",
|
||||||
// "foo.loo", but not "bar", "foo.bar.loo", etc. An empty scope
|
// "foo.loo", but not "bar", "foo.bar.loo", etc. An empty scope
|
||||||
@ -78,7 +135,7 @@ func GetModule(name string) (Module, error) {
|
|||||||
//
|
//
|
||||||
// Because modules are registered to a map, the returned slice
|
// Because modules are registered to a map, the returned slice
|
||||||
// will be sorted to keep it deterministic.
|
// will be sorted to keep it deterministic.
|
||||||
func GetModules(scope string) []Module {
|
func GetModules(scope string) []ModuleInfo {
|
||||||
modulesMu.Lock()
|
modulesMu.Lock()
|
||||||
defer modulesMu.Unlock()
|
defer modulesMu.Unlock()
|
||||||
|
|
||||||
@ -90,7 +147,7 @@ func GetModules(scope string) []Module {
|
|||||||
scopeParts = []string{}
|
scopeParts = []string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var mods []Module
|
var mods []ModuleInfo
|
||||||
iterateModules:
|
iterateModules:
|
||||||
for name, m := range modules {
|
for name, m := range modules {
|
||||||
modParts := strings.Split(name, ".")
|
modParts := strings.Split(name, ".")
|
||||||
@ -203,6 +260,6 @@ func strictUnmarshalJSON(data []byte, v interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
modules = make(map[string]Module)
|
modules = make(map[string]ModuleInfo)
|
||||||
modulesMu sync.Mutex
|
modulesMu sync.Mutex
|
||||||
)
|
)
|
||||||
|
@ -15,9 +15,12 @@
|
|||||||
package caddyhttp
|
package caddyhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
weakrand "math/rand"
|
weakrand "math/rand"
|
||||||
"net"
|
"net"
|
||||||
@ -34,10 +37,7 @@ import (
|
|||||||
func init() {
|
func init() {
|
||||||
weakrand.Seed(time.Now().UnixNano())
|
weakrand.Seed(time.Now().UnixNano())
|
||||||
|
|
||||||
err := caddy.RegisterModule(caddy.Module{
|
err := caddy.RegisterModule(App{})
|
||||||
Name: "http",
|
|
||||||
New: func() interface{} { return new(App) },
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -55,6 +55,14 @@ type App struct {
|
|||||||
ctx caddy.Context
|
ctx caddy.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (App) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http",
|
||||||
|
New: func() caddy.Module { return new(App) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Provision sets up the app.
|
// Provision sets up the app.
|
||||||
func (app *App) Provision(ctx caddy.Context) error {
|
func (app *App) Provision(ctx caddy.Context) error {
|
||||||
app.ctx = ctx
|
app.ctx = ctx
|
||||||
@ -224,7 +232,7 @@ func (app *App) automaticHTTPS() error {
|
|||||||
// find all qualifying domain names, de-duplicated
|
// find all qualifying domain names, de-duplicated
|
||||||
domainSet := make(map[string]struct{})
|
domainSet := make(map[string]struct{})
|
||||||
for _, route := range srv.Routes {
|
for _, route := range srv.Routes {
|
||||||
for _, matcherSet := range route.matcherSets {
|
for _, matcherSet := range route.MatcherSets {
|
||||||
for _, m := range matcherSet {
|
for _, m := range matcherSet {
|
||||||
if hm, ok := m.(*MatchHost); ok {
|
if hm, ok := m.(*MatchHost); ok {
|
||||||
for _, d := range *hm {
|
for _, d := range *hm {
|
||||||
@ -244,6 +252,14 @@ func (app *App) automaticHTTPS() error {
|
|||||||
for d := range domainSet {
|
for d := range domainSet {
|
||||||
domains = append(domains, d)
|
domains = append(domains, d)
|
||||||
if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) {
|
if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) {
|
||||||
|
// if a certificate for this name is already loaded,
|
||||||
|
// don't obtain another one for it, unless we are
|
||||||
|
// supposed to ignore loaded certificates
|
||||||
|
if !srv.AutoHTTPS.IgnoreLoadedCerts &&
|
||||||
|
len(tlsApp.CertificatesWithSAN(d)) > 0 {
|
||||||
|
log.Printf("[INFO][%s] Skipping automatic certificate management because a certificate with that SAN is already loaded", d)
|
||||||
|
continue
|
||||||
|
}
|
||||||
domainsForCerts = append(domainsForCerts, d)
|
domainsForCerts = append(domainsForCerts, d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -319,16 +335,16 @@ func (app *App) automaticHTTPS() error {
|
|||||||
}
|
}
|
||||||
redirTo += "{http.request.uri}"
|
redirTo += "{http.request.uri}"
|
||||||
|
|
||||||
redirRoutes = append(redirRoutes, ServerRoute{
|
redirRoutes = append(redirRoutes, Route{
|
||||||
matcherSets: []MatcherSet{
|
MatcherSets: []MatcherSet{
|
||||||
{
|
{
|
||||||
MatchProtocol("http"),
|
MatchProtocol("http"),
|
||||||
MatchHost(domains),
|
MatchHost(domains),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
handlers: []MiddlewareHandler{
|
Handlers: []MiddlewareHandler{
|
||||||
StaticResponse{
|
StaticResponse{
|
||||||
StatusCode: weakString(strconv.Itoa(http.StatusTemporaryRedirect)), // TODO: use permanent redirect instead
|
StatusCode: WeakString(strconv.Itoa(http.StatusTemporaryRedirect)), // TODO: use permanent redirect instead
|
||||||
Headers: http.Header{
|
Headers: http.Header{
|
||||||
"Location": []string{redirTo},
|
"Location": []string{redirTo},
|
||||||
"Connection": []string{"close"},
|
"Connection": []string{"close"},
|
||||||
@ -431,6 +447,77 @@ type MiddlewareHandler interface {
|
|||||||
// emptyHandler is used as a no-op handler.
|
// emptyHandler is used as a no-op handler.
|
||||||
var emptyHandler HandlerFunc = func(http.ResponseWriter, *http.Request) error { return nil }
|
var emptyHandler HandlerFunc = func(http.ResponseWriter, *http.Request) error { return nil }
|
||||||
|
|
||||||
|
// WeakString is a type that unmarshals any JSON value
|
||||||
|
// as a string literal, with the following exceptions:
|
||||||
|
// 1) actual string values are decoded as strings, and
|
||||||
|
// 2) null is decoded as empty string
|
||||||
|
// and provides methods for getting the value as various
|
||||||
|
// primitive types. However, using this type removes any
|
||||||
|
// type safety as far as deserializing JSON is concerned.
|
||||||
|
type WeakString string
|
||||||
|
|
||||||
|
// UnmarshalJSON satisfies json.Unmarshaler according to
|
||||||
|
// this type's documentation.
|
||||||
|
func (ws *WeakString) UnmarshalJSON(b []byte) error {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
if b[0] == byte('"') && b[len(b)-1] == byte('"') {
|
||||||
|
var s string
|
||||||
|
err := json.Unmarshal(b, &s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*ws = WeakString(s)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if bytes.Equal(b, []byte("null")) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
*ws = WeakString(b)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON marshals was a boolean if true or false,
|
||||||
|
// a number if an integer, or a string otherwise.
|
||||||
|
func (ws WeakString) MarshalJSON() ([]byte, error) {
|
||||||
|
if ws == "true" {
|
||||||
|
return []byte("true"), nil
|
||||||
|
}
|
||||||
|
if ws == "false" {
|
||||||
|
return []byte("false"), nil
|
||||||
|
}
|
||||||
|
if num, err := strconv.Atoi(string(ws)); err == nil {
|
||||||
|
return json.Marshal(num)
|
||||||
|
}
|
||||||
|
return json.Marshal(string(ws))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int returns ws as an integer. If ws is not an
|
||||||
|
// integer, 0 is returned.
|
||||||
|
func (ws WeakString) Int() int {
|
||||||
|
num, _ := strconv.Atoi(string(ws))
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64 returns ws as a float64. If ws is not a
|
||||||
|
// float value, the zero value is returned.
|
||||||
|
func (ws WeakString) Float64() float64 {
|
||||||
|
num, _ := strconv.ParseFloat(string(ws), 64)
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool returns ws as a boolean. If ws is not a
|
||||||
|
// boolean, false is returned.
|
||||||
|
func (ws WeakString) Bool() bool {
|
||||||
|
return string(ws) == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns ws as a string.
|
||||||
|
func (ws WeakString) String() string {
|
||||||
|
return string(ws)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// DefaultHTTPPort is the default port for HTTP.
|
// DefaultHTTPPort is the default port for HTTP.
|
||||||
DefaultHTTPPort = 80
|
DefaultHTTPPort = 80
|
||||||
|
@ -24,10 +24,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(Log{})
|
||||||
Name: "http.handlers.log",
|
|
||||||
New: func() interface{} { return new(Log) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log implements a simple logging middleware.
|
// Log implements a simple logging middleware.
|
||||||
@ -36,6 +33,14 @@ type Log struct {
|
|||||||
counter int
|
counter int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (Log) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.handlers.log",
|
||||||
|
New: func() caddy.Module { return new(Log) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (l *Log) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
func (l *Log) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
@ -16,17 +16,16 @@ package caddybrotli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/andybalholm/brotli"
|
"github.com/andybalholm/brotli"
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(Brotli{})
|
||||||
Name: "http.encoders.brotli",
|
|
||||||
New: func() interface{} { return new(Brotli) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Brotli can create brotli encoders. Note that brotli
|
// Brotli can create brotli encoders. Note that brotli
|
||||||
@ -35,6 +34,30 @@ type Brotli struct {
|
|||||||
Quality *int `json:"quality,omitempty"`
|
Quality *int `json:"quality,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (Brotli) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.encoders.brotli",
|
||||||
|
New: func() caddy.Module { return new(Brotli) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens.
|
||||||
|
func (b *Brotli) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
for d.Next() {
|
||||||
|
if !d.NextArg() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
qualityStr := d.Val()
|
||||||
|
quality, err := strconv.Atoi(qualityStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.Quality = &quality
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Validate validates b's configuration.
|
// Validate validates b's configuration.
|
||||||
func (b Brotli) Validate() error {
|
func (b Brotli) Validate() error {
|
||||||
if b.Quality != nil {
|
if b.Quality != nil {
|
||||||
@ -64,6 +87,7 @@ func (b Brotli) NewEncoder() encode.Encoder {
|
|||||||
|
|
||||||
// Interface guards
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ encode.Encoding = (*Brotli)(nil)
|
_ encode.Encoding = (*Brotli)(nil)
|
||||||
_ caddy.Validator = (*Brotli)(nil)
|
_ caddy.Validator = (*Brotli)(nil)
|
||||||
|
_ caddyfile.Unmarshaler = (*Brotli)(nil)
|
||||||
)
|
)
|
||||||
|
99
modules/caddyhttp/encode/caddyfile.go
Normal file
99
modules/caddyhttp/encode/caddyfile.go
Normal file
@ -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)
|
@ -35,10 +35,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(Encode{})
|
||||||
Name: "http.handlers.encode",
|
|
||||||
New: func() interface{} { return new(Encode) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode is a middleware which can encode responses.
|
// Encode is a middleware which can encode responses.
|
||||||
@ -50,21 +47,25 @@ type Encode struct {
|
|||||||
writerPools map[string]*sync.Pool // TODO: these pools do not get reused through config reloads...
|
writerPools map[string]*sync.Pool // TODO: these pools do not get reused through config reloads...
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (Encode) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.handlers.encode",
|
||||||
|
New: func() caddy.Module { return new(Encode) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Provision provisions enc.
|
// Provision provisions enc.
|
||||||
func (enc *Encode) Provision(ctx caddy.Context) error {
|
func (enc *Encode) Provision(ctx caddy.Context) error {
|
||||||
enc.writerPools = make(map[string]*sync.Pool)
|
|
||||||
|
|
||||||
for modName, rawMsg := range enc.EncodingsRaw {
|
for modName, rawMsg := range enc.EncodingsRaw {
|
||||||
val, err := ctx.LoadModule("http.encoders."+modName, rawMsg)
|
val, err := ctx.LoadModule("http.encoders."+modName, rawMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("loading encoder module '%s': %v", modName, err)
|
return fmt.Errorf("loading encoder module '%s': %v", modName, err)
|
||||||
}
|
}
|
||||||
encoder := val.(Encoding)
|
encoding := val.(Encoding)
|
||||||
|
err = enc.addEncoding(encoding)
|
||||||
enc.writerPools[encoder.AcceptEncoding()] = &sync.Pool{
|
if err != nil {
|
||||||
New: func() interface{} {
|
return err
|
||||||
return encoder.NewEncoder()
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
enc.EncodingsRaw = nil // allow GC to deallocate - TODO: Does this help?
|
enc.EncodingsRaw = nil // allow GC to deallocate - TODO: Does this help?
|
||||||
@ -85,10 +86,28 @@ func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
|
|||||||
defer w.(*responseWriter).Close()
|
defer w.(*responseWriter).Close()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return next.ServeHTTP(w, r)
|
return next.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (enc *Encode) addEncoding(e Encoding) error {
|
||||||
|
ae := e.AcceptEncoding()
|
||||||
|
if ae == "" {
|
||||||
|
return fmt.Errorf("encoder does not specify an Accept-Encoding value")
|
||||||
|
}
|
||||||
|
if _, ok := enc.writerPools[ae]; ok {
|
||||||
|
return fmt.Errorf("encoder already added: %s", ae)
|
||||||
|
}
|
||||||
|
if enc.writerPools == nil {
|
||||||
|
enc.writerPools = make(map[string]*sync.Pool)
|
||||||
|
}
|
||||||
|
enc.writerPools[ae] = &sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return e.NewEncoder()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// openResponseWriter creates a new response writer that may (or may not)
|
// openResponseWriter creates a new response writer that may (or may not)
|
||||||
// encode the response with encodingName. The returned response writer MUST
|
// encode the response with encodingName. The returned response writer MUST
|
||||||
// be closed after the handler completes.
|
// be closed after the handler completes.
|
||||||
|
@ -18,16 +18,15 @@ import (
|
|||||||
"compress/flate"
|
"compress/flate"
|
||||||
"compress/gzip" // TODO: consider using https://github.com/klauspost/compress/gzip
|
"compress/gzip" // TODO: consider using https://github.com/klauspost/compress/gzip
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(Gzip{})
|
||||||
Name: "http.encoders.gzip",
|
|
||||||
New: func() interface{} { return new(Gzip) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gzip can create gzip encoders.
|
// Gzip can create gzip encoders.
|
||||||
@ -35,6 +34,30 @@ type Gzip struct {
|
|||||||
Level int `json:"level,omitempty"`
|
Level int `json:"level,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (Gzip) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.encoders.gzip",
|
||||||
|
New: func() caddy.Module { return new(Gzip) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens.
|
||||||
|
func (g *Gzip) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
for d.Next() {
|
||||||
|
if !d.NextArg() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
levelStr := d.Val()
|
||||||
|
level, err := strconv.Atoi(levelStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
g.Level = level
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Provision provisions g's configuration.
|
// Provision provisions g's configuration.
|
||||||
func (g *Gzip) Provision(ctx caddy.Context) error {
|
func (g *Gzip) Provision(ctx caddy.Context) error {
|
||||||
if g.Level == 0 {
|
if g.Level == 0 {
|
||||||
@ -69,7 +92,8 @@ var defaultGzipLevel = 5
|
|||||||
|
|
||||||
// Interface guards
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ encode.Encoding = (*Gzip)(nil)
|
_ encode.Encoding = (*Gzip)(nil)
|
||||||
_ caddy.Provisioner = (*Gzip)(nil)
|
_ caddy.Provisioner = (*Gzip)(nil)
|
||||||
_ caddy.Validator = (*Gzip)(nil)
|
_ caddy.Validator = (*Gzip)(nil)
|
||||||
|
_ caddyfile.Unmarshaler = (*Gzip)(nil)
|
||||||
)
|
)
|
||||||
|
@ -16,20 +16,31 @@ package caddyzstd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
||||||
"github.com/klauspost/compress/zstd"
|
"github.com/klauspost/compress/zstd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(Zstd{})
|
||||||
Name: "http.encoders.zstd",
|
|
||||||
New: func() interface{} { return new(Zstd) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zstd can create Zstandard encoders.
|
// Zstd can create Zstandard encoders.
|
||||||
type Zstd struct{}
|
type Zstd struct{}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (Zstd) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.encoders.zstd",
|
||||||
|
New: func() caddy.Module { return new(Zstd) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens.
|
||||||
|
func (z *Zstd) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// AcceptEncoding returns the name of the encoding as
|
// AcceptEncoding returns the name of the encoding as
|
||||||
// used in the Accept-Encoding request headers.
|
// used in the Accept-Encoding request headers.
|
||||||
func (Zstd) AcceptEncoding() string { return "zstd" }
|
func (Zstd) AcceptEncoding() string { return "zstd" }
|
||||||
@ -40,5 +51,8 @@ func (z Zstd) NewEncoder() encode.Encoder {
|
|||||||
return writer
|
return writer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guard
|
// Interface guards
|
||||||
var _ encode.Encoding = (*Zstd)(nil)
|
var (
|
||||||
|
_ encode.Encoding = (*Zstd)(nil)
|
||||||
|
_ caddyfile.Unmarshaler = (*Zstd)(nil)
|
||||||
|
)
|
||||||
|
104
modules/caddyhttp/fileserver/caddyfile.go
Normal file
104
modules/caddyhttp/fileserver/caddyfile.go
Normal file
@ -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
|
||||||
|
}
|
@ -21,14 +21,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(MatchFile{})
|
||||||
Name: "http.matchers.file",
|
|
||||||
New: func() interface{} { return new(MatchFile) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchFile is an HTTP request matcher that can match
|
// MatchFile is an HTTP request matcher that can match
|
||||||
@ -51,6 +49,50 @@ type MatchFile struct {
|
|||||||
TryPolicy string `json:"try_policy,omitempty"`
|
TryPolicy string `json:"try_policy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (MatchFile) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.matchers.file",
|
||||||
|
New: func() caddy.Module { return new(MatchFile) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalCaddyfile sets up the matcher from Caddyfile tokens. Syntax:
|
||||||
|
//
|
||||||
|
// file {
|
||||||
|
// root <path>
|
||||||
|
// try_files <files...>
|
||||||
|
// try_policy first_exist|smallest_size|largest_size|most_recent_modified
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
for d.Next() {
|
||||||
|
for d.NextBlock() {
|
||||||
|
switch d.Val() {
|
||||||
|
case "root":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
m.Root = d.Val()
|
||||||
|
case "try_files":
|
||||||
|
m.TryFiles = d.RemainingArgs()
|
||||||
|
if len(m.TryFiles) == 0 {
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
case "try_policy":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
m.TryPolicy = d.Val()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.Root == "" {
|
||||||
|
m.Root = "{http.var.root}"
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Validate ensures m has a valid configuration.
|
// Validate ensures m has a valid configuration.
|
||||||
func (m MatchFile) Validate() error {
|
func (m MatchFile) Validate() error {
|
||||||
switch m.TryPolicy {
|
switch m.TryPolicy {
|
||||||
@ -87,7 +129,7 @@ func (m MatchFile) Match(r *http.Request) bool {
|
|||||||
func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) {
|
func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) {
|
||||||
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
|
||||||
|
|
||||||
root := repl.ReplaceAll(m.Root, "")
|
root := repl.ReplaceAll(m.Root, ".")
|
||||||
|
|
||||||
// if list of files to try was omitted entirely,
|
// if list of files to try was omitted entirely,
|
||||||
// assume URL path
|
// assume URL path
|
||||||
|
@ -36,10 +36,7 @@ import (
|
|||||||
func init() {
|
func init() {
|
||||||
weakrand.Seed(time.Now().UnixNano())
|
weakrand.Seed(time.Now().UnixNano())
|
||||||
|
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(FileServer{})
|
||||||
Name: "http.handlers.file_server",
|
|
||||||
New: func() interface{} { return new(FileServer) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileServer implements a static file server responder for Caddy.
|
// FileServer implements a static file server responder for Caddy.
|
||||||
@ -48,8 +45,14 @@ type FileServer struct {
|
|||||||
Hide []string `json:"hide,omitempty"`
|
Hide []string `json:"hide,omitempty"`
|
||||||
IndexNames []string `json:"index_names,omitempty"`
|
IndexNames []string `json:"index_names,omitempty"`
|
||||||
Browse *Browse `json:"browse,omitempty"`
|
Browse *Browse `json:"browse,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Content negotiation
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (FileServer) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.handlers.file_server",
|
||||||
|
New: func() caddy.Module { return new(FileServer) },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision sets up the static files responder.
|
// Provision sets up the static files responder.
|
||||||
@ -83,7 +86,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, _ cadd
|
|||||||
|
|
||||||
filesToHide := fsrv.transformHidePaths(repl)
|
filesToHide := fsrv.transformHidePaths(repl)
|
||||||
|
|
||||||
root := repl.ReplaceAll(fsrv.Root, "")
|
root := repl.ReplaceAll(fsrv.Root, ".")
|
||||||
suffix := repl.ReplaceAll(r.URL.Path, "")
|
suffix := repl.ReplaceAll(r.URL.Path, "")
|
||||||
filename := sanitizedPathJoin(root, suffix)
|
filename := sanitizedPathJoin(root, suffix)
|
||||||
|
|
||||||
@ -302,7 +305,7 @@ func calculateEtag(d os.FileInfo) string {
|
|||||||
return `"` + t + s + `"`
|
return `"` + t + s + `"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultIndexNames = []string{"index.html"}
|
var defaultIndexNames = []string{"index.html", "index.txt"}
|
||||||
|
|
||||||
var bufPool = sync.Pool{
|
var bufPool = sync.Pool{
|
||||||
New: func() interface{} {
|
New: func() interface{} {
|
||||||
|
91
modules/caddyhttp/headers/caddyfile.go
Normal file
91
modules/caddyhttp/headers/caddyfile.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -23,10 +23,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(Headers{})
|
||||||
Name: "http.handlers.headers",
|
|
||||||
New: func() interface{} { return new(Headers) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Headers is a middleware which can mutate HTTP headers.
|
// Headers is a middleware which can mutate HTTP headers.
|
||||||
@ -35,6 +32,14 @@ type Headers struct {
|
|||||||
Response *RespHeaderOps `json:"response,omitempty"`
|
Response *RespHeaderOps `json:"response,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (Headers) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.handlers.headers",
|
||||||
|
New: func() caddy.Module { return new(Headers) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// HeaderOps defines some operations to
|
// HeaderOps defines some operations to
|
||||||
// perform on HTTP headers.
|
// perform on HTTP headers.
|
||||||
type HeaderOps struct {
|
type HeaderOps struct {
|
||||||
|
@ -28,16 +28,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(Markdown{})
|
||||||
Name: "http.handlers.markdown",
|
|
||||||
New: func() interface{} { return new(Markdown) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Markdown is a middleware for rendering a Markdown response body.
|
// Markdown is a middleware for rendering a Markdown response body.
|
||||||
type Markdown struct {
|
type Markdown struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (Markdown) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.handlers.markdown",
|
||||||
|
New: func() caddy.Module { return new(Markdown) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
func (m Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
||||||
buf := bufPool.Get().(*bytes.Buffer)
|
buf := bufPool.Get().(*bytes.Buffer)
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
|
@ -28,6 +28,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/pkg/caddyscript"
|
"github.com/caddyserver/caddy/v2/pkg/caddyscript"
|
||||||
"go.starlark.net/starlark"
|
"go.starlark.net/starlark"
|
||||||
)
|
)
|
||||||
@ -79,50 +80,31 @@ type (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(MatchHost{})
|
||||||
|
caddy.RegisterModule(MatchPath{})
|
||||||
|
caddy.RegisterModule(MatchPathRE{})
|
||||||
|
caddy.RegisterModule(MatchMethod{})
|
||||||
|
caddy.RegisterModule(MatchQuery{})
|
||||||
|
caddy.RegisterModule(MatchHeader{})
|
||||||
|
caddy.RegisterModule(MatchHeaderRE{})
|
||||||
|
caddy.RegisterModule(new(MatchProtocol))
|
||||||
|
caddy.RegisterModule(MatchRemoteIP{})
|
||||||
|
caddy.RegisterModule(MatchNegate{})
|
||||||
|
caddy.RegisterModule(new(MatchStarlarkExpr))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (MatchHost) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
Name: "http.matchers.host",
|
Name: "http.matchers.host",
|
||||||
New: func() interface{} { return new(MatchHost) },
|
New: func() caddy.Module { return new(MatchHost) },
|
||||||
})
|
}
|
||||||
caddy.RegisterModule(caddy.Module{
|
}
|
||||||
Name: "http.matchers.path",
|
|
||||||
New: func() interface{} { return new(MatchPath) },
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
||||||
})
|
func (m *MatchHost) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
caddy.RegisterModule(caddy.Module{
|
*m = d.RemainingArgs()
|
||||||
Name: "http.matchers.path_regexp",
|
return nil
|
||||||
New: func() interface{} { return new(MatchPathRE) },
|
|
||||||
})
|
|
||||||
caddy.RegisterModule(caddy.Module{
|
|
||||||
Name: "http.matchers.method",
|
|
||||||
New: func() interface{} { return new(MatchMethod) },
|
|
||||||
})
|
|
||||||
caddy.RegisterModule(caddy.Module{
|
|
||||||
Name: "http.matchers.query",
|
|
||||||
New: func() interface{} { return new(MatchQuery) },
|
|
||||||
})
|
|
||||||
caddy.RegisterModule(caddy.Module{
|
|
||||||
Name: "http.matchers.header",
|
|
||||||
New: func() interface{} { return new(MatchHeader) },
|
|
||||||
})
|
|
||||||
caddy.RegisterModule(caddy.Module{
|
|
||||||
Name: "http.matchers.header_regexp",
|
|
||||||
New: func() interface{} { return new(MatchHeaderRE) },
|
|
||||||
})
|
|
||||||
caddy.RegisterModule(caddy.Module{
|
|
||||||
Name: "http.matchers.protocol",
|
|
||||||
New: func() interface{} { return new(MatchProtocol) },
|
|
||||||
})
|
|
||||||
caddy.RegisterModule(caddy.Module{
|
|
||||||
Name: "http.matchers.remote_ip",
|
|
||||||
New: func() interface{} { return new(MatchRemoteIP) },
|
|
||||||
})
|
|
||||||
caddy.RegisterModule(caddy.Module{
|
|
||||||
Name: "http.matchers.not",
|
|
||||||
New: func() interface{} { return new(MatchNegate) },
|
|
||||||
})
|
|
||||||
caddy.RegisterModule(caddy.Module{
|
|
||||||
Name: "http.matchers.starlark_expr",
|
|
||||||
New: func() interface{} { return new(MatchStarlarkExpr) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match returns true if r matches m.
|
// Match returns true if r matches m.
|
||||||
@ -158,6 +140,14 @@ outer:
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (MatchPath) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.matchers.path",
|
||||||
|
New: func() caddy.Module { return new(MatchPath) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Match returns true if r matches m.
|
// Match returns true if r matches m.
|
||||||
func (m MatchPath) Match(r *http.Request) bool {
|
func (m MatchPath) Match(r *http.Request) bool {
|
||||||
for _, matchPath := range m {
|
for _, matchPath := range m {
|
||||||
@ -177,12 +167,44 @@ func (m MatchPath) Match(r *http.Request) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
||||||
|
func (m *MatchPath) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
for d.Next() {
|
||||||
|
*m = d.RemainingArgs()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (MatchPathRE) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.matchers.path_regexp",
|
||||||
|
New: func() caddy.Module { return new(MatchPathRE) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Match returns true if r matches m.
|
// Match returns true if r matches m.
|
||||||
func (m MatchPathRE) Match(r *http.Request) bool {
|
func (m MatchPathRE) Match(r *http.Request) bool {
|
||||||
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
|
||||||
return m.MatchRegexp.Match(r.URL.Path, repl, "path_regexp")
|
return m.MatchRegexp.Match(r.URL.Path, repl, "path_regexp")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (MatchMethod) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.matchers.method",
|
||||||
|
New: func() caddy.Module { return new(MatchMethod) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
||||||
|
func (m *MatchMethod) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
for d.Next() {
|
||||||
|
*m = d.RemainingArgs()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Match returns true if r matches m.
|
// Match returns true if r matches m.
|
||||||
func (m MatchMethod) Match(r *http.Request) bool {
|
func (m MatchMethod) Match(r *http.Request) bool {
|
||||||
for _, method := range m {
|
for _, method := range m {
|
||||||
@ -193,6 +215,26 @@ func (m MatchMethod) Match(r *http.Request) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (MatchQuery) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.matchers.query",
|
||||||
|
New: func() caddy.Module { return new(MatchQuery) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
||||||
|
func (m *MatchQuery) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
for d.Next() {
|
||||||
|
parts := strings.SplitN(d.Val(), "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return d.Errf("malformed query matcher token: %s; must be in param=val format", d.Val())
|
||||||
|
}
|
||||||
|
url.Values(*m).Set(parts[0], parts[1])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Match returns true if r matches m.
|
// Match returns true if r matches m.
|
||||||
func (m MatchQuery) Match(r *http.Request) bool {
|
func (m MatchQuery) Match(r *http.Request) bool {
|
||||||
for param, vals := range m {
|
for param, vals := range m {
|
||||||
@ -206,6 +248,26 @@ func (m MatchQuery) Match(r *http.Request) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (MatchHeader) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.matchers.header",
|
||||||
|
New: func() caddy.Module { return new(MatchHeader) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
||||||
|
func (m *MatchHeader) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
for d.Next() {
|
||||||
|
var field, val string
|
||||||
|
if !d.Args(&field, &val) {
|
||||||
|
return d.Errf("expected both field and value")
|
||||||
|
}
|
||||||
|
http.Header(*m).Set(field, val)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Match returns true if r matches m.
|
// Match returns true if r matches m.
|
||||||
func (m MatchHeader) Match(r *http.Request) bool {
|
func (m MatchHeader) Match(r *http.Request) bool {
|
||||||
for field, allowedFieldVals := range m {
|
for field, allowedFieldVals := range m {
|
||||||
@ -227,6 +289,29 @@ func (m MatchHeader) Match(r *http.Request) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (MatchHeaderRE) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.matchers.header_regexp",
|
||||||
|
New: func() caddy.Module { return new(MatchHeaderRE) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
||||||
|
func (m *MatchHeaderRE) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
if *m == nil {
|
||||||
|
*m = make(map[string]*MatchRegexp)
|
||||||
|
}
|
||||||
|
for d.Next() {
|
||||||
|
var field, val string
|
||||||
|
if !d.Args(&field, &val) {
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
(*m)[field] = &MatchRegexp{Pattern: val}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Match returns true if r matches m.
|
// Match returns true if r matches m.
|
||||||
func (m MatchHeaderRE) Match(r *http.Request) bool {
|
func (m MatchHeaderRE) Match(r *http.Request) bool {
|
||||||
for field, rm := range m {
|
for field, rm := range m {
|
||||||
@ -261,6 +346,14 @@ func (m MatchHeaderRE) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (MatchProtocol) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.matchers.protocol",
|
||||||
|
New: func() caddy.Module { return new(MatchProtocol) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Match returns true if r matches m.
|
// Match returns true if r matches m.
|
||||||
func (m MatchProtocol) Match(r *http.Request) bool {
|
func (m MatchProtocol) Match(r *http.Request) bool {
|
||||||
switch string(m) {
|
switch string(m) {
|
||||||
@ -274,6 +367,26 @@ func (m MatchProtocol) Match(r *http.Request) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
||||||
|
func (m *MatchProtocol) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
for d.Next() {
|
||||||
|
var proto string
|
||||||
|
if !d.Args(&proto) {
|
||||||
|
return d.Err("expected exactly one protocol")
|
||||||
|
}
|
||||||
|
*m = MatchProtocol(proto)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (MatchNegate) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.matchers.not",
|
||||||
|
New: func() caddy.Module { return new(MatchNegate) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// UnmarshalJSON unmarshals data into m's unexported map field.
|
// UnmarshalJSON unmarshals data into m's unexported map field.
|
||||||
// This is done because we cannot embed the map directly into
|
// This is done because we cannot embed the map directly into
|
||||||
// the struct, but we need a struct because we need another
|
// the struct, but we need a struct because we need another
|
||||||
@ -282,6 +395,12 @@ func (m *MatchNegate) UnmarshalJSON(data []byte) error {
|
|||||||
return json.Unmarshal(data, &m.matchersRaw)
|
return json.Unmarshal(data, &m.matchersRaw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
||||||
|
func (m *MatchNegate) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
// TODO: figure out how this will work
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Provision loads the matcher modules to be negated.
|
// Provision loads the matcher modules to be negated.
|
||||||
func (m *MatchNegate) Provision(ctx caddy.Context) error {
|
func (m *MatchNegate) Provision(ctx caddy.Context) error {
|
||||||
for modName, rawMsg := range m.matchersRaw {
|
for modName, rawMsg := range m.matchersRaw {
|
||||||
@ -301,6 +420,22 @@ func (m MatchNegate) Match(r *http.Request) bool {
|
|||||||
return !m.matchers.Match(r)
|
return !m.matchers.Match(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.matchers.remote_ip",
|
||||||
|
New: func() caddy.Module { return new(MatchRemoteIP) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
||||||
|
func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
for d.Next() {
|
||||||
|
m.Ranges = d.RemainingArgs()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Provision parses m's IP ranges, either from IP or CIDR expressions.
|
// Provision parses m's IP ranges, either from IP or CIDR expressions.
|
||||||
func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
|
func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
|
||||||
for _, str := range m.Ranges {
|
for _, str := range m.Ranges {
|
||||||
@ -362,6 +497,14 @@ func (m MatchRemoteIP) Match(r *http.Request) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (MatchStarlarkExpr) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.matchers.starlark_expr", // TODO: Rename to 'starlark'?
|
||||||
|
New: func() caddy.Module { return new(MatchStarlarkExpr) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Match returns true if r matches m.
|
// Match returns true if r matches m.
|
||||||
func (m MatchStarlarkExpr) Match(r *http.Request) bool {
|
func (m MatchStarlarkExpr) Match(r *http.Request) bool {
|
||||||
input := string(m)
|
input := string(m)
|
||||||
@ -379,7 +522,7 @@ func (m MatchStarlarkExpr) Match(r *http.Request) bool {
|
|||||||
// MatchRegexp is an embeddable type for matching
|
// MatchRegexp is an embeddable type for matching
|
||||||
// using regular expressions.
|
// using regular expressions.
|
||||||
type MatchRegexp struct {
|
type MatchRegexp struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name,omitempty"`
|
||||||
Pattern string `json:"pattern"`
|
Pattern string `json:"pattern"`
|
||||||
compiled *regexp.Regexp
|
compiled *regexp.Regexp
|
||||||
}
|
}
|
||||||
@ -431,6 +574,23 @@ func (mre *MatchRegexp) Match(input string, repl caddy.Replacer, scope string) b
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
||||||
|
func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
for d.Next() {
|
||||||
|
args := d.RemainingArgs()
|
||||||
|
switch len(args) {
|
||||||
|
case 1:
|
||||||
|
mre.Pattern = args[0]
|
||||||
|
case 2:
|
||||||
|
mre.Name = args[0]
|
||||||
|
mre.Pattern = args[1]
|
||||||
|
default:
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ResponseMatcher is a type which can determine if a given response
|
// ResponseMatcher is a type which can determine if a given response
|
||||||
// status code and its headers match some criteria.
|
// status code and its headers match some criteria.
|
||||||
type ResponseMatcher struct {
|
type ResponseMatcher struct {
|
||||||
@ -506,4 +666,14 @@ var (
|
|||||||
_ caddy.Provisioner = (*MatchNegate)(nil)
|
_ caddy.Provisioner = (*MatchNegate)(nil)
|
||||||
_ RequestMatcher = (*MatchStarlarkExpr)(nil)
|
_ RequestMatcher = (*MatchStarlarkExpr)(nil)
|
||||||
_ caddy.Provisioner = (*MatchRegexp)(nil)
|
_ caddy.Provisioner = (*MatchRegexp)(nil)
|
||||||
|
|
||||||
|
_ caddyfile.Unmarshaler = (*MatchHost)(nil)
|
||||||
|
_ caddyfile.Unmarshaler = (*MatchPath)(nil)
|
||||||
|
_ caddyfile.Unmarshaler = (*MatchPathRE)(nil)
|
||||||
|
_ caddyfile.Unmarshaler = (*MatchMethod)(nil)
|
||||||
|
_ caddyfile.Unmarshaler = (*MatchQuery)(nil)
|
||||||
|
_ caddyfile.Unmarshaler = (*MatchHeader)(nil)
|
||||||
|
_ caddyfile.Unmarshaler = (*MatchHeaderRE)(nil)
|
||||||
|
_ caddyfile.Unmarshaler = (*MatchProtocol)(nil)
|
||||||
|
_ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
|
||||||
)
|
)
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
package caddyhttp
|
package caddyhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
@ -28,6 +29,7 @@ import (
|
|||||||
func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.ResponseWriter) {
|
func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.ResponseWriter) {
|
||||||
httpVars := func(key string) (string, bool) {
|
httpVars := func(key string) (string, bool) {
|
||||||
if req != nil {
|
if req != nil {
|
||||||
|
// query string parameters
|
||||||
if strings.HasPrefix(key, queryReplPrefix) {
|
if strings.HasPrefix(key, queryReplPrefix) {
|
||||||
vals := req.URL.Query()[key[len(queryReplPrefix):]]
|
vals := req.URL.Query()[key[len(queryReplPrefix):]]
|
||||||
// always return true, since the query param might
|
// always return true, since the query param might
|
||||||
@ -35,6 +37,7 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon
|
|||||||
return strings.Join(vals, ","), true
|
return strings.Join(vals, ","), true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// request header fields
|
||||||
if strings.HasPrefix(key, reqHeaderReplPrefix) {
|
if strings.HasPrefix(key, reqHeaderReplPrefix) {
|
||||||
field := key[len(reqHeaderReplPrefix):]
|
field := key[len(reqHeaderReplPrefix):]
|
||||||
vals := req.Header[textproto.CanonicalMIMEHeaderKey(field)]
|
vals := req.Header[textproto.CanonicalMIMEHeaderKey(field)]
|
||||||
@ -43,6 +46,7 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon
|
|||||||
return strings.Join(vals, ","), true
|
return strings.Join(vals, ","), true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cookies
|
||||||
if strings.HasPrefix(key, cookieReplPrefix) {
|
if strings.HasPrefix(key, cookieReplPrefix) {
|
||||||
name := key[len(cookieReplPrefix):]
|
name := key[len(cookieReplPrefix):]
|
||||||
for _, cookie := range req.Cookies() {
|
for _, cookie := range req.Cookies() {
|
||||||
@ -87,14 +91,7 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon
|
|||||||
return req.URL.RawQuery, true
|
return req.URL.RawQuery, true
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(key, respHeaderReplPrefix) {
|
// hostname labels
|
||||||
field := key[len(respHeaderReplPrefix):]
|
|
||||||
vals := w.Header()[textproto.CanonicalMIMEHeaderKey(field)]
|
|
||||||
// always return true, since the header field might
|
|
||||||
// be present only in some requests
|
|
||||||
return strings.Join(vals, ","), true
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(key, hostLabelReplPrefix) {
|
if strings.HasPrefix(key, hostLabelReplPrefix) {
|
||||||
idxStr := key[len(hostLabelReplPrefix):]
|
idxStr := key[len(hostLabelReplPrefix):]
|
||||||
idx, err := strconv.Atoi(idxStr)
|
idx, err := strconv.Atoi(idxStr)
|
||||||
@ -111,6 +108,7 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon
|
|||||||
return hostLabels[idx], true
|
return hostLabels[idx], true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// path parts
|
||||||
if strings.HasPrefix(key, pathPartsReplPrefix) {
|
if strings.HasPrefix(key, pathPartsReplPrefix) {
|
||||||
idxStr := key[len(pathPartsReplPrefix):]
|
idxStr := key[len(pathPartsReplPrefix):]
|
||||||
idx, err := strconv.Atoi(idxStr)
|
idx, err := strconv.Atoi(idxStr)
|
||||||
@ -129,9 +127,31 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon
|
|||||||
}
|
}
|
||||||
return pathParts[idx], true
|
return pathParts[idx], true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// middleware variables
|
||||||
|
if strings.HasPrefix(key, varsReplPrefix) {
|
||||||
|
varName := key[len(varsReplPrefix):]
|
||||||
|
tbl := req.Context().Value(VarCtxKey).(map[string]interface{})
|
||||||
|
raw, ok := tbl[varName]
|
||||||
|
if !ok {
|
||||||
|
// variables can be dynamic, so always return true
|
||||||
|
// even when it may not be set; treat as empty
|
||||||
|
return "", true
|
||||||
|
}
|
||||||
|
// do our best to convert it to a string efficiently
|
||||||
|
switch val := raw.(type) {
|
||||||
|
case string:
|
||||||
|
return val, true
|
||||||
|
case fmt.Stringer:
|
||||||
|
return val.String(), true
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%s", val), true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if w != nil {
|
if w != nil {
|
||||||
|
// response header fields
|
||||||
if strings.HasPrefix(key, respHeaderReplPrefix) {
|
if strings.HasPrefix(key, respHeaderReplPrefix) {
|
||||||
field := key[len(respHeaderReplPrefix):]
|
field := key[len(respHeaderReplPrefix):]
|
||||||
vals := w.Header()[textproto.CanonicalMIMEHeaderKey(field)]
|
vals := w.Header()[textproto.CanonicalMIMEHeaderKey(field)]
|
||||||
@ -153,5 +173,6 @@ const (
|
|||||||
cookieReplPrefix = "http.request.cookie."
|
cookieReplPrefix = "http.request.cookie."
|
||||||
hostLabelReplPrefix = "http.request.host.labels."
|
hostLabelReplPrefix = "http.request.host.labels."
|
||||||
pathPartsReplPrefix = "http.request.uri.path."
|
pathPartsReplPrefix = "http.request.uri.path."
|
||||||
|
varsReplPrefix = "http.var."
|
||||||
respHeaderReplPrefix = "http.response.header."
|
respHeaderReplPrefix = "http.response.header."
|
||||||
)
|
)
|
||||||
|
@ -22,10 +22,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(RequestBody{})
|
||||||
Name: "http.handlers.request_body",
|
|
||||||
New: func() interface{} { return new(RequestBody) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequestBody is a middleware for manipulating the request body.
|
// RequestBody is a middleware for manipulating the request body.
|
||||||
@ -33,6 +30,14 @@ type RequestBody struct {
|
|||||||
MaxSize int64 `json:"max_size,omitempty"`
|
MaxSize int64 `json:"max_size,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (RequestBody) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.handlers.request_body", // TODO: better name for this?
|
||||||
|
New: func() caddy.Module { return new(RequestBody) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (rb RequestBody) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
func (rb RequestBody) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
||||||
if r.Body == nil {
|
if r.Body == nil {
|
||||||
return next.ServeHTTP(w, r)
|
return next.ServeHTTP(w, r)
|
||||||
|
@ -16,12 +16,38 @@ package reverseproxy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register caddy module.
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(new(LoadBalanced))
|
||||||
Name: "http.handlers.reverse_proxy",
|
httpcaddyfile.RegisterHandlerDirective("reverse_proxy", parseCaddyfile) // TODO: "proxy"?
|
||||||
New: func() interface{} { return new(LoadBalanced) },
|
}
|
||||||
})
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (*LoadBalanced) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.handlers.reverse_proxy",
|
||||||
|
New: func() caddy.Module { return new(LoadBalanced) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax:
|
||||||
|
//
|
||||||
|
// proxy [<matcher>] <to>
|
||||||
|
//
|
||||||
|
// TODO: This needs to be finished. It definitely needs to be able to open a block...
|
||||||
|
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||||
|
lb := new(LoadBalanced)
|
||||||
|
for h.Next() {
|
||||||
|
allTo := h.RemainingArgs()
|
||||||
|
if len(allTo) == 0 {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
|
for _, to := range allTo {
|
||||||
|
lb.Upstreams = append(lb.Upstreams, &UpstreamConfig{Host: to})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lb, nil
|
||||||
}
|
}
|
||||||
|
@ -179,21 +179,21 @@ type LoadBalanced struct {
|
|||||||
|
|
||||||
// The following struct fields are set by caddy configuration.
|
// The following struct fields are set by caddy configuration.
|
||||||
// TryInterval is the max duration for which request retrys will be performed for a request.
|
// TryInterval is the max duration for which request retrys will be performed for a request.
|
||||||
TryInterval string `json:"try_interval"`
|
TryInterval string `json:"try_interval,omitempty"`
|
||||||
|
|
||||||
// Upstreams are the configs for upstream hosts
|
// Upstreams are the configs for upstream hosts
|
||||||
Upstreams []*UpstreamConfig `json:"upstreams"`
|
Upstreams []*UpstreamConfig `json:"upstreams,omitempty"`
|
||||||
|
|
||||||
// LoadBalanceType is the string representation of what loadbalancing algorithm to use. i.e. "random" or "round_robin".
|
// LoadBalanceType is the string representation of what loadbalancing algorithm to use. i.e. "random" or "round_robin".
|
||||||
LoadBalanceType string `json:"load_balance_type"`
|
LoadBalanceType string `json:"load_balance_type,omitempty"`
|
||||||
|
|
||||||
// NoHealthyUpstreamsMessage is returned as a response when there are no healthy upstreams to loadbalance to.
|
// NoHealthyUpstreamsMessage is returned as a response when there are no healthy upstreams to loadbalance to.
|
||||||
NoHealthyUpstreamsMessage string `json:"no_healthy_upstreams_message"`
|
NoHealthyUpstreamsMessage string `json:"no_healthy_upstreams_message,omitempty"`
|
||||||
|
|
||||||
// TODO :- store healthcheckers as package level state where each upstream gets a single healthchecker
|
// TODO :- store healthcheckers as package level state where each upstream gets a single healthchecker
|
||||||
// currently a healthchecker is created for each upstream defined, even if a healthchecker was previously created
|
// currently a healthchecker is created for each upstream defined, even if a healthchecker was previously created
|
||||||
// for that upstream
|
// for that upstream
|
||||||
HealthCheckers []*HealthChecker
|
HealthCheckers []*HealthChecker `json:"health_checkers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup stops all health checkers on a loadbalanced reverse proxy.
|
// Cleanup stops all health checkers on a loadbalanced reverse proxy.
|
||||||
@ -320,22 +320,22 @@ func (lb *LoadBalanced) random() *upstream {
|
|||||||
// UpstreamConfig represents the config of an upstream.
|
// UpstreamConfig represents the config of an upstream.
|
||||||
type UpstreamConfig struct {
|
type UpstreamConfig struct {
|
||||||
// Host is the host name of the upstream server.
|
// Host is the host name of the upstream server.
|
||||||
Host string `json:"host"`
|
Host string `json:"host,omitempty"`
|
||||||
|
|
||||||
// FastHealthCheckDuration is the duration for which a health check is performed when a node is considered unhealthy.
|
// FastHealthCheckDuration is the duration for which a health check is performed when a node is considered unhealthy.
|
||||||
FastHealthCheckDuration string `json:"fast_health_check_duration"`
|
FastHealthCheckDuration string `json:"fast_health_check_duration,omitempty"`
|
||||||
|
|
||||||
CircuitBreaker json.RawMessage `json:"circuit_breaker"`
|
CircuitBreaker json.RawMessage `json:"circuit_breaker,omitempty"`
|
||||||
|
|
||||||
// // CircuitBreakerConfig is the config passed to setup a circuit breaker.
|
// // CircuitBreakerConfig is the config passed to setup a circuit breaker.
|
||||||
// CircuitBreakerConfig *circuitbreaker.Config `json:"circuit_breaker"`
|
// CircuitBreakerConfig *circuitbreaker.Config `json:"circuit_breaker,omitempty"`
|
||||||
circuitbreaker CircuitBreaker
|
circuitbreaker CircuitBreaker
|
||||||
|
|
||||||
// HealthCheckDuration is the default duration for which a health check is performed.
|
// HealthCheckDuration is the default duration for which a health check is performed.
|
||||||
HealthCheckDuration string `json:"health_check_duration"`
|
HealthCheckDuration string `json:"health_check_duration,omitempty"`
|
||||||
|
|
||||||
// HealthCheckPath is the path at the upstream host to use for healthchecks.
|
// HealthCheckPath is the path at the upstream host to use for healthchecks.
|
||||||
HealthCheckPath string `json:"health_check_path"`
|
HealthCheckPath string `json:"health_check_path,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// upstream represents an upstream host.
|
// upstream represents an upstream host.
|
||||||
|
37
modules/caddyhttp/rewrite/caddyfile.go
Normal file
37
modules/caddyhttp/rewrite/caddyfile.go
Normal file
@ -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
|
||||||
|
}
|
@ -24,10 +24,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(Rewrite{})
|
||||||
Name: "http.handlers.rewrite",
|
|
||||||
New: func() interface{} { return new(Rewrite) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewrite is a middleware which can rewrite HTTP requests.
|
// Rewrite is a middleware which can rewrite HTTP requests.
|
||||||
@ -37,6 +34,14 @@ type Rewrite struct {
|
|||||||
Rehandle bool `json:"rehandle,omitempty"`
|
Rehandle bool `json:"rehandle,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (Rewrite) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.handlers.rewrite",
|
||||||
|
New: func() caddy.Module { return new(Rewrite) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
||||||
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
|
||||||
var rehandleNeeded bool
|
var rehandleNeeded bool
|
||||||
|
@ -22,37 +22,38 @@ import (
|
|||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServerRoute represents a set of matching rules,
|
// Route represents a set of matching rules,
|
||||||
// middlewares, and a responder for handling HTTP
|
// middlewares, and a responder for handling HTTP
|
||||||
// requests.
|
// requests.
|
||||||
type ServerRoute struct {
|
type Route struct {
|
||||||
Group string `json:"group,omitempty"`
|
Group string `json:"group,omitempty"`
|
||||||
MatcherSets []map[string]json.RawMessage `json:"match,omitempty"`
|
MatcherSetsRaw []map[string]json.RawMessage `json:"match,omitempty"`
|
||||||
Handle []json.RawMessage `json:"handle,omitempty"`
|
HandlersRaw []json.RawMessage `json:"handle,omitempty"`
|
||||||
Terminal bool `json:"terminal,omitempty"`
|
Terminal bool `json:"terminal,omitempty"`
|
||||||
|
|
||||||
// decoded values
|
// decoded values
|
||||||
matcherSets []MatcherSet
|
MatcherSets []MatcherSet `json:"-"`
|
||||||
handlers []MiddlewareHandler
|
Handlers []MiddlewareHandler `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty returns true if the route has all zero/default values.
|
// Empty returns true if the route has all zero/default values.
|
||||||
func (sr ServerRoute) Empty() bool {
|
func (r Route) Empty() bool {
|
||||||
return len(sr.MatcherSets) == 0 &&
|
return len(r.MatcherSetsRaw) == 0 &&
|
||||||
len(sr.Handle) == 0 &&
|
len(r.MatcherSets) == 0 &&
|
||||||
len(sr.handlers) == 0 &&
|
len(r.HandlersRaw) == 0 &&
|
||||||
!sr.Terminal &&
|
len(r.Handlers) == 0 &&
|
||||||
sr.Group == ""
|
!r.Terminal &&
|
||||||
|
r.Group == ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sr ServerRoute) anyMatcherSetMatches(r *http.Request) bool {
|
func (r Route) anyMatcherSetMatches(req *http.Request) bool {
|
||||||
for _, ms := range sr.matcherSets {
|
for _, ms := range r.MatcherSets {
|
||||||
if ms.Match(r) {
|
if ms.Match(req) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if no matchers, always match
|
// if no matchers, always match
|
||||||
return len(sr.matcherSets) == 0
|
return len(r.MatcherSets) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatcherSet is a set of matchers which
|
// MatcherSet is a set of matchers which
|
||||||
@ -73,13 +74,13 @@ func (mset MatcherSet) Match(r *http.Request) bool {
|
|||||||
|
|
||||||
// RouteList is a list of server routes that can
|
// RouteList is a list of server routes that can
|
||||||
// create a middleware chain.
|
// create a middleware chain.
|
||||||
type RouteList []ServerRoute
|
type RouteList []Route
|
||||||
|
|
||||||
// Provision sets up all the routes by loading the modules.
|
// Provision sets up all the routes by loading the modules.
|
||||||
func (routes RouteList) Provision(ctx caddy.Context) error {
|
func (routes RouteList) Provision(ctx caddy.Context) error {
|
||||||
for i, route := range routes {
|
for i, route := range routes {
|
||||||
// matchers
|
// matchers
|
||||||
for _, matcherSet := range route.MatcherSets {
|
for _, matcherSet := range route.MatcherSetsRaw {
|
||||||
var matchers MatcherSet
|
var matchers MatcherSet
|
||||||
for modName, rawMsg := range matcherSet {
|
for modName, rawMsg := range matcherSet {
|
||||||
val, err := ctx.LoadModule("http.matchers."+modName, rawMsg)
|
val, err := ctx.LoadModule("http.matchers."+modName, rawMsg)
|
||||||
@ -88,19 +89,19 @@ func (routes RouteList) Provision(ctx caddy.Context) error {
|
|||||||
}
|
}
|
||||||
matchers = append(matchers, val.(RequestMatcher))
|
matchers = append(matchers, val.(RequestMatcher))
|
||||||
}
|
}
|
||||||
routes[i].matcherSets = append(routes[i].matcherSets, matchers)
|
routes[i].MatcherSets = append(routes[i].MatcherSets, matchers)
|
||||||
}
|
}
|
||||||
routes[i].MatcherSets = nil // allow GC to deallocate - TODO: Does this help?
|
routes[i].MatcherSetsRaw = nil // allow GC to deallocate - TODO: Does this help?
|
||||||
|
|
||||||
// handlers
|
// handlers
|
||||||
for j, rawMsg := range route.Handle {
|
for j, rawMsg := range route.HandlersRaw {
|
||||||
mh, err := ctx.LoadModuleInline("handler", "http.handlers", rawMsg)
|
mh, err := ctx.LoadModuleInline("handler", "http.handlers", rawMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("loading handler module in position %d: %v", j, err)
|
return fmt.Errorf("loading handler module in position %d: %v", j, err)
|
||||||
}
|
}
|
||||||
routes[i].handlers = append(routes[i].handlers, mh.(MiddlewareHandler))
|
routes[i].Handlers = append(routes[i].Handlers, mh.(MiddlewareHandler))
|
||||||
}
|
}
|
||||||
routes[i].Handle = nil // allow GC to deallocate - TODO: Does this help?
|
routes[i].HandlersRaw = nil // allow GC to deallocate - TODO: Does this help?
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -135,7 +136,7 @@ func (routes RouteList) BuildCompositeRoute(req *http.Request) Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// apply the rest of the route
|
// apply the rest of the route
|
||||||
for _, mh := range route.handlers {
|
for _, mh := range route.Handlers {
|
||||||
// we have to be sure to wrap mh outside
|
// we have to be sure to wrap mh outside
|
||||||
// of our current stack frame so that the
|
// of our current stack frame so that the
|
||||||
// reference to this mh isn't overwritten
|
// reference to this mh isn't overwritten
|
||||||
|
@ -57,7 +57,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
repl := caddy.NewReplacer()
|
repl := caddy.NewReplacer()
|
||||||
ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
|
ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
|
||||||
ctx = context.WithValue(ctx, ServerCtxKey, s)
|
ctx = context.WithValue(ctx, ServerCtxKey, s)
|
||||||
ctx = context.WithValue(ctx, TableCtxKey, make(map[string]interface{})) // TODO: Implement this
|
ctx = context.WithValue(ctx, VarCtxKey, make(map[string]interface{}))
|
||||||
r = r.WithContext(ctx)
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
// once the pointer to the request won't change
|
// once the pointer to the request won't change
|
||||||
@ -201,6 +201,14 @@ type AutoHTTPSConfig struct {
|
|||||||
// that certificates will not be provisioned and managed
|
// that certificates will not be provisioned and managed
|
||||||
// for these names.
|
// for these names.
|
||||||
SkipCerts []string `json:"skip_certificates,omitempty"`
|
SkipCerts []string `json:"skip_certificates,omitempty"`
|
||||||
|
|
||||||
|
// By default, automatic HTTPS will obtain and renew
|
||||||
|
// certificates for qualifying hostnames. However, if
|
||||||
|
// a certificate with a matching SAN is already loaded
|
||||||
|
// into the cache, certificate management will not be
|
||||||
|
// enabled. To force automated certificate management
|
||||||
|
// regardless of loaded certificates, set this to true.
|
||||||
|
IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skipped returns true if name is in skipSlice, which
|
// Skipped returns true if name is in skipSlice, which
|
||||||
@ -225,6 +233,6 @@ const (
|
|||||||
// For referencing the server instance
|
// For referencing the server instance
|
||||||
ServerCtxKey caddy.CtxKey = "server"
|
ServerCtxKey caddy.CtxKey = "server"
|
||||||
|
|
||||||
// For the request's variable table (TODO: implement this)
|
// For the request's variable table
|
||||||
TableCtxKey caddy.CtxKey = "table"
|
VarCtxKey caddy.CtxKey = "vars"
|
||||||
)
|
)
|
||||||
|
@ -18,22 +18,26 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(StaticError{})
|
||||||
Name: "http.handlers.error",
|
|
||||||
New: func() interface{} { return new(StaticError) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StaticError implements a simple handler that returns an error.
|
// StaticError implements a simple handler that returns an error.
|
||||||
type StaticError struct {
|
type StaticError struct {
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
StatusCode weakString `json:"status_code,omitempty"`
|
StatusCode WeakString `json:"status_code,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (StaticError) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.handlers.error",
|
||||||
|
New: func() caddy.Module { return new(StaticError) },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e StaticError) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error {
|
func (e StaticError) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error {
|
||||||
@ -53,43 +57,3 @@ func (e StaticError) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler
|
|||||||
|
|
||||||
// Interface guard
|
// Interface guard
|
||||||
var _ MiddlewareHandler = (*StaticError)(nil)
|
var _ MiddlewareHandler = (*StaticError)(nil)
|
||||||
|
|
||||||
// weakString is a type that unmarshals any JSON value
|
|
||||||
// as a string literal, and provides methods for
|
|
||||||
// getting the value as different primitive types.
|
|
||||||
// However, using this type removes any type safety
|
|
||||||
// as far as deserializing JSON is concerned.
|
|
||||||
type weakString string
|
|
||||||
|
|
||||||
// UnmarshalJSON satisfies json.Unmarshaler. It
|
|
||||||
// unmarshals b by always interpreting it as a
|
|
||||||
// string literal.
|
|
||||||
func (ws *weakString) UnmarshalJSON(b []byte) error {
|
|
||||||
*ws = weakString(strings.Trim(string(b), `"`))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Int returns ws as an integer. If ws is not an
|
|
||||||
// integer, 0 is returned.
|
|
||||||
func (ws weakString) Int() int {
|
|
||||||
num, _ := strconv.Atoi(string(ws))
|
|
||||||
return num
|
|
||||||
}
|
|
||||||
|
|
||||||
// Float64 returns ws as a float64. If ws is not a
|
|
||||||
// float value, the zero value is returned.
|
|
||||||
func (ws weakString) Float64() float64 {
|
|
||||||
num, _ := strconv.ParseFloat(string(ws), 64)
|
|
||||||
return num
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bool returns ws as a boolean. If ws is not a
|
|
||||||
// boolean, false is returned.
|
|
||||||
func (ws weakString) Bool() bool {
|
|
||||||
return string(ws) == "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns ws as a string.
|
|
||||||
func (ws weakString) String() string {
|
|
||||||
return string(ws)
|
|
||||||
}
|
|
||||||
|
@ -20,21 +20,61 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(StaticResponse{})
|
||||||
Name: "http.handlers.static_response",
|
// TODO: Caddyfile directive
|
||||||
New: func() interface{} { return new(StaticResponse) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StaticResponse implements a simple responder for static responses.
|
// StaticResponse implements a simple responder for static responses.
|
||||||
type StaticResponse struct {
|
type StaticResponse struct {
|
||||||
StatusCode weakString `json:"status_code"`
|
StatusCode WeakString `json:"status_code,omitempty"`
|
||||||
Headers http.Header `json:"headers"`
|
Headers http.Header `json:"headers,omitempty"`
|
||||||
Body string `json:"body"`
|
Body string `json:"body,omitempty"`
|
||||||
Close bool `json:"close"`
|
Close bool `json:"close,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (StaticResponse) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.handlers.static_response",
|
||||||
|
New: func() caddy.Module { return new(StaticResponse) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
|
||||||
|
//
|
||||||
|
// static_response [<matcher>] <status> {
|
||||||
|
// body <text>
|
||||||
|
// close
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
func (s *StaticResponse) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
for d.Next() {
|
||||||
|
var statusCodeStr string
|
||||||
|
if d.Args(&statusCodeStr) {
|
||||||
|
s.StatusCode = WeakString(statusCodeStr)
|
||||||
|
}
|
||||||
|
for d.NextBlock() {
|
||||||
|
switch d.Val() {
|
||||||
|
case "body":
|
||||||
|
if s.Body != "" {
|
||||||
|
return d.Err("body already specified")
|
||||||
|
}
|
||||||
|
if !d.Args(&s.Body) {
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
case "close":
|
||||||
|
if s.Close {
|
||||||
|
return d.Err("close already specified")
|
||||||
|
}
|
||||||
|
s.Close = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error {
|
func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error {
|
||||||
|
@ -30,7 +30,7 @@ func TestStaticResponseHandler(t *testing.T) {
|
|||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
s := StaticResponse{
|
s := StaticResponse{
|
||||||
StatusCode: weakString(strconv.Itoa(http.StatusNotFound)),
|
StatusCode: WeakString(strconv.Itoa(http.StatusNotFound)),
|
||||||
Headers: http.Header{
|
Headers: http.Header{
|
||||||
"X-Test": []string{"Testing"},
|
"X-Test": []string{"Testing"},
|
||||||
},
|
},
|
||||||
|
@ -22,10 +22,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(Subroute{})
|
||||||
Name: "http.handlers.subroute",
|
|
||||||
New: func() interface{} { return new(Subroute) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subroute implements a handler that compiles and executes routes.
|
// Subroute implements a handler that compiles and executes routes.
|
||||||
@ -37,6 +34,14 @@ type Subroute struct {
|
|||||||
Routes RouteList `json:"routes,omitempty"`
|
Routes RouteList `json:"routes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (Subroute) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.handlers.subroute",
|
||||||
|
New: func() caddy.Module { return new(Subroute) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Provision sets up subrouting.
|
// Provision sets up subrouting.
|
||||||
func (sr *Subroute) Provision(ctx caddy.Context) error {
|
func (sr *Subroute) Provision(ctx caddy.Context) error {
|
||||||
if sr.Routes != nil {
|
if sr.Routes != 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)
|
|
62
modules/caddyhttp/templates/caddyfile.go
Normal file
62
modules/caddyhttp/templates/caddyfile.go
Normal file
@ -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
|
||||||
|
}
|
@ -27,10 +27,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(Templates{})
|
||||||
Name: "http.handlers.templates",
|
|
||||||
New: func() interface{} { return new(Templates) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Templates is a middleware which execute response bodies as templates.
|
// Templates is a middleware which execute response bodies as templates.
|
||||||
@ -40,6 +37,14 @@ type Templates struct {
|
|||||||
Delimiters []string `json:"delimiters,omitempty"`
|
Delimiters []string `json:"delimiters,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (Templates) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "http.handlers.templates",
|
||||||
|
New: func() caddy.Module { return new(Templates) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Provision provisions t.
|
// Provision provisions t.
|
||||||
func (t *Templates) Provision(ctx caddy.Context) error {
|
func (t *Templates) Provision(ctx caddy.Context) error {
|
||||||
if t.MIMETypes == nil {
|
if t.MIMETypes == nil {
|
||||||
@ -108,7 +113,8 @@ func (t *Templates) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
|
|||||||
func (t *Templates) executeTemplate(rr caddyhttp.ResponseRecorder, r *http.Request) error {
|
func (t *Templates) executeTemplate(rr caddyhttp.ResponseRecorder, r *http.Request) error {
|
||||||
var fs http.FileSystem
|
var fs http.FileSystem
|
||||||
if t.IncludeRoot != "" {
|
if t.IncludeRoot != "" {
|
||||||
fs = http.Dir(t.IncludeRoot)
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
|
||||||
|
fs = http.Dir(repl.ReplaceAll(t.IncludeRoot, "."))
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := &templateContext{
|
ctx := &templateContext{
|
||||||
|
@ -136,19 +136,6 @@ func (c templateContext) Cookie(name string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hostname gets the (remote) hostname of the client making the request.
|
|
||||||
// Performance warning: This involves a DNS lookup.
|
|
||||||
func (c templateContext) Hostname() string {
|
|
||||||
ip := c.RemoteIP()
|
|
||||||
|
|
||||||
hostnameList, err := net.LookupAddr(ip)
|
|
||||||
if err != nil || len(hostnameList) == 0 {
|
|
||||||
return c.Req.RemoteAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
return hostnameList[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoteIP gets the IP address of the client making the request.
|
// RemoteIP gets the IP address of the client making the request.
|
||||||
func (c templateContext) RemoteIP() string {
|
func (c templateContext) RemoteIP() string {
|
||||||
ip, _, err := net.SplitHostPort(c.Req.RemoteAddr)
|
ip, _, err := net.SplitHostPort(c.Req.RemoteAddr)
|
||||||
|
81
modules/caddyhttp/vars.go
Normal file
81
modules/caddyhttp/vars.go
Normal file
@ -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)
|
||||||
|
)
|
@ -28,10 +28,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(ACMEManagerMaker{})
|
||||||
Name: "tls.management.acme",
|
|
||||||
New: func() interface{} { return new(ACMEManagerMaker) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ACMEManagerMaker makes an ACME manager
|
// ACMEManagerMaker makes an ACME manager
|
||||||
@ -57,9 +54,17 @@ type ACMEManagerMaker struct {
|
|||||||
keyType certcrypto.KeyType
|
keyType certcrypto.KeyType
|
||||||
}
|
}
|
||||||
|
|
||||||
// newManager is a no-op to satisfy the ManagerMaker interface,
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (ACMEManagerMaker) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "tls.management.acme",
|
||||||
|
New: func() caddy.Module { return new(ACMEManagerMaker) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager is a no-op to satisfy the ManagerMaker interface,
|
||||||
// because this manager type is a special case.
|
// because this manager type is a special case.
|
||||||
func (m *ACMEManagerMaker) newManager(interactive bool) (certmagic.Manager, error) {
|
func (m ACMEManagerMaker) NewManager(interactive bool) (certmagic.Manager, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,4 +208,4 @@ func onDemandAskRequest(ask string, name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Interface guard
|
// Interface guard
|
||||||
var _ managerMaker = (*ACMEManagerMaker)(nil)
|
var _ ManagerMaker = (*ACMEManagerMaker)(nil)
|
||||||
|
@ -172,7 +172,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
|
|||||||
// add all the cipher suites in order, without duplicates
|
// add all the cipher suites in order, without duplicates
|
||||||
cipherSuitesAdded := make(map[uint16]struct{})
|
cipherSuitesAdded := make(map[uint16]struct{})
|
||||||
for _, csName := range p.CipherSuites {
|
for _, csName := range p.CipherSuites {
|
||||||
csID := supportedCipherSuites[csName]
|
csID := SupportedCipherSuites[csName]
|
||||||
if _, ok := cipherSuitesAdded[csID]; !ok {
|
if _, ok := cipherSuitesAdded[csID]; !ok {
|
||||||
cipherSuitesAdded[csID] = struct{}{}
|
cipherSuitesAdded[csID] = struct{}{}
|
||||||
cfg.CipherSuites = append(cfg.CipherSuites, csID)
|
cfg.CipherSuites = append(cfg.CipherSuites, csID)
|
||||||
@ -182,7 +182,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
|
|||||||
// add all the curve preferences in order, without duplicates
|
// add all the curve preferences in order, without duplicates
|
||||||
curvesAdded := make(map[tls.CurveID]struct{})
|
curvesAdded := make(map[tls.CurveID]struct{})
|
||||||
for _, curveName := range p.Curves {
|
for _, curveName := range p.Curves {
|
||||||
curveID := supportedCurves[curveName]
|
curveID := SupportedCurves[curveName]
|
||||||
if _, ok := curvesAdded[curveID]; !ok {
|
if _, ok := curvesAdded[curveID]; !ok {
|
||||||
curvesAdded[curveID] = struct{}{}
|
curvesAdded[curveID] = struct{}{}
|
||||||
cfg.CurvePreferences = append(cfg.CurvePreferences, curveID)
|
cfg.CurvePreferences = append(cfg.CurvePreferences, curveID)
|
||||||
@ -203,10 +203,10 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
|
|||||||
|
|
||||||
// min and max protocol versions
|
// min and max protocol versions
|
||||||
if p.ProtocolMin != "" {
|
if p.ProtocolMin != "" {
|
||||||
cfg.MinVersion = supportedProtocols[p.ProtocolMin]
|
cfg.MinVersion = SupportedProtocols[p.ProtocolMin]
|
||||||
}
|
}
|
||||||
if p.ProtocolMax != "" {
|
if p.ProtocolMax != "" {
|
||||||
cfg.MaxVersion = supportedProtocols[p.ProtocolMax]
|
cfg.MaxVersion = SupportedProtocols[p.ProtocolMax]
|
||||||
}
|
}
|
||||||
if p.ProtocolMin > p.ProtocolMax {
|
if p.ProtocolMin > p.ProtocolMax {
|
||||||
return fmt.Errorf("protocol min (%x) cannot be greater than protocol max (%x)", p.ProtocolMin, p.ProtocolMax)
|
return fmt.Errorf("protocol min (%x) cannot be greater than protocol max (%x)", p.ProtocolMin, p.ProtocolMax)
|
||||||
|
@ -23,14 +23,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(FileLoader{})
|
||||||
Name: "tls.certificates.load_files",
|
|
||||||
New: func() interface{} { return fileLoader{} },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fileLoader loads certificates and their associated keys from disk.
|
// FileLoader loads certificates and their associated keys from disk.
|
||||||
type fileLoader []CertKeyFilePair
|
type FileLoader []CertKeyFilePair
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (FileLoader) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "tls.certificates.load_files",
|
||||||
|
New: func() caddy.Module { return new(FileLoader) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CertKeyFilePair pairs certificate and key file names along with their
|
// CertKeyFilePair pairs certificate and key file names along with their
|
||||||
// encoding format so that they can be loaded from disk.
|
// encoding format so that they can be loaded from disk.
|
||||||
@ -42,7 +47,7 @@ type CertKeyFilePair struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadCertificates returns the certificates to be loaded by fl.
|
// LoadCertificates returns the certificates to be loaded by fl.
|
||||||
func (fl fileLoader) LoadCertificates() ([]Certificate, error) {
|
func (fl FileLoader) LoadCertificates() ([]Certificate, error) {
|
||||||
var certs []Certificate
|
var certs []Certificate
|
||||||
for _, pair := range fl {
|
for _, pair := range fl {
|
||||||
certData, err := ioutil.ReadFile(pair.Certificate)
|
certData, err := ioutil.ReadFile(pair.Certificate)
|
||||||
@ -73,4 +78,4 @@ func (fl fileLoader) LoadCertificates() ([]Certificate, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Interface guard
|
// Interface guard
|
||||||
var _ CertificateLoader = (fileLoader)(nil)
|
var _ CertificateLoader = (FileLoader)(nil)
|
||||||
|
@ -28,22 +28,27 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(FolderLoader{})
|
||||||
Name: "tls.certificates.load_folders",
|
|
||||||
New: func() interface{} { return folderLoader{} },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// folderLoader loads certificates and their associated keys from disk
|
// FolderLoader loads certificates and their associated keys from disk
|
||||||
// by recursively walking the specified directories, looking for PEM
|
// by recursively walking the specified directories, looking for PEM
|
||||||
// files which contain both a certificate and a key.
|
// files which contain both a certificate and a key.
|
||||||
type folderLoader []string
|
type FolderLoader []string
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (FolderLoader) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "tls.certificates.load_folders",
|
||||||
|
New: func() caddy.Module { return new(FolderLoader) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// LoadCertificates loads all the certificates+keys in the directories
|
// LoadCertificates loads all the certificates+keys in the directories
|
||||||
// listed in fl from all files ending with .pem. This method of loading
|
// listed in fl from all files ending with .pem. This method of loading
|
||||||
// certificates expects the certificate and key to be bundled into the
|
// certificates expects the certificate and key to be bundled into the
|
||||||
// same file.
|
// same file.
|
||||||
func (fl folderLoader) LoadCertificates() ([]Certificate, error) {
|
func (fl FolderLoader) LoadCertificates() ([]Certificate, error) {
|
||||||
var certs []Certificate
|
var certs []Certificate
|
||||||
for _, dir := range fl {
|
for _, dir := range fl {
|
||||||
err := filepath.Walk(dir, func(fpath string, info os.FileInfo, err error) error {
|
err := filepath.Walk(dir, func(fpath string, info os.FileInfo, err error) error {
|
||||||
@ -135,4 +140,4 @@ func x509CertFromCertAndKeyPEMFile(fpath string) (tls.Certificate, error) {
|
|||||||
return cert, nil
|
return cert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ CertificateLoader = (folderLoader)(nil)
|
var _ CertificateLoader = (FolderLoader)(nil)
|
||||||
|
@ -20,14 +20,19 @@ import (
|
|||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
caddy.RegisterModule(MatchServerName{})
|
||||||
|
}
|
||||||
|
|
||||||
// MatchServerName matches based on SNI.
|
// MatchServerName matches based on SNI.
|
||||||
type MatchServerName []string
|
type MatchServerName []string
|
||||||
|
|
||||||
func init() {
|
// CaddyModule returns the Caddy module information.
|
||||||
caddy.RegisterModule(caddy.Module{
|
func (MatchServerName) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
Name: "tls.handshake_match.sni",
|
Name: "tls.handshake_match.sni",
|
||||||
New: func() interface{} { return MatchServerName{} },
|
New: func() caddy.Module { return new(MatchServerName) },
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match matches hello based on SNI.
|
// Match matches hello based on SNI.
|
||||||
|
@ -29,7 +29,7 @@ import (
|
|||||||
// SessionTicketService configures and manages TLS session tickets.
|
// SessionTicketService configures and manages TLS session tickets.
|
||||||
type SessionTicketService struct {
|
type SessionTicketService struct {
|
||||||
KeySource json.RawMessage `json:"key_source,omitempty"`
|
KeySource json.RawMessage `json:"key_source,omitempty"`
|
||||||
RotationInterval caddy.Duration `json:"rotation_interval,omitempty"`
|
RotationInterval caddy.Duration `json:"rotation_interval,omitempty"`
|
||||||
MaxKeys int `json:"max_keys,omitempty"`
|
MaxKeys int `json:"max_keys,omitempty"`
|
||||||
DisableRotation bool `json:"disable_rotation,omitempty"`
|
DisableRotation bool `json:"disable_rotation,omitempty"`
|
||||||
Disabled bool `json:"disabled,omitempty"`
|
Disabled bool `json:"disabled,omitempty"`
|
||||||
|
@ -24,10 +24,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(standardSTEKProvider{})
|
||||||
Name: "tls.stek.standard",
|
|
||||||
New: func() interface{} { return new(standardSTEKProvider) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type standardSTEKProvider struct {
|
type standardSTEKProvider struct {
|
||||||
@ -35,6 +32,14 @@ type standardSTEKProvider struct {
|
|||||||
timer *time.Timer
|
timer *time.Timer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (standardSTEKProvider) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "tls.stek.standard",
|
||||||
|
New: func() caddy.Module { return new(standardSTEKProvider) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize sets the configuration for s and returns the starting keys.
|
// Initialize sets the configuration for s and returns the starting keys.
|
||||||
func (s *standardSTEKProvider) Initialize(config *caddytls.SessionTicketService) ([][32]byte, error) {
|
func (s *standardSTEKProvider) Initialize(config *caddytls.SessionTicketService) ([][32]byte, error) {
|
||||||
// keep a reference to the config; we'll need it when rotating keys
|
// keep a reference to the config; we'll need it when rotating keys
|
||||||
|
@ -30,10 +30,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(caddy.Module{
|
caddy.RegisterModule(TLS{})
|
||||||
Name: "tls",
|
|
||||||
New: func() interface{} { return new(TLS) },
|
|
||||||
})
|
|
||||||
|
|
||||||
// opt-in TLS 1.3 for Go1.12
|
// opt-in TLS 1.3 for Go1.12
|
||||||
// TODO: remove this line when Go1.13 is released.
|
// TODO: remove this line when Go1.13 is released.
|
||||||
@ -45,14 +42,22 @@ func init() {
|
|||||||
// TLS represents a process-wide TLS configuration.
|
// TLS represents a process-wide TLS configuration.
|
||||||
type TLS struct {
|
type TLS struct {
|
||||||
Certificates map[string]json.RawMessage `json:"certificates,omitempty"`
|
Certificates map[string]json.RawMessage `json:"certificates,omitempty"`
|
||||||
Automation AutomationConfig `json:"automation,omitempty"`
|
Automation AutomationConfig `json:"automation"`
|
||||||
SessionTickets SessionTicketService `json:"session_tickets,omitempty"`
|
SessionTickets SessionTicketService `json:"session_tickets"`
|
||||||
|
|
||||||
certificateLoaders []CertificateLoader
|
certificateLoaders []CertificateLoader
|
||||||
certCache *certmagic.Cache
|
certCache *certmagic.Cache
|
||||||
ctx caddy.Context
|
ctx caddy.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (TLS) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
Name: "tls",
|
||||||
|
New: func() caddy.Module { return new(TLS) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Provision sets up the configuration for the TLS app.
|
// Provision sets up the configuration for the TLS app.
|
||||||
func (t *TLS) Provision(ctx caddy.Context) error {
|
func (t *TLS) Provision(ctx caddy.Context) error {
|
||||||
t.ctx = ctx
|
t.ctx = ctx
|
||||||
@ -73,7 +78,7 @@ func (t *TLS) Provision(ctx caddy.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("loading TLS automation management module: %s", err)
|
return fmt.Errorf("loading TLS automation management module: %s", err)
|
||||||
}
|
}
|
||||||
t.Automation.Policies[i].Management = val.(managerMaker)
|
t.Automation.Policies[i].Management = val.(ManagerMaker)
|
||||||
t.Automation.Policies[i].ManagementRaw = nil // allow GC to deallocate - TODO: Does this help?
|
t.Automation.Policies[i].ManagementRaw = nil // allow GC to deallocate - TODO: Does this help?
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,16 +110,12 @@ func (t *TLS) Provision(ctx caddy.Context) error {
|
|||||||
onDemandRateLimiter.SetLimit(0)
|
onDemandRateLimiter.SetLimit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// load manual/static (unmanaged) certificates - we do this in
|
||||||
}
|
// provision so that other apps (such as http) can know which
|
||||||
|
// certificates have been manually loaded
|
||||||
// Start activates the TLS module.
|
|
||||||
func (t *TLS) Start() error {
|
|
||||||
magic := certmagic.New(t.certCache, certmagic.Config{
|
magic := certmagic.New(t.certCache, certmagic.Config{
|
||||||
Storage: t.ctx.Storage(),
|
Storage: ctx.Storage(),
|
||||||
})
|
})
|
||||||
|
|
||||||
// load manual/static (unmanaged) certificates
|
|
||||||
for _, loader := range t.certificateLoaders {
|
for _, loader := range t.certificateLoaders {
|
||||||
certs, err := loader.LoadCertificates()
|
certs, err := loader.LoadCertificates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -128,6 +129,11 @@ func (t *TLS) Start() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start activates the TLS module.
|
||||||
|
func (t *TLS) Start() error {
|
||||||
// load automated (managed) certificates
|
// load automated (managed) certificates
|
||||||
if automatedRawMsg, ok := t.Certificates[automateKey]; ok {
|
if automatedRawMsg, ok := t.Certificates[automateKey]; ok {
|
||||||
var names []string
|
var names []string
|
||||||
@ -204,6 +210,12 @@ func (t *TLS) getAutomationPolicyForName(name string) AutomationPolicy {
|
|||||||
return AutomationPolicy{Management: mgmt}
|
return AutomationPolicy{Management: mgmt}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CertificatesWithSAN returns the list of all certificates
|
||||||
|
// in the cache the match the given SAN value.
|
||||||
|
func (t *TLS) CertificatesWithSAN(san string) []certmagic.Certificate {
|
||||||
|
return t.certCache.CertificatesWithSAN(san)
|
||||||
|
}
|
||||||
|
|
||||||
// CertificateLoader is a type that can load certificates.
|
// CertificateLoader is a type that can load certificates.
|
||||||
// Certificates can optionally be associated with tags.
|
// Certificates can optionally be associated with tags.
|
||||||
type CertificateLoader interface {
|
type CertificateLoader interface {
|
||||||
@ -230,7 +242,7 @@ type AutomationPolicy struct {
|
|||||||
Hosts []string `json:"hosts,omitempty"`
|
Hosts []string `json:"hosts,omitempty"`
|
||||||
ManagementRaw json.RawMessage `json:"management,omitempty"`
|
ManagementRaw json.RawMessage `json:"management,omitempty"`
|
||||||
|
|
||||||
Management managerMaker `json:"-"`
|
Management ManagerMaker `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeCertMagicConfig converts ap into a CertMagic config. Passing onDemand
|
// makeCertMagicConfig converts ap into a CertMagic config. Passing onDemand
|
||||||
@ -245,7 +257,7 @@ func (ap AutomationPolicy) makeCertMagicConfig(ctx caddy.Context) certmagic.Conf
|
|||||||
}
|
}
|
||||||
|
|
||||||
return certmagic.Config{
|
return certmagic.Config{
|
||||||
NewManager: ap.Management.newManager,
|
NewManager: ap.Management.NewManager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,9 +295,9 @@ type RateLimit struct {
|
|||||||
Burst int `json:"burst,omitempty"`
|
Burst int `json:"burst,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// managerMaker makes a certificate manager.
|
// ManagerMaker makes a certificate manager.
|
||||||
type managerMaker interface {
|
type ManagerMaker interface {
|
||||||
newManager(interactive bool) (certmagic.Manager, error)
|
NewManager(interactive bool) (certmagic.Manager, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// These perpetual values are used for on-demand TLS.
|
// These perpetual values are used for on-demand TLS.
|
||||||
|
@ -22,12 +22,16 @@ import (
|
|||||||
"github.com/klauspost/cpuid"
|
"github.com/klauspost/cpuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// supportedCipherSuites is the unordered map of cipher suite
|
// SupportedCipherSuites is the unordered map of cipher suite
|
||||||
// string names to their definition in crypto/tls. All values
|
// string names to their definition in crypto/tls. All values
|
||||||
// should be IANA-reserved names. See
|
// should be IANA-reserved names. See
|
||||||
// https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml
|
// https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml
|
||||||
|
// Two of the cipher suite constants in the standard lib do not use the
|
||||||
|
// full IANA name, but we do; see:
|
||||||
|
// https://github.com/golang/go/issues/32061 and
|
||||||
|
// https://github.com/golang/go/issues/30325#issuecomment-512862374.
|
||||||
// TODO: might not be needed much longer: https://github.com/golang/go/issues/30325
|
// TODO: might not be needed much longer: https://github.com/golang/go/issues/30325
|
||||||
var supportedCipherSuites = map[string]uint16{
|
var SupportedCipherSuites = map[string]uint16{
|
||||||
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||||
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||||
@ -84,22 +88,24 @@ func getOptimalDefaultCipherSuites() []uint16 {
|
|||||||
return defaultCipherSuitesWithoutAESNI
|
return defaultCipherSuitesWithoutAESNI
|
||||||
}
|
}
|
||||||
|
|
||||||
// supportedCurves is the unordered map of supported curves.
|
// SupportedCurves is the unordered map of supported curves.
|
||||||
// https://golang.org/pkg/crypto/tls/#CurveID
|
// https://golang.org/pkg/crypto/tls/#CurveID
|
||||||
var supportedCurves = map[string]tls.CurveID{
|
var SupportedCurves = map[string]tls.CurveID{
|
||||||
"X25519": tls.X25519,
|
// TODO: Use IANA names, probably? see https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8
|
||||||
"P256": tls.CurveP256,
|
// All named crypto/elliptic curves have secpXXXr1 IANA names.
|
||||||
"P384": tls.CurveP384,
|
"x25519": tls.X25519, // x25519, 29
|
||||||
"P521": tls.CurveP521,
|
"p256": tls.CurveP256, // secp256r1, 23
|
||||||
|
"p384": tls.CurveP384, // secp384r1, 24
|
||||||
|
"p521": tls.CurveP521, // secp521r1, 25
|
||||||
}
|
}
|
||||||
|
|
||||||
// supportedCertKeyTypes is all the key types that are supported
|
// supportedCertKeyTypes is all the key types that are supported
|
||||||
// for certificates that are obtained through ACME.
|
// for certificates that are obtained through ACME.
|
||||||
var supportedCertKeyTypes = map[string]certcrypto.KeyType{
|
var supportedCertKeyTypes = map[string]certcrypto.KeyType{
|
||||||
"RSA2048": certcrypto.RSA2048,
|
"rsa_2048": certcrypto.RSA2048,
|
||||||
"RSA4096": certcrypto.RSA4096,
|
"rsa_4096": certcrypto.RSA4096,
|
||||||
"P256": certcrypto.EC256,
|
"ec_p256": certcrypto.EC256,
|
||||||
"P384": certcrypto.EC384,
|
"ec_p384": certcrypto.EC384,
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultCurves is the list of only the curves we want to use
|
// defaultCurves is the list of only the curves we want to use
|
||||||
@ -115,9 +121,9 @@ var defaultCurves = []tls.CurveID{
|
|||||||
tls.CurveP256,
|
tls.CurveP256,
|
||||||
}
|
}
|
||||||
|
|
||||||
// supportedProtocols is a map of supported protocols.
|
// SupportedProtocols is a map of supported protocols.
|
||||||
// HTTP/2 only supports TLS 1.2 and higher.
|
// Note that HTTP/2 only supports TLS 1.2 and higher.
|
||||||
var supportedProtocols = map[string]uint16{
|
var SupportedProtocols = map[string]uint16{
|
||||||
"tls1.0": tls.VersionTLS10,
|
"tls1.0": tls.VersionTLS10,
|
||||||
"tls1.1": tls.VersionTLS11,
|
"tls1.1": tls.VersionTLS11,
|
||||||
"tls1.2": tls.VersionTLS12,
|
"tls1.2": tls.VersionTLS12,
|
||||||
|
@ -21,7 +21,7 @@ import (
|
|||||||
|
|
||||||
func TestGetModules(t *testing.T) {
|
func TestGetModules(t *testing.T) {
|
||||||
modulesMu.Lock()
|
modulesMu.Lock()
|
||||||
modules = map[string]Module{
|
modules = map[string]ModuleInfo{
|
||||||
"a": {Name: "a"},
|
"a": {Name: "a"},
|
||||||
"a.b": {Name: "a.b"},
|
"a.b": {Name: "a.b"},
|
||||||
"a.b.c": {Name: "a.b.c"},
|
"a.b.c": {Name: "a.b.c"},
|
||||||
@ -38,11 +38,11 @@ func TestGetModules(t *testing.T) {
|
|||||||
|
|
||||||
for i, tc := range []struct {
|
for i, tc := range []struct {
|
||||||
input string
|
input string
|
||||||
expect []Module
|
expect []ModuleInfo
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
input: "",
|
input: "",
|
||||||
expect: []Module{
|
expect: []ModuleInfo{
|
||||||
{Name: "a"},
|
{Name: "a"},
|
||||||
{Name: "b"},
|
{Name: "b"},
|
||||||
{Name: "c"},
|
{Name: "c"},
|
||||||
@ -50,7 +50,7 @@ func TestGetModules(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "a",
|
input: "a",
|
||||||
expect: []Module{
|
expect: []ModuleInfo{
|
||||||
{Name: "a.b"},
|
{Name: "a.b"},
|
||||||
{Name: "a.c"},
|
{Name: "a.c"},
|
||||||
{Name: "a.d"},
|
{Name: "a.d"},
|
||||||
@ -58,7 +58,7 @@ func TestGetModules(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "a.b",
|
input: "a.b",
|
||||||
expect: []Module{
|
expect: []ModuleInfo{
|
||||||
{Name: "a.b.c"},
|
{Name: "a.b.c"},
|
||||||
{Name: "a.b.cd"},
|
{Name: "a.b.cd"},
|
||||||
},
|
},
|
||||||
@ -68,7 +68,7 @@ func TestGetModules(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "b",
|
input: "b",
|
||||||
expect: []Module{
|
expect: []ModuleInfo{
|
||||||
{Name: "b.a"},
|
{Name: "b.a"},
|
||||||
{Name: "b.b"},
|
{Name: "b.b"},
|
||||||
},
|
},
|
||||||
|
13
storage.go
13
storage.go
@ -23,10 +23,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RegisterModule(Module{
|
RegisterModule(fileStorage{})
|
||||||
Name: "caddy.storage.file_system",
|
|
||||||
New: func() interface{} { return new(fileStorage) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StorageConverter is a type that can convert itself
|
// StorageConverter is a type that can convert itself
|
||||||
@ -43,6 +40,14 @@ type fileStorage struct {
|
|||||||
Root string `json:"root"`
|
Root string `json:"root"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (fileStorage) CaddyModule() ModuleInfo {
|
||||||
|
return ModuleInfo{
|
||||||
|
Name: "caddy.storage.file_system",
|
||||||
|
New: func() Module { return new(fileStorage) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s fileStorage) CertMagicStorage() (certmagic.Storage, error) {
|
func (s fileStorage) CertMagicStorage() (certmagic.Storage, error) {
|
||||||
return &certmagic.FileStorage{Path: s.Root}, nil
|
return &certmagic.FileStorage{Path: s.Root}, nil
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user