Merge pull request #1500 from mholt/customports

httpserver: Flags to customize HTTP and HTTPS ports (including for ACME challenges)
This commit is contained in:
Matt Holt 2017-03-07 11:31:47 -07:00 committed by GitHub
commit df9d062a8f
6 changed files with 84 additions and 44 deletions

View File

@ -134,7 +134,7 @@ func (c Context) Port() (string, error) {
if err != nil { if err != nil {
if !strings.Contains(c.Req.Host, ":") { if !strings.Contains(c.Req.Host, ":") {
// common with sites served on the default port 80 // common with sites served on the default port 80
return "80", nil return HTTPPort, nil
} }
return "", err return "", err
} }

View File

@ -93,7 +93,7 @@ func enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error {
cfg.TLS.Enabled && cfg.TLS.Enabled &&
(!cfg.TLS.Manual || cfg.TLS.OnDemand) && (!cfg.TLS.Manual || cfg.TLS.OnDemand) &&
cfg.Addr.Host != "localhost" { cfg.Addr.Host != "localhost" {
cfg.Addr.Port = "443" cfg.Addr.Port = HTTPSPort
} }
} }
return nil return nil
@ -108,8 +108,8 @@ func enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error {
func makePlaintextRedirects(allConfigs []*SiteConfig) []*SiteConfig { func makePlaintextRedirects(allConfigs []*SiteConfig) []*SiteConfig {
for i, cfg := range allConfigs { for i, cfg := range allConfigs {
if cfg.TLS.Managed && if cfg.TLS.Managed &&
!hostHasOtherPort(allConfigs, i, "80") && !hostHasOtherPort(allConfigs, i, HTTPPort) &&
(cfg.Addr.Port == "443" || !hostHasOtherPort(allConfigs, i, "443")) { (cfg.Addr.Port == HTTPSPort || !hostHasOtherPort(allConfigs, i, HTTPSPort)) {
allConfigs = append(allConfigs, redirPlaintextHost(cfg)) 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 // redirPlaintextHost returns a new plaintext HTTP configuration for
// a virtualHost that simply redirects to cfg, which is assumed to // a virtualHost that simply redirects to cfg, which is assumed to
// be the HTTPS configuration. The returned configuration is set // 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 { func redirPlaintextHost(cfg *SiteConfig) *SiteConfig {
redirPort := cfg.Addr.Port redirPort := cfg.Addr.Port
if redirPort == "443" { if redirPort == DefaultHTTPSPort {
// default port is redundant redirPort = "" // default port is redundant
redirPort = ""
} }
redirMiddleware := func(next Handler) Handler { redirMiddleware := func(next Handler) Handler {
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { return HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
toURL := "https://" + r.Host toURL := "https://"
if redirPort != "" { if redirPort == "" {
toURL += ":" + 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() toURL += r.URL.RequestURI()
w.Header().Set("Connection", "close") w.Header().Set("Connection", "close")
@ -155,12 +156,13 @@ func redirPlaintextHost(cfg *SiteConfig) *SiteConfig {
}) })
} }
host := cfg.Addr.Host host := cfg.Addr.Host
port := "80" port := HTTPPort
addr := net.JoinHostPort(host, port) addr := net.JoinHostPort(host, port)
return &SiteConfig{ return &SiteConfig{
Addr: Address{Original: addr, Host: host, Port: port}, Addr: Address{Original: addr, Host: host, Port: port},
ListenHost: cfg.ListenHost, ListenHost: cfg.ListenHost,
middleware: []Middleware{redirMiddleware}, middleware: []Middleware{redirMiddleware},
TLS: &caddytls.Config{AltHTTPPort: cfg.TLS.AltHTTPPort}, TLS: &caddytls.Config{AltHTTPPort: cfg.TLS.AltHTTPPort, AltTLSSNIPort: cfg.TLS.AltTLSSNIPort},
Timeouts: cfg.Timeouts,
} }
} }

View File

@ -11,7 +11,7 @@ import (
func TestRedirPlaintextHost(t *testing.T) { func TestRedirPlaintextHost(t *testing.T) {
cfg := redirPlaintextHost(&SiteConfig{ cfg := redirPlaintextHost(&SiteConfig{
Addr: Address{ Addr: Address{
Host: "example.com", Host: "foohost",
Port: "1234", Port: "1234",
}, },
ListenHost: "93.184.216.34", ListenHost: "93.184.216.34",
@ -19,7 +19,7 @@ func TestRedirPlaintextHost(t *testing.T) {
}) })
// Check host and port // 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) 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 { if actual, expected := cfg.ListenHost, "93.184.216.34"; actual != expected {
@ -38,7 +38,7 @@ func TestRedirPlaintextHost(t *testing.T) {
// Check redirect for correctness // Check redirect for correctness
rec := httptest.NewRecorder() 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -52,17 +52,17 @@ func TestRedirPlaintextHost(t *testing.T) {
if rec.Code != http.StatusMovedPermanently { if rec.Code != http.StatusMovedPermanently {
t.Errorf("Expected status %d but got %d", http.StatusMovedPermanently, rec.Code) 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) t.Errorf("Expected Location: '%s' but got '%s'", want, got)
} }
// browsers can infer a default port from scheme, so make sure the port // 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. // 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) handler = cfg.middleware[0](nil)
rec = httptest.NewRecorder() 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -76,7 +76,7 @@ func TestRedirPlaintextHost(t *testing.T) {
if rec.Code != http.StatusMovedPermanently { if rec.Code != http.StatusMovedPermanently {
t.Errorf("Expected status %d but got %d", http.StatusMovedPermanently, rec.Code) 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) t.Errorf("Expected Location: '%s' but got '%s'", want, got)
} }
} }

View File

@ -19,6 +19,8 @@ import (
const serverType = "http" const serverType = "http"
func init() { 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(&Host, "host", DefaultHost, "Default host")
flag.StringVar(&Port, "port", DefaultPort, "Default port") flag.StringVar(&Port, "port", DefaultPort, "Default port")
flag.StringVar(&Root, "root", DefaultRoot, "Root path of default site") 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 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 // Save the config to our master list, and key it for lookups
cfg := &SiteConfig{ cfg := &SiteConfig{
Addr: addr, Addr: addr,
Root: Root, Root: Root,
TLS: &caddytls.Config{Hostname: addr.Host}, TLS: &caddytls.Config{
Hostname: addr.Host,
AltHTTPPort: altHTTPPort,
AltTLSSNIPort: altTLSSNIPort,
},
originCaddyfile: sourceFile, originCaddyfile: sourceFile,
} }
h.saveConfig(key, cfg) h.saveConfig(key, cfg)
@ -154,7 +170,7 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) {
if !cfg.TLS.Enabled { if !cfg.TLS.Enabled {
continue continue
} }
if cfg.Addr.Port == "80" || cfg.Addr.Scheme == "http" { if cfg.Addr.Port == HTTPPort || cfg.Addr.Scheme == "http" {
cfg.TLS.Enabled = false cfg.TLS.Enabled = false
log.Printf("[WARNING] TLS disabled for %s", cfg.Addr) log.Printf("[WARNING] TLS disabled for %s", cfg.Addr)
} else if cfg.Addr.Scheme == "" { } 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 // this is vital, otherwise the function call below that
// sets the listener address will use the default port // sets the listener address will use the default port
// instead of 443 because it doesn't know about TLS. // 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 scheme := a.Scheme
if scheme == "" { if scheme == "" {
if a.Port == "443" { if a.Port == HTTPSPort {
scheme = "https" scheme = "https"
} else { } else {
scheme = "http" scheme = "http"
@ -282,8 +298,8 @@ func (a Address) String() string {
} }
s += a.Host s += a.Host
if a.Port != "" && if a.Port != "" &&
((scheme == "https" && a.Port != "443") || ((scheme == "https" && a.Port != DefaultHTTPSPort) ||
(scheme == "http" && a.Port != "80")) { (scheme == "http" && a.Port != DefaultHTTPPort)) {
s += ":" + a.Port s += ":" + a.Port
} }
if a.Path != "" { if a.Path != "" {
@ -327,9 +343,9 @@ func standardizeAddress(str string) (Address, error) {
// see if we can set port based off scheme // see if we can set port based off scheme
if port == "" { if port == "" {
if u.Scheme == "http" { if u.Scheme == "http" {
port = "80" port = HTTPPort
} else if u.Scheme == "https" { } 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 // 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) return Address{}, fmt.Errorf("[%s] scheme and port violate convention", input)
} }
// standardize http and https ports to their respective port numbers // standardize http and https ports to their respective port numbers
if port == "http" { if port == "http" {
u.Scheme = "http" u.Scheme = "http"
port = "80" port = HTTPPort
} else if port == "https" { } else if port == "https" {
u.Scheme = "https" u.Scheme = "https"
port = "443" port = HTTPSPort
} }
return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err
@ -477,6 +493,10 @@ const (
DefaultPort = "2015" DefaultPort = "2015"
// DefaultRoot is the default root folder. // DefaultRoot is the default root folder.
DefaultRoot = "." 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 // These "soft defaults" are configurable by
@ -499,4 +519,10 @@ var (
// QUIC indicates whether QUIC is enabled or not. // QUIC indicates whether QUIC is enabled or not.
QUIC bool QUIC bool
// HTTPPort is the port to use for HTTP.
HTTPPort = DefaultHTTPPort
// HTTPSPort is the port to use for HTTPS.
HTTPSPort = DefaultHTTPSPort
) )

View File

@ -112,33 +112,39 @@ var newACMEClient = func(config *Config, allowPrompts bool) (*ACMEClient, error)
// Use HTTP and TLS-SNI challenges by default // Use HTTP and TLS-SNI challenges by default
// See if HTTP challenge needs to be proxied // See if HTTP challenge needs to be proxied
useHTTPPort := "" // empty port value will use challenge default useHTTPPort := HTTPChallengePort
if caddy.HasListenerWithAddress(net.JoinHostPort(config.ListenHost, HTTPChallengePort)) { if config.AltHTTPPort != "" {
useHTTPPort = 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. // 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 // must be called before SetChallengeProvider(), since they reset the
// challenge provider back to the default one! // challenge provider back to the default one!
err := c.acmeClient.SetHTTPAddress(net.JoinHostPort(config.ListenHost, useHTTPPort)) err := c.acmeClient.SetHTTPAddress(net.JoinHostPort(config.ListenHost, useHTTPPort))
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = c.acmeClient.SetTLSAddress(net.JoinHostPort(config.ListenHost, "")) err = c.acmeClient.SetTLSAddress(net.JoinHostPort(config.ListenHost, useTLSSNIPort))
if err != nil { if err != nil {
return nil, err return nil, err
} }
// See if TLS challenge needs to be handled by our own facilities // 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{}) c.acmeClient.SetChallengeProvider(acme.TLSSNI01, tlsSniSolver{})
} }
} else { } else {
// Otherwise, DNS challenge it is // Otherwise, use DNS challenge exclusively
// Load provider constructor function // Load provider constructor function
provFn, ok := dnsProviders[config.DNSProvider] provFn, ok := dnsProviders[config.DNSProvider]

View File

@ -84,6 +84,12 @@ type Config struct {
// coming in on port 80 to this alternate port // coming in on port 80 to this alternate port
AltHTTPPort string 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 // The string identifier of the DNS provider
// to use when solving the ACME DNS challenge // to use when solving the ACME DNS challenge
DNSProvider string DNSProvider string
@ -479,11 +485,11 @@ var defaultCurves = []tls.CurveID{
const ( const (
// HTTPChallengePort is the officially designated port for // HTTPChallengePort is the officially designated port for
// the HTTP challenge. // the HTTP challenge according to the ACME spec.
HTTPChallengePort = "80" HTTPChallengePort = "80"
// TLSSNIChallengePort is the officially designated port for // TLSSNIChallengePort is the officially designated port for
// the TLS-SNI challenge. // the TLS-SNI challenge according to the ACME spec.
TLSSNIChallengePort = "443" TLSSNIChallengePort = "443"
// DefaultHTTPAlternatePort is the port on which the ACME // DefaultHTTPAlternatePort is the port on which the ACME