diff --git a/cmd/caddy2/main.go b/cmd/caddy2/main.go index 5a09d871..482c4f59 100644 --- a/cmd/caddy2/main.go +++ b/cmd/caddy2/main.go @@ -9,7 +9,6 @@ import ( _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/caddylog" _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/reverseproxy" _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/staticfiles" - _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/staticresp" _ "bitbucket.org/lightcodelabs/caddy2/modules/caddytls" ) diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index 0731fea2..e3090538 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -30,13 +30,15 @@ func init() { } type httpModuleConfig struct { - Servers map[string]*httpServerConfig `json:"servers"` + HTTPPort int `json:"http_port"` + HTTPSPort int `json:"https_port"` + GracePeriod caddy2.Duration `json:"grace_period"` + Servers map[string]*httpServerConfig `json:"servers"` servers []*http.Server } func (hc *httpModuleConfig) Provision() error { - // TODO: Either prevent overlapping listeners on different servers, or combine them into one for _, srv := range hc.Servers { err := srv.Routes.setup() if err != nil { @@ -51,6 +53,27 @@ func (hc *httpModuleConfig) Provision() error { return nil } +func (hc *httpModuleConfig) Validate() error { + // each server must use distinct listener addresses + lnAddrs := make(map[string]string) + for srvName, srv := range hc.Servers { + for _, addr := range srv.Listen { + netw, expanded, err := parseListenAddr(addr) + if err != nil { + return fmt.Errorf("invalid listener address '%s': %v", addr, err) + } + for _, a := range expanded { + if sn, ok := lnAddrs[netw+a]; ok { + return fmt.Errorf("listener address repeated: %s (already claimed by server '%s')", a, sn) + } + lnAddrs[netw+a] = srvName + } + } + } + + return nil +} + func (hc *httpModuleConfig) Start(handle caddy2.Handle) error { err := hc.automaticHTTPS(handle) if err != nil { @@ -83,7 +106,12 @@ func (hc *httpModuleConfig) Start(handle caddy2.Handle) error { } // enable TLS - if len(srv.TLSConnPolicies) > 0 { + httpPort := hc.HTTPPort + if httpPort == 0 { + httpPort = DefaultHTTPPort + } + _, port, _ := net.SplitHostPort(addr) + if len(srv.TLSConnPolicies) > 0 && port != strconv.Itoa(httpPort) { tlsCfg, err := srv.TLSConnPolicies.TLSConfig(handle) if err != nil { return fmt.Errorf("%s/%s: making TLS configuration: %v", network, addr, err) @@ -100,9 +128,16 @@ func (hc *httpModuleConfig) Start(handle caddy2.Handle) error { return nil } +// Stop gracefully shuts down the HTTP server. func (hc *httpModuleConfig) Stop() error { + ctx := context.Background() + if hc.GracePeriod > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, time.Duration(hc.GracePeriod)) + defer cancel() + } for _, s := range hc.servers { - err := s.Shutdown(context.Background()) // TODO + err := s.Shutdown(ctx) if err != nil { return err } @@ -117,6 +152,9 @@ func (hc *httpModuleConfig) automaticHTTPS(handle caddy2.Handle) error { } tlsApp := tlsAppIface.(*caddytls.TLS) + lnAddrMap := make(map[string]struct{}) + var redirRoutes routeList + for srvName, srv := range hc.Servers { srv.tlsApp = tlsApp @@ -157,13 +195,93 @@ func (hc *httpModuleConfig) automaticHTTPS(handle caddy2.Handle) error { {ALPN: defaultALPN}, } - // TODO: create HTTP->HTTPS redirects + if srv.DisableAutoHTTPSRedir { + continue + } + + // create HTTP->HTTPS redirects + for _, addr := range srv.Listen { + netw, host, port, err := splitListenAddr(addr) + if err != nil { + return fmt.Errorf("%s: invalid listener address: %v", srvName, addr) + } + httpRedirLnAddr := joinListenAddr(netw, host, strconv.Itoa(hc.HTTPPort)) + lnAddrMap[httpRedirLnAddr] = struct{}{} + + if parts := strings.SplitN(port, "-", 2); len(parts) == 2 { + port = parts[0] + } + redirTo := "https://{request.host}" + + httpsPort := hc.HTTPSPort + if httpsPort == 0 { + httpsPort = DefaultHTTPSPort + } + if port != strconv.Itoa(httpsPort) { + redirTo += ":" + port + } + redirTo += "{request.uri}" + + redirRoutes = append(redirRoutes, serverRoute{ + matchers: []RouteMatcher{ + matchProtocol("http"), + matchHost(domains), + }, + responder: Static{ + StatusCode: http.StatusTemporaryRedirect, // TODO: use permanent redirect instead + Headers: http.Header{ + "Location": []string{redirTo}, + "Connection": []string{"close"}, + }, + Close: true, + }, + }) + } + } + } + + if len(lnAddrMap) > 0 { + var lnAddrs []string + mapLoop: + for addr := range lnAddrMap { + netw, addrs, err := parseListenAddr(addr) + if err != nil { + continue + } + for _, a := range addrs { + if hc.listenerTaken(netw, a) { + continue mapLoop + } + } + lnAddrs = append(lnAddrs, addr) + } + hc.Servers["auto_https_redirects"] = &httpServerConfig{ + Listen: lnAddrs, + Routes: redirRoutes, + DisableAutoHTTPS: true, } } return nil } +func (hc *httpModuleConfig) listenerTaken(network, address string) bool { + for _, srv := range hc.Servers { + for _, addr := range srv.Listen { + netw, addrs, err := parseListenAddr(addr) + if err != nil || netw != network { + continue + } + for _, a := range addrs { + if a == address { + return true + } + } + } + } + return false +} + var defaultALPN = []string{"h2", "http/1.1"} type httpServerConfig struct { @@ -204,6 +322,7 @@ func (s httpServerConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) { // it can be accessed by error handlers c := context.WithValue(r.Context(), ErrorCtxKey, err) r = r.WithContext(c) + // TODO: add error values to Replacer if len(s.Errors.Routes) == 0 { // TODO: implement a default error handler? @@ -284,13 +403,11 @@ func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) error { var emptyHandler HandlerFunc = func(w http.ResponseWriter, r *http.Request) error { return nil } func parseListenAddr(a string) (network string, addrs []string, err error) { - network = "tcp" - if idx := strings.Index(a, "/"); idx >= 0 { - network = strings.ToLower(strings.TrimSpace(a[:idx])) - a = a[idx+1:] - } var host, port string - host, port, err = net.SplitHostPort(a) + network, host, port, err = splitListenAddr(a) + if network == "" { + network = "tcp" + } if err != nil { return } @@ -317,6 +434,27 @@ func parseListenAddr(a string) (network string, addrs []string, err error) { return } +func splitListenAddr(a string) (network, host, port string, err error) { + if idx := strings.Index(a, "/"); idx >= 0 { + network = strings.ToLower(strings.TrimSpace(a[:idx])) + a = a[idx+1:] + } + host, port, err = net.SplitHostPort(a) + return +} + +func joinListenAddr(network, host, port string) string { + var a string + if network != "" { + a = network + "/" + } + a += host + if port != "" { + a += ":" + port + } + return a +} + type middlewareResponseWriter struct { *ResponseWriterWrapper allowWrites bool @@ -336,7 +474,16 @@ 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 + + // DefaultHTTPSPort is the default port for HTTPS. + DefaultHTTPSPort = 443 +) + // Interface guards var _ HTTPInterfaces = middlewareResponseWriter{} diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 731832be..7336a1b2 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -68,17 +68,11 @@ func (m matchScript) Match(r *http.Request) bool { func (m matchProtocol) Match(r *http.Request) bool { switch string(m) { case "grpc": - if r.Header.Get("content-type") == "application/grpc" { - return true - } + return r.Header.Get("content-type") == "application/grpc" case "https": - if r.TLS != nil { - return true - } + return r.TLS != nil case "http": - if r.TLS == nil { - return true - } + return r.TLS == nil } return false diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go index 6a2ecd18..947de092 100644 --- a/modules/caddyhttp/replacer.go +++ b/modules/caddyhttp/replacer.go @@ -1,10 +1,14 @@ package caddyhttp import ( + "net" "net/http" + "os" "strings" ) +// Replacer can replace values in strings based +// on a request and/or response writer. type Replacer struct { req *http.Request resp http.ResponseWriter @@ -42,15 +46,33 @@ func (r *Replacer) replaceAll(input, empty string, mapping map[string]string) st func (r *Replacer) defaults() map[string]string { m := map[string]string{ - "host": r.req.Host, - "method": r.req.Method, - "scheme": func() 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" }(), - "uri": r.req.URL.RequestURI(), + "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 + }(), } for field, vals := range r.req.Header { diff --git a/modules/caddyhttp/staticresp/staticresp.go b/modules/caddyhttp/staticresp.go similarity index 70% rename from modules/caddyhttp/staticresp/staticresp.go rename to modules/caddyhttp/staticresp.go index e1691339..dca60cbf 100644 --- a/modules/caddyhttp/staticresp/staticresp.go +++ b/modules/caddyhttp/staticresp.go @@ -1,11 +1,10 @@ -package staticresp +package caddyhttp import ( "fmt" "net/http" "bitbucket.org/lightcodelabs/caddy2" - "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp" ) func init() { @@ -16,15 +15,16 @@ 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 map[string][]string `json:"headers"` - Body string `json:"body"` - Close bool `json:"close"` + StatusCode int `json:"status_code"` + Headers http.Header `json:"headers"` + Body string `json:"body"` + Close bool `json:"close"` } func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error { - repl := r.Context().Value(caddyhttp.ReplacerCtxKey).(*caddyhttp.Replacer) + repl := r.Context().Value(ReplacerCtxKey).(*Replacer) // close the connection r.Close = s.Close @@ -54,4 +54,4 @@ func (s Static) ServeHTTP(w http.ResponseWriter, r *http.Request) error { } // Interface guard -var _ caddyhttp.Handler = (*Static)(nil) +var _ Handler = (*Static)(nil)