summaryrefslogtreecommitdiff
path: root/modules/caddyhttp
diff options
context:
space:
mode:
Diffstat (limited to 'modules/caddyhttp')
-rw-r--r--modules/caddyhttp/caddyhttp.go60
-rw-r--r--modules/caddyhttp/caddyhttp_test.go11
-rw-r--r--modules/caddyhttp/matchers_test.go4
-rw-r--r--modules/caddyhttp/replacer.go80
-rw-r--r--modules/caddyhttp/routes.go84
-rw-r--r--modules/caddyhttp/staticresp.go1
6 files changed, 141 insertions, 99 deletions
diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go
index e309053..dfb2ea0 100644
--- a/modules/caddyhttp/caddyhttp.go
+++ b/modules/caddyhttp/caddyhttp.go
@@ -22,29 +22,31 @@ func init() {
err := caddy2.RegisterModule(caddy2.Module{
Name: "http",
- New: func() (interface{}, error) { return new(httpModuleConfig), nil },
+ New: func() (interface{}, error) { return new(App), nil },
})
if err != nil {
log.Fatal(err)
}
}
-type httpModuleConfig struct {
- HTTPPort int `json:"http_port"`
- HTTPSPort int `json:"https_port"`
- GracePeriod caddy2.Duration `json:"grace_period"`
- Servers map[string]*httpServerConfig `json:"servers"`
+// App is the HTTP app for Caddy.
+type App struct {
+ HTTPPort int `json:"http_port"`
+ HTTPSPort int `json:"https_port"`
+ GracePeriod caddy2.Duration `json:"grace_period"`
+ Servers map[string]*Server `json:"servers"`
servers []*http.Server
}
-func (hc *httpModuleConfig) Provision() error {
+// Provision sets up the app.
+func (hc *App) Provision() error {
for _, srv := range hc.Servers {
- err := srv.Routes.setup()
+ err := srv.Routes.Provision()
if err != nil {
return fmt.Errorf("setting up server routes: %v", err)
}
- err = srv.Errors.Routes.setup()
+ err = srv.Errors.Routes.Provision()
if err != nil {
return fmt.Errorf("setting up server error handling routes: %v", err)
}
@@ -53,7 +55,8 @@ func (hc *httpModuleConfig) Provision() error {
return nil
}
-func (hc *httpModuleConfig) Validate() error {
+// Validate ensures the app's configuration is valid.
+func (hc *App) Validate() error {
// each server must use distinct listener addresses
lnAddrs := make(map[string]string)
for srvName, srv := range hc.Servers {
@@ -74,7 +77,8 @@ func (hc *httpModuleConfig) Validate() error {
return nil
}
-func (hc *httpModuleConfig) Start(handle caddy2.Handle) error {
+// Start runs the app. It sets up automatic HTTPS if enabled.
+func (hc *App) Start(handle caddy2.Handle) error {
err := hc.automaticHTTPS(handle)
if err != nil {
return fmt.Errorf("enabling automatic HTTPS: %v", err)
@@ -129,7 +133,7 @@ func (hc *httpModuleConfig) Start(handle caddy2.Handle) error {
}
// Stop gracefully shuts down the HTTP server.
-func (hc *httpModuleConfig) Stop() error {
+func (hc *App) Stop() error {
ctx := context.Background()
if hc.GracePeriod > 0 {
var cancel context.CancelFunc
@@ -145,7 +149,7 @@ func (hc *httpModuleConfig) Stop() error {
return nil
}
-func (hc *httpModuleConfig) automaticHTTPS(handle caddy2.Handle) error {
+func (hc *App) automaticHTTPS(handle caddy2.Handle) error {
tlsAppIface, err := handle.App("tls")
if err != nil {
return fmt.Errorf("getting tls app: %v", err)
@@ -153,7 +157,7 @@ func (hc *httpModuleConfig) automaticHTTPS(handle caddy2.Handle) error {
tlsApp := tlsAppIface.(*caddytls.TLS)
lnAddrMap := make(map[string]struct{})
- var redirRoutes routeList
+ var redirRoutes RouteList
for srvName, srv := range hc.Servers {
srv.tlsApp = tlsApp
@@ -222,7 +226,7 @@ func (hc *httpModuleConfig) automaticHTTPS(handle caddy2.Handle) error {
}
redirTo += "{request.uri}"
- redirRoutes = append(redirRoutes, serverRoute{
+ redirRoutes = append(redirRoutes, ServerRoute{
matchers: []RouteMatcher{
matchProtocol("http"),
matchHost(domains),
@@ -255,7 +259,7 @@ func (hc *httpModuleConfig) automaticHTTPS(handle caddy2.Handle) error {
}
lnAddrs = append(lnAddrs, addr)
}
- hc.Servers["auto_https_redirects"] = &httpServerConfig{
+ hc.Servers["auto_https_redirects"] = &Server{
Listen: lnAddrs,
Routes: redirRoutes,
DisableAutoHTTPS: true,
@@ -265,7 +269,7 @@ func (hc *httpModuleConfig) automaticHTTPS(handle caddy2.Handle) error {
return nil
}
-func (hc *httpModuleConfig) listenerTaken(network, address string) bool {
+func (hc *App) listenerTaken(network, address string) bool {
for _, srv := range hc.Servers {
for _, addr := range srv.Listen {
netw, addrs, err := parseListenAddr(addr)
@@ -284,12 +288,13 @@ func (hc *httpModuleConfig) listenerTaken(network, address string) bool {
var defaultALPN = []string{"h2", "http/1.1"}
-type httpServerConfig struct {
+// Server is an HTTP server.
+type Server struct {
Listen []string `json:"listen"`
ReadTimeout caddy2.Duration `json:"read_timeout"`
ReadHeaderTimeout caddy2.Duration `json:"read_header_timeout"`
HiddenFiles []string `json:"hidden_files"` // TODO:... experimenting with shared/common state
- Routes routeList `json:"routes"`
+ Routes RouteList `json:"routes"`
Errors httpErrorConfig `json:"errors"`
TLSConnPolicies caddytls.ConnectionPolicies `json:"tls_connection_policies"`
DisableAutoHTTPS bool `json:"disable_auto_https"`
@@ -299,23 +304,24 @@ type httpServerConfig struct {
}
type httpErrorConfig struct {
- Routes routeList `json:"routes"`
- // TODO: some way to configure the logging of errors, probably? standardize the logging configuration first.
+ Routes RouteList `json:"routes"`
+ // TODO: some way to configure the logging of errors, probably? standardize
+ // the logging configuration first.
}
// ServeHTTP is the entry point for all HTTP requests.
-func (s httpServerConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if s.tlsApp.HandleHTTPChallenge(w, r) {
return
}
// set up the replacer
- repl := &Replacer{req: r, resp: w, custom: make(map[string]string)}
+ repl := NewReplacer(r, w)
ctx := context.WithValue(r.Context(), ReplacerCtxKey, repl)
r = r.WithContext(ctx)
// build and execute the main middleware chain
- stack := s.Routes.buildMiddlewareChain(w, r)
+ stack := s.Routes.BuildHandlerChain(w, r)
err := executeMiddlewareChain(w, r, stack)
if err != nil {
// add the error value to the request context so
@@ -328,7 +334,7 @@ func (s httpServerConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// TODO: implement a default error handler?
log.Printf("[ERROR] %s", err)
} else {
- errStack := s.Errors.Routes.buildMiddlewareChain(w, r)
+ errStack := s.Errors.Routes.BuildHandlerChain(w, r)
err := executeMiddlewareChain(w, r, errStack)
if err != nil {
// TODO: what should we do if the error handler has an error?
@@ -411,6 +417,7 @@ func parseListenAddr(a string) (network string, addrs []string, err error) {
if err != nil {
return
}
+ host = NewReplacer(nil, nil).Replace(host, "")
ports := strings.SplitN(port, "-", 2)
if len(ports) == 1 {
ports = append(ports, ports[0])
@@ -474,9 +481,6 @@ func (mrw middlewareResponseWriter) Write(b []byte) (int, error) {
return mrw.ResponseWriterWrapper.Write(b)
}
-// ReplacerCtxKey is the context key for the request's replacer.
-const ReplacerCtxKey caddy2.CtxKey = "replacer"
-
const (
// DefaultHTTPPort is the default port for HTTP.
DefaultHTTPPort = 80
diff --git a/modules/caddyhttp/caddyhttp_test.go b/modules/caddyhttp/caddyhttp_test.go
index 8d25332..dee3977 100644
--- a/modules/caddyhttp/caddyhttp_test.go
+++ b/modules/caddyhttp/caddyhttp_test.go
@@ -1,6 +1,7 @@
package caddyhttp
import (
+ "os"
"reflect"
"testing"
)
@@ -114,6 +115,11 @@ func TestJoinListenerAddr(t *testing.T) {
}
func TestParseListenerAddr(t *testing.T) {
+ hostname, err := os.Hostname()
+ if err != nil {
+ t.Fatalf("Cannot ascertain system hostname: %v", err)
+ }
+
for i, tc := range []struct {
input string
expectNetwork string
@@ -170,6 +176,11 @@ func TestParseListenerAddr(t *testing.T) {
expectNetwork: "tcp",
expectAddrs: []string{"localhost:0"},
},
+ {
+ input: "{system.hostname}:0",
+ expectNetwork: "tcp",
+ expectAddrs: []string{hostname + ":0"},
+ },
} {
actualNetwork, actualAddrs, err := parseListenAddr(tc.input)
if tc.expectErr && err == nil {
diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go
index 07c98e3..7da09a6 100644
--- a/modules/caddyhttp/matchers_test.go
+++ b/modules/caddyhttp/matchers_test.go
@@ -205,7 +205,7 @@ func TestPathREMatcher(t *testing.T) {
// set up the fake request and its Replacer
req := &http.Request{URL: &url.URL{Path: tc.input}}
- repl := &Replacer{req: req, resp: httptest.NewRecorder(), custom: make(map[string]string)}
+ repl := NewReplacer(req, httptest.NewRecorder())
ctx := context.WithValue(req.Context(), ReplacerCtxKey, repl)
req = req.WithContext(ctx)
@@ -322,7 +322,7 @@ func TestHeaderREMatcher(t *testing.T) {
// set up the fake request and its Replacer
req := &http.Request{Header: tc.input, URL: new(url.URL)}
- repl := &Replacer{req: req, resp: httptest.NewRecorder(), custom: make(map[string]string)}
+ repl := NewReplacer(req, httptest.NewRecorder())
ctx := context.WithValue(req.Context(), ReplacerCtxKey, repl)
req = req.WithContext(ctx)
diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go
index 644cfc1..1fd3428 100644
--- a/modules/caddyhttp/replacer.go
+++ b/modules/caddyhttp/replacer.go
@@ -5,18 +5,31 @@ import (
"net/http"
"os"
"strings"
+
+ "bitbucket.org/lightcodelabs/caddy2"
)
// Replacer can replace values in strings based
// on a request and/or response writer. The zero
-// Replacer is not valid; it must be initialized
-// within this package.
+// Replacer is not valid; use NewReplacer() to
+// initialize one.
type Replacer struct {
req *http.Request
resp http.ResponseWriter
custom map[string]string
}
+// NewReplacer makes a new Replacer, initializing all necessary
+// fields. The request and response writer are optional, but
+// necessary for most replacements to work.
+func NewReplacer(req *http.Request, rw http.ResponseWriter) *Replacer {
+ return &Replacer{
+ req: req,
+ resp: rw,
+ custom: make(map[string]string),
+ }
+}
+
// Map sets a custom variable mapping to a value.
func (r *Replacer) Map(variable, value string) {
r.custom[variable] = value
@@ -48,51 +61,58 @@ func (r *Replacer) replaceAll(input, empty string, mapping map[string]string) st
func (r *Replacer) defaults() map[string]string {
m := map[string]string{
- "request.host": func() string {
+ "system.hostname": func() string {
+ // OK if there is an error; just return empty string
+ name, _ := os.Hostname()
+ return name
+ }(),
+ }
+
+ if r.req != nil {
+ m["request.host"] = func() string {
host, _, err := net.SplitHostPort(r.req.Host)
if err != nil {
return r.req.Host // OK; there probably was no port
}
return host
- }(),
- "request.hostport": r.req.Host, // may include both host and port
- "request.method": r.req.Method,
- "request.port": func() string {
+ }()
+ m["request.hostport"] = r.req.Host // may include both host and port
+ m["request.method"] = r.req.Method
+ m["request.port"] = func() string {
// if there is no port, there will be an error; in
// that case, port is the empty string anyway
_, port, _ := net.SplitHostPort(r.req.Host)
return port
- }(),
- "request.scheme": func() string {
+ }()
+ m["request.scheme"] = func() string {
if r.req.TLS != nil {
return "https"
}
return "http"
- }(),
- "request.uri": r.req.URL.RequestURI(),
- "system.hostname": func() string {
- // OK if there is an error; just return empty string
- name, _ := os.Hostname()
- return name
- }(),
- }
+ }()
+ m["request.uri"] = r.req.URL.RequestURI()
+ m["request.uri.path"] = r.req.URL.Path
- // TODO: why should header fields, cookies, and query params get special treatment like this?
- // maybe they should be scoped by words like "request.header." just like everything else.
- for field, vals := range r.req.Header {
- m[">"+strings.ToLower(field)] = strings.Join(vals, ",")
- }
- for field, vals := range r.resp.Header() {
- m["<"+strings.ToLower(field)] = strings.Join(vals, ",")
- }
- for _, cookie := range r.req.Cookies() {
- m["~"+cookie.Name] = cookie.Value
- }
- for param, vals := range r.req.URL.Query() {
- m["?"+param] = strings.Join(vals, ",")
+ // TODO: why should header fields, cookies, and query params get special treatment like this?
+ // maybe they should be scoped by words like "request.header." just like everything else.
+ for field, vals := range r.req.Header {
+ m[">"+strings.ToLower(field)] = strings.Join(vals, ",")
+ }
+ for field, vals := range r.resp.Header() {
+ m["<"+strings.ToLower(field)] = strings.Join(vals, ",")
+ }
+ for _, cookie := range r.req.Cookies() {
+ m["~"+cookie.Name] = cookie.Value
+ }
+ for param, vals := range r.req.URL.Query() {
+ m["?"+param] = strings.Join(vals, ",")
+ }
}
return m
}
const phOpen, phClose = "{", "}"
+
+// ReplacerCtxKey is the context key for the request's replacer.
+const ReplacerCtxKey caddy2.CtxKey = "replacer"
diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go
index 48a5000..cd612ac 100644
--- a/modules/caddyhttp/routes.go
+++ b/modules/caddyhttp/routes.go
@@ -8,7 +8,10 @@ import (
"bitbucket.org/lightcodelabs/caddy2"
)
-type serverRoute struct {
+// ServerRoute represents a set of matching rules,
+// middlewares, and a responder for handling HTTP
+// requests.
+type ServerRoute struct {
Matchers map[string]json.RawMessage `json:"match"`
Apply []json.RawMessage `json:"apply"`
Respond json.RawMessage `json:"respond"`
@@ -21,9 +24,49 @@ type serverRoute struct {
responder Handler
}
-type routeList []serverRoute
+// RouteList is a list of server routes that can
+// create a middleware chain.
+type RouteList []ServerRoute
-func (routes routeList) buildMiddlewareChain(w http.ResponseWriter, r *http.Request) Handler {
+// Provision sets up all the routes by loading the modules.
+func (routes RouteList) Provision() error {
+ for i, route := range routes {
+ // matchers
+ for modName, rawMsg := range route.Matchers {
+ val, err := caddy2.LoadModule("http.matchers."+modName, rawMsg)
+ if err != nil {
+ return fmt.Errorf("loading matcher module '%s': %v", modName, err)
+ }
+ routes[i].matchers = append(routes[i].matchers, val.(RouteMatcher))
+ }
+ routes[i].Matchers = nil // allow GC to deallocate - TODO: Does this help?
+
+ // middleware
+ for j, rawMsg := range route.Apply {
+ mid, err := caddy2.LoadModuleInline("middleware", "http.middleware", rawMsg)
+ if err != nil {
+ return fmt.Errorf("loading middleware module in position %d: %v", j, err)
+ }
+ routes[i].middleware = append(routes[i].middleware, mid.(MiddlewareHandler))
+ }
+ routes[i].Apply = nil // allow GC to deallocate - TODO: Does this help?
+
+ // responder
+ if route.Respond != nil {
+ resp, err := caddy2.LoadModuleInline("responder", "http.responders", route.Respond)
+ if err != nil {
+ return fmt.Errorf("loading responder module: %v", err)
+ }
+ routes[i].responder = resp.(Handler)
+ }
+ routes[i].Respond = nil // allow GC to deallocate - TODO: Does this help?
+ }
+ return nil
+}
+
+// BuildHandlerChain creates a chain of handlers by
+// applying all the matching routes.
+func (routes RouteList) BuildHandlerChain(w http.ResponseWriter, r *http.Request) Handler {
if len(routes) == 0 {
return emptyHandler
}
@@ -68,38 +111,3 @@ routeLoop:
return stack
}
-
-func (routes routeList) setup() error {
- for i, route := range routes {
- // matchers
- for modName, rawMsg := range route.Matchers {
- val, err := caddy2.LoadModule("http.matchers."+modName, rawMsg)
- if err != nil {
- return fmt.Errorf("loading matcher module '%s': %v", modName, err)
- }
- routes[i].matchers = append(routes[i].matchers, val.(RouteMatcher))
- }
- routes[i].Matchers = nil // allow GC to deallocate - TODO: Does this help?
-
- // middleware
- for j, rawMsg := range route.Apply {
- mid, err := caddy2.LoadModuleInline("middleware", "http.middleware", rawMsg)
- if err != nil {
- return fmt.Errorf("loading middleware module in position %d: %v", j, err)
- }
- routes[i].middleware = append(routes[i].middleware, mid.(MiddlewareHandler))
- }
- routes[i].Apply = nil // allow GC to deallocate - TODO: Does this help?
-
- // responder
- if route.Respond != nil {
- resp, err := caddy2.LoadModuleInline("responder", "http.responders", route.Respond)
- if err != nil {
- return fmt.Errorf("loading responder module: %v", err)
- }
- routes[i].responder = resp.(Handler)
- }
- routes[i].Respond = nil // allow GC to deallocate - TODO: Does this help?
- }
- return nil
-}
diff --git a/modules/caddyhttp/staticresp.go b/modules/caddyhttp/staticresp.go
index dca60cb..408de60 100644
--- a/modules/caddyhttp/staticresp.go
+++ b/modules/caddyhttp/staticresp.go
@@ -15,7 +15,6 @@ func init() {
}
// Static implements a simple responder for static responses.
-// It is Caddy's default responder. TODO: Or is it?
type Static struct {
StatusCode int `json:"status_code"`
Headers http.Header `json:"headers"`