diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index e3090538..dfb2ea07 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 8d25332a..dee39772 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 07c98e30..7da09a60 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 644cfc19..1fd34281 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,28 +61,6 @@ 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 { - 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 { - // 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 { - 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() @@ -77,22 +68,51 @@ func (r *Replacer) defaults() map[string]string { }(), } - // 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, ",") + 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 + }() + 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 + }() + m["request.scheme"] = func() string { + if r.req.TLS != nil { + return "https" + } + return "http" + }() + 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, ",") + } } 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 48a5000a..cd612ac7 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 dca60cbf..408de606 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"` diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index a085fa30..bdbd79f4 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -85,6 +85,9 @@ func (cp *ConnectionPolicy) buildStandardTLSConfig(handle caddy2.Handle) error { NextProtos: cp.ALPN, PreferServerCipherSuites: true, GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + // TODO: Must fix https://github.com/mholt/caddy/issues/2588 + // (allow customizing the selection of a very specific certificate + // based on the ClientHelloInfo) cfgTpl, err := tlsApp.getConfigForName(hello.ServerName) if err != nil { return nil, fmt.Errorf("getting config for name %s: %v", hello.ServerName, err)