From e3f2d96a5ef3b3696c1424ff0a2f645551eae913 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 6 Mar 2017 18:18:49 -0700 Subject: [PATCH] httpserver: Flags to customize HTTP and HTTPS ports (incl. for ACME) This commit removes _almost_ all instances of hard-coded ports 80 and 443 strings, and now allows the user to define what the HTTP and HTTPS ports are by the -http-port and -https-ports flags. (One instance of "80" is still hard-coded in tls.go because it cannot import httpserver to get access to the HTTP port variable. I don't suspect this will be a problem in practice, but one workaround would be to define an exported variable in the caddytls package and let the httpserver package set it as well as its own HTTPPort variable.) The port numbers required by the ACME challenges HTTP-01 and TLS-SNI-01 are hard-coded into the spec as ports 80 and 443 for good reasons, but the big question is whether they necessarily need to be the HTTP and HTTPS ports. Although the answer is probably no, they chose those ports for convenience and widest compatibility/deployability. So this commit also assumes that the "HTTP port" is necessarily the same port on which to serve the HTTP-01 challenge, and the "HTTPS port" is necessarily the same one on which to serve the TLS-SNI-01 challenge. In other words, changing the HTTP and HTTPS ports also changes the ports the challenges will be served on. If you change the HTTP and HTTPS ports, you are responsible for configuring your system to forward ports 80 and 443 properly. Closes #918 and closes #1293. Also related: #468. --- caddyhttp/httpserver/context.go | 2 +- caddyhttp/httpserver/https.go | 26 ++++++++------- caddyhttp/httpserver/https_test.go | 14 ++++---- caddyhttp/httpserver/plugin.go | 52 ++++++++++++++++++++++-------- caddytls/client.go | 24 ++++++++------ caddytls/config.go | 10 ++++-- 6 files changed, 84 insertions(+), 44 deletions(-) diff --git a/caddyhttp/httpserver/context.go b/caddyhttp/httpserver/context.go index 8a8cec93..6dc3a197 100644 --- a/caddyhttp/httpserver/context.go +++ b/caddyhttp/httpserver/context.go @@ -111,7 +111,7 @@ func (c Context) Port() (string, error) { if err != nil { if !strings.Contains(c.Req.Host, ":") { // common with sites served on the default port 80 - return "80", nil + return HTTPPort, nil } return "", err } diff --git a/caddyhttp/httpserver/https.go b/caddyhttp/httpserver/https.go index 60b0c242..fcbd2d90 100644 --- a/caddyhttp/httpserver/https.go +++ b/caddyhttp/httpserver/https.go @@ -93,7 +93,7 @@ func enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error { cfg.TLS.Enabled && (!cfg.TLS.Manual || cfg.TLS.OnDemand) && cfg.Addr.Host != "localhost" { - cfg.Addr.Port = "443" + cfg.Addr.Port = HTTPSPort } } return nil @@ -108,8 +108,8 @@ func enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error { func makePlaintextRedirects(allConfigs []*SiteConfig) []*SiteConfig { for i, cfg := range allConfigs { if cfg.TLS.Managed && - !hostHasOtherPort(allConfigs, i, "80") && - (cfg.Addr.Port == "443" || !hostHasOtherPort(allConfigs, i, "443")) { + !hostHasOtherPort(allConfigs, i, HTTPPort) && + (cfg.Addr.Port == HTTPSPort || !hostHasOtherPort(allConfigs, i, HTTPSPort)) { allConfigs = append(allConfigs, redirPlaintextHost(cfg)) } } @@ -135,18 +135,19 @@ func hostHasOtherPort(allConfigs []*SiteConfig, thisConfigIdx int, otherPort str // redirPlaintextHost returns a new plaintext HTTP configuration for // a virtualHost that simply redirects to cfg, which is assumed to // be the HTTPS configuration. The returned configuration is set -// to listen on port 80. The TLS field of cfg must not be nil. +// to listen on HTTPPort. The TLS field of cfg must not be nil. func redirPlaintextHost(cfg *SiteConfig) *SiteConfig { redirPort := cfg.Addr.Port - if redirPort == "443" { - // default port is redundant - redirPort = "" + if redirPort == DefaultHTTPSPort { + redirPort = "" // default port is redundant } redirMiddleware := func(next Handler) Handler { return HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { - toURL := "https://" + r.Host - if redirPort != "" { - toURL += ":" + redirPort + toURL := "https://" + if redirPort == "" { + toURL += cfg.Addr.Host // don't use r.Host as it may have a port included + } else { + toURL += net.JoinHostPort(cfg.Addr.Host, redirPort) } toURL += r.URL.RequestURI() w.Header().Set("Connection", "close") @@ -155,12 +156,13 @@ func redirPlaintextHost(cfg *SiteConfig) *SiteConfig { }) } host := cfg.Addr.Host - port := "80" + port := HTTPPort addr := net.JoinHostPort(host, port) return &SiteConfig{ Addr: Address{Original: addr, Host: host, Port: port}, ListenHost: cfg.ListenHost, middleware: []Middleware{redirMiddleware}, - TLS: &caddytls.Config{AltHTTPPort: cfg.TLS.AltHTTPPort}, + TLS: &caddytls.Config{AltHTTPPort: cfg.TLS.AltHTTPPort, AltTLSSNIPort: cfg.TLS.AltTLSSNIPort}, + Timeouts: cfg.Timeouts, } } diff --git a/caddyhttp/httpserver/https_test.go b/caddyhttp/httpserver/https_test.go index 04a4db1e..04dcbaea 100644 --- a/caddyhttp/httpserver/https_test.go +++ b/caddyhttp/httpserver/https_test.go @@ -11,7 +11,7 @@ import ( func TestRedirPlaintextHost(t *testing.T) { cfg := redirPlaintextHost(&SiteConfig{ Addr: Address{ - Host: "example.com", + Host: "foohost", Port: "1234", }, ListenHost: "93.184.216.34", @@ -19,7 +19,7 @@ func TestRedirPlaintextHost(t *testing.T) { }) // Check host and port - if actual, expected := cfg.Addr.Host, "example.com"; actual != expected { + if actual, expected := cfg.Addr.Host, "foohost"; actual != expected { t.Errorf("Expected redir config to have host %s but got %s", expected, actual) } if actual, expected := cfg.ListenHost, "93.184.216.34"; actual != expected { @@ -38,7 +38,7 @@ func TestRedirPlaintextHost(t *testing.T) { // Check redirect for correctness rec := httptest.NewRecorder() - req, err := http.NewRequest("GET", "http://foo/bar?q=1", nil) + req, err := http.NewRequest("GET", "http://foohost/bar?q=1", nil) if err != nil { t.Fatal(err) } @@ -52,17 +52,17 @@ func TestRedirPlaintextHost(t *testing.T) { if rec.Code != http.StatusMovedPermanently { t.Errorf("Expected status %d but got %d", http.StatusMovedPermanently, rec.Code) } - if got, want := rec.Header().Get("Location"), "https://foo:1234/bar?q=1"; got != want { + if got, want := rec.Header().Get("Location"), "https://foohost:1234/bar?q=1"; got != want { t.Errorf("Expected Location: '%s' but got '%s'", want, got) } // browsers can infer a default port from scheme, so make sure the port // doesn't get added in explicitly for default ports like 443 for https. - cfg = redirPlaintextHost(&SiteConfig{Addr: Address{Host: "example.com", Port: "443"}, TLS: new(caddytls.Config)}) + cfg = redirPlaintextHost(&SiteConfig{Addr: Address{Host: "foohost", Port: "443"}, TLS: new(caddytls.Config)}) handler = cfg.middleware[0](nil) rec = httptest.NewRecorder() - req, err = http.NewRequest("GET", "http://foo/bar?q=1", nil) + req, err = http.NewRequest("GET", "http://foohost/bar?q=1", nil) if err != nil { t.Fatal(err) } @@ -76,7 +76,7 @@ func TestRedirPlaintextHost(t *testing.T) { if rec.Code != http.StatusMovedPermanently { t.Errorf("Expected status %d but got %d", http.StatusMovedPermanently, rec.Code) } - if got, want := rec.Header().Get("Location"), "https://foo/bar?q=1"; got != want { + if got, want := rec.Header().Get("Location"), "https://foohost/bar?q=1"; got != want { t.Errorf("Expected Location: '%s' but got '%s'", want, got) } } diff --git a/caddyhttp/httpserver/plugin.go b/caddyhttp/httpserver/plugin.go index 7736fcf3..4c7caa3e 100644 --- a/caddyhttp/httpserver/plugin.go +++ b/caddyhttp/httpserver/plugin.go @@ -19,6 +19,8 @@ import ( const serverType = "http" func init() { + flag.StringVar(&HTTPPort, "http-port", HTTPPort, "Default port to use for HTTP") + flag.StringVar(&HTTPSPort, "https-port", HTTPSPort, "Default port to use for HTTPS") flag.StringVar(&Host, "host", DefaultHost, "Default host") flag.StringVar(&Port, "port", DefaultPort, "Default port") flag.StringVar(&Root, "root", DefaultRoot, "Root path of default site") @@ -119,11 +121,25 @@ func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []cadd addr.Port = Port } + // If default HTTP or HTTPS ports have been customized, + // make sure the ACME challenge ports match + var altHTTPPort, altTLSSNIPort string + if HTTPPort != DefaultHTTPPort { + altHTTPPort = HTTPPort + } + if HTTPSPort != DefaultHTTPSPort { + altTLSSNIPort = HTTPSPort + } + // Save the config to our master list, and key it for lookups cfg := &SiteConfig{ - Addr: addr, - Root: Root, - TLS: &caddytls.Config{Hostname: addr.Host}, + Addr: addr, + Root: Root, + TLS: &caddytls.Config{ + Hostname: addr.Host, + AltHTTPPort: altHTTPPort, + AltTLSSNIPort: altTLSSNIPort, + }, originCaddyfile: sourceFile, } h.saveConfig(key, cfg) @@ -154,7 +170,7 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) { if !cfg.TLS.Enabled { continue } - if cfg.Addr.Port == "80" || cfg.Addr.Scheme == "http" { + if cfg.Addr.Port == HTTPPort || cfg.Addr.Scheme == "http" { cfg.TLS.Enabled = false log.Printf("[WARNING] TLS disabled for %s", cfg.Addr) } else if cfg.Addr.Scheme == "" { @@ -169,7 +185,7 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) { // this is vital, otherwise the function call below that // sets the listener address will use the default port // instead of 443 because it doesn't know about TLS. - cfg.Addr.Port = "443" + cfg.Addr.Port = HTTPSPort } } @@ -270,7 +286,7 @@ func (a Address) String() string { } scheme := a.Scheme if scheme == "" { - if a.Port == "443" { + if a.Port == HTTPSPort { scheme = "https" } else { scheme = "http" @@ -282,8 +298,8 @@ func (a Address) String() string { } s += a.Host if a.Port != "" && - ((scheme == "https" && a.Port != "443") || - (scheme == "http" && a.Port != "80")) { + ((scheme == "https" && a.Port != DefaultHTTPSPort) || + (scheme == "http" && a.Port != DefaultHTTPPort)) { s += ":" + a.Port } if a.Path != "" { @@ -327,9 +343,9 @@ func standardizeAddress(str string) (Address, error) { // see if we can set port based off scheme if port == "" { if u.Scheme == "http" { - port = "80" + port = HTTPPort } else if u.Scheme == "https" { - port = "443" + port = HTTPSPort } } @@ -339,17 +355,17 @@ func standardizeAddress(str string) (Address, error) { } // error if scheme and port combination violate convention - if (u.Scheme == "http" && port == "443") || (u.Scheme == "https" && port == "80") { + if (u.Scheme == "http" && port == HTTPSPort) || (u.Scheme == "https" && port == HTTPPort) { return Address{}, fmt.Errorf("[%s] scheme and port violate convention", input) } // standardize http and https ports to their respective port numbers if port == "http" { u.Scheme = "http" - port = "80" + port = HTTPPort } else if port == "https" { u.Scheme = "https" - port = "443" + port = HTTPSPort } return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err @@ -477,6 +493,10 @@ const ( DefaultPort = "2015" // DefaultRoot is the default root folder. DefaultRoot = "." + // DefaultHTTPPort is the default port for HTTP. + DefaultHTTPPort = "80" + // DefaultHTTPSPort is the default port for HTTPS. + DefaultHTTPSPort = "443" ) // These "soft defaults" are configurable by @@ -499,4 +519,10 @@ var ( // QUIC indicates whether QUIC is enabled or not. QUIC bool + + // HTTPPort is the port to use for HTTP. + HTTPPort = DefaultHTTPPort + + // HTTPSPort is the port to use for HTTPS. + HTTPSPort = DefaultHTTPSPort ) diff --git a/caddytls/client.go b/caddytls/client.go index 852fbe42..d1c3295e 100644 --- a/caddytls/client.go +++ b/caddytls/client.go @@ -112,33 +112,39 @@ var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error) // Use HTTP and TLS-SNI challenges by default // See if HTTP challenge needs to be proxied - useHTTPPort := "" // empty port value will use challenge default - if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, HTTPChallengePort)) { + useHTTPPort := HTTPChallengePort + if config.AltHTTPPort != "" { useHTTPPort = config.AltHTTPPort - if useHTTPPort == "" { - useHTTPPort = DefaultHTTPAlternatePort - } + } + if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, useHTTPPort)) { + useHTTPPort = DefaultHTTPAlternatePort + } + + // See which port TLS-SNI challenges will be accomplished on + useTLSSNIPort := TLSSNIChallengePort + if config.AltTLSSNIPort != "" { + useTLSSNIPort = config.AltTLSSNIPort } // Always respect user's bind preferences by using config.ListenHost. - // NOTE(Sep'16): At time of writing, SetHTTPAddress() and SetTLSaddress() + // NOTE(Sep'16): At time of writing, SetHTTPAddress() and SetTLSAddress() // must be called before SetChallengeProvider(), since they reset the // challenge provider back to the default one! err := c.acmeClient.SetHTTPAddress(net.JoinHostPort(config.ListenHost, useHTTPPort)) if err != nil { return nil, err } - err = c.acmeClient.SetTLSAddress(net.JoinHostPort(config.ListenHost, "")) + err = c.acmeClient.SetTLSAddress(net.JoinHostPort(config.ListenHost, useTLSSNIPort)) if err != nil { return nil, err } // See if TLS challenge needs to be handled by our own facilities - if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, TLSSNIChallengePort)) { + if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, useTLSSNIPort)) { c.acmeClient.SetChallengeProvider(acme.TLSSNI01, tlsSniSolver{}) } } else { - // Otherwise, DNS challenge it is + // Otherwise, use DNS challenge exclusively // Load provider constructor function provFn, ok := dnsProviders[config.DNSProvider] diff --git a/caddytls/config.go b/caddytls/config.go index d6d77999..a1be44ae 100644 --- a/caddytls/config.go +++ b/caddytls/config.go @@ -84,6 +84,12 @@ type Config struct { // coming in on port 80 to this alternate port AltHTTPPort string + // The alternate port (ONLY port, not host) + // to use for the ACME TLS-SNI challenge. + // The system must forward the standard port + // for the TLS-SNI challenge to this port. + AltTLSSNIPort string + // The string identifier of the DNS provider // to use when solving the ACME DNS challenge DNSProvider string @@ -479,11 +485,11 @@ var defaultCurves = []tls.CurveID{ const ( // HTTPChallengePort is the officially designated port for - // the HTTP challenge. + // the HTTP challenge according to the ACME spec. HTTPChallengePort = "80" // TLSSNIChallengePort is the officially designated port for - // the TLS-SNI challenge. + // the TLS-SNI challenge according to the ACME spec. TLSSNIChallengePort = "443" // DefaultHTTPAlternatePort is the port on which the ACME