diff --git a/caddyhttp/httpserver/server.go b/caddyhttp/httpserver/server.go index 7345a0d71..a9b0e8465 100644 --- a/caddyhttp/httpserver/server.go +++ b/caddyhttp/httpserver/server.go @@ -31,6 +31,7 @@ type Server struct { connTimeout time.Duration // max time to wait for a connection before force stop tlsGovChan chan struct{} // close to stop the TLS maintenance goroutine vhosts *vhostTrie + tlsConfig caddytls.ConfigGroup } // ensure it satisfies the interface @@ -72,16 +73,31 @@ func NewServer(addr string, group []*SiteConfig) (*Server, error) { } // Set up TLS configuration - var tlsConfigs []*caddytls.Config + tlsConfigs := make(caddytls.ConfigGroup) + var allConfigs []*caddytls.Config + for _, site := range group { - tlsConfigs = append(tlsConfigs, site.TLS) + + if err := site.TLS.Build(tlsConfigs); err != nil { + return nil, err + } + + tlsConfigs[site.TLS.Hostname] = site.TLS + allConfigs = append(allConfigs, site.TLS) } - var err error - s.Server.TLSConfig, err = caddytls.MakeTLSConfig(tlsConfigs) - if err != nil { + + // Check if configs are valid + if err := caddytls.CheckConfigs(allConfigs); err != nil { return nil, err } + s.tlsConfig = tlsConfigs + + s.Server.TLSConfig = &tls.Config{ + GetConfigForClient: s.tlsConfig.GetConfigForClient, + GetCertificate: s.tlsConfig.GetCertificate, + } + // As of Go 1.7, HTTP/2 is enabled only if NextProtos includes the string "h2" if HTTP2 && s.Server.TLSConfig != nil && len(s.Server.TLSConfig.NextProtos) == 0 { s.Server.TLSConfig.NextProtos = []string{"h2"} diff --git a/caddyhttp/proxy/reverseproxy.go b/caddyhttp/proxy/reverseproxy.go index 57b478294..2627658a4 100644 --- a/caddyhttp/proxy/reverseproxy.go +++ b/caddyhttp/proxy/reverseproxy.go @@ -442,7 +442,7 @@ func newConnHijackerTransport(base http.RoundTripper) *connHijackerTransport { if b, _ := base.(*http.Transport); b != nil { tlsClientConfig := b.TLSClientConfig if tlsClientConfig.NextProtos != nil { - tlsClientConfig = cloneTLSClientConfig(tlsClientConfig) + tlsClientConfig = tlsClientConfig.Clone() tlsClientConfig.NextProtos = nil } @@ -566,37 +566,6 @@ func (tlsHandshakeTimeoutError) Timeout() bool { return true } func (tlsHandshakeTimeoutError) Temporary() bool { return true } func (tlsHandshakeTimeoutError) Error() string { return "net/http: TLS handshake timeout" } -// cloneTLSClientConfig is like cloneTLSConfig but omits -// the fields SessionTicketsDisabled and SessionTicketKey. -// This makes it safe to call cloneTLSClientConfig on a config -// in active use by a server. -func cloneTLSClientConfig(cfg *tls.Config) *tls.Config { - if cfg == nil { - return &tls.Config{} - } - return &tls.Config{ - Rand: cfg.Rand, - Time: cfg.Time, - Certificates: cfg.Certificates, - NameToCertificate: cfg.NameToCertificate, - GetCertificate: cfg.GetCertificate, - RootCAs: cfg.RootCAs, - NextProtos: cfg.NextProtos, - ServerName: cfg.ServerName, - ClientAuth: cfg.ClientAuth, - ClientCAs: cfg.ClientCAs, - InsecureSkipVerify: cfg.InsecureSkipVerify, - CipherSuites: cfg.CipherSuites, - PreferServerCipherSuites: cfg.PreferServerCipherSuites, - ClientSessionCache: cfg.ClientSessionCache, - MinVersion: cfg.MinVersion, - MaxVersion: cfg.MaxVersion, - CurvePreferences: cfg.CurvePreferences, - DynamicRecordSizingDisabled: cfg.DynamicRecordSizingDisabled, - Renegotiation: cfg.Renegotiation, - } -} - func requestIsWebsocket(req *http.Request) bool { return strings.ToLower(req.Header.Get("Upgrade")) == "websocket" && strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade") } diff --git a/caddytls/config.go b/caddytls/config.go index 61683f02f..33a16fcc7 100644 --- a/caddytls/config.go +++ b/caddytls/config.go @@ -108,6 +108,12 @@ type Config struct { // Add the must staple TLS extension to the CSR generated by lego/acme MustStaple bool + + // Disables HTTP2 completely + DisableHTTP2 bool + + // Holds final tls.Config + tlsConfig *tls.Config } // OnDemandState contains some state relevant for providing @@ -217,88 +223,70 @@ func (c *Config) StorageFor(caURL string) (Storage, error) { return s, nil } -// MakeTLSConfig reduces configs into a single tls.Config. -// If TLS is to be disabled, a nil tls.Config will be returned. -func MakeTLSConfig(configs []*Config) (*tls.Config, error) { - if len(configs) == 0 { - return nil, nil +func (cfg *Config) Build(group ConfigGroup) error { + config, err := cfg.build() + + if err != nil { + return err } + cfg.tlsConfig = config + cfg.tlsConfig.GetCertificate = group.GetCertificate + return nil +} + +func (cfg *Config) build() (*tls.Config, error) { config := new(tls.Config) + ciphersAdded := make(map[uint16]struct{}) curvesAdded := make(map[tls.CurveID]struct{}) - configMap := make(configGroup) - for i, cfg := range configs { - if cfg == nil { - // avoid nil pointer dereference below - configs[i] = new(Config) - continue - } - - // Key this config by its hostname; this - // overwrites configs with the same hostname - configMap[cfg.Hostname] = cfg - - // Can't serve TLS and not-TLS on same port - if i > 0 && cfg.Enabled != configs[i-1].Enabled { - thisConfProto, lastConfProto := "not TLS", "not TLS" - if cfg.Enabled { - thisConfProto = "TLS" - } - if configs[i-1].Enabled { - lastConfProto = "TLS" - } - return nil, fmt.Errorf("cannot multiplex %s (%s) and %s (%s) on same listener", - configs[i-1].Hostname, lastConfProto, cfg.Hostname, thisConfProto) - } - - if !cfg.Enabled { - continue - } - - // Union cipher suites - for _, ciph := range cfg.Ciphers { - if _, ok := ciphersAdded[ciph]; !ok { - ciphersAdded[ciph] = struct{}{} - config.CipherSuites = append(config.CipherSuites, ciph) - } - } - - // Can't resolve conflicting PreferServerCipherSuites settings - if i > 0 && cfg.PreferServerCipherSuites != configs[i-1].PreferServerCipherSuites { - return nil, fmt.Errorf("cannot both PreferServerCipherSuites and not prefer them") - } - config.PreferServerCipherSuites = cfg.PreferServerCipherSuites - - // Union curves - for _, curv := range cfg.CurvePreferences { - if _, ok := curvesAdded[curv]; !ok { - curvesAdded[curv] = struct{}{} - config.CurvePreferences = append(config.CurvePreferences, curv) - } - } - - // Go with the widest range of protocol versions - if config.MinVersion == 0 || cfg.ProtocolMinVersion < config.MinVersion { - config.MinVersion = cfg.ProtocolMinVersion - } - if cfg.ProtocolMaxVersion > config.MaxVersion { - config.MaxVersion = cfg.ProtocolMaxVersion - } - - // Go with the strictest ClientAuth type - if cfg.ClientAuth > config.ClientAuth { - config.ClientAuth = cfg.ClientAuth + // Add cipher suites + for _, ciph := range cfg.Ciphers { + if _, ok := ciphersAdded[ciph]; !ok { + ciphersAdded[ciph] = struct{}{} + config.CipherSuites = append(config.CipherSuites, ciph) } } - // Is TLS disabled? If so, we're done here. - // By now, we know that all configs agree - // whether it is or not, so we can just look - // at the first one. - if len(configs) == 0 || !configs[0].Enabled { - return nil, nil + config.PreferServerCipherSuites = cfg.PreferServerCipherSuites + + // Union curves + for _, curv := range cfg.CurvePreferences { + if _, ok := curvesAdded[curv]; !ok { + curvesAdded[curv] = struct{}{} + config.CurvePreferences = append(config.CurvePreferences, curv) + } + } + + config.MinVersion = cfg.ProtocolMinVersion + config.MaxVersion = cfg.ProtocolMaxVersion + config.ClientAuth = cfg.ClientAuth + + // Set up client authentication if enabled + if config.ClientAuth != tls.NoClientCert { + pool := x509.NewCertPool() + clientCertsAdded := make(map[string]struct{}) + + for _, caFile := range cfg.ClientCerts { + // don't add cert to pool more than once + if _, ok := clientCertsAdded[caFile]; ok { + continue + } + clientCertsAdded[caFile] = struct{}{} + + // Any client with a certificate from this CA will be allowed to connect + caCrt, err := ioutil.ReadFile(caFile) + if err != nil { + return nil, err + } + + if !pool.AppendCertsFromPEM(caCrt) { + return nil, fmt.Errorf("error loading client certificate '%s': no certificates were successfully parsed", caFile) + } + } + + config.ClientCAs = pool } // Default cipher suites @@ -311,43 +299,44 @@ func MakeTLSConfig(configs []*Config) (*tls.Config, error) { config.CipherSuites = append([]uint16{tls.TLS_FALLBACK_SCSV}, config.CipherSuites...) } - // Default curves - if len(config.CurvePreferences) == 0 { - config.CurvePreferences = defaultCurves + if cfg.DisableHTTP2 { + config.NextProtos = []string{} + } else { + config.NextProtos = []string{"h2"} } - // Set up client authentication if enabled - if config.ClientAuth != tls.NoClientCert { - pool := x509.NewCertPool() - clientCertsAdded := make(map[string]struct{}) - for _, cfg := range configs { - for _, caFile := range cfg.ClientCerts { - // don't add cert to pool more than once - if _, ok := clientCertsAdded[caFile]; ok { - continue - } - clientCertsAdded[caFile] = struct{}{} - - // Any client with a certificate from this CA will be allowed to connect - caCrt, err := ioutil.ReadFile(caFile) - if err != nil { - return nil, err - } - - if !pool.AppendCertsFromPEM(caCrt) { - return nil, fmt.Errorf("error loading client certificate '%s': no certificates were successfully parsed", caFile) - } - } - } - config.ClientCAs = pool - } - - // Associate the GetCertificate callback, or almost nothing we just did will work - config.GetCertificate = configMap.GetCertificate - return config, nil } +// CheckConfigs checks if multiple TLS configs does not collide with each other +func CheckConfigs(configs []*Config) error { + if len(configs) == 0 { + return nil + } + + for i, cfg := range configs { + + // Can't serve TLS and not-TLS on same port + if i > 0 && cfg.Enabled != configs[i-1].Enabled { + thisConfProto, lastConfProto := "not TLS", "not TLS" + if cfg.Enabled { + thisConfProto = "TLS" + } + if configs[i-1].Enabled { + lastConfProto = "TLS" + } + return fmt.Errorf("cannot multiplex %s (%s) and %s (%s) on same listener", + configs[i-1].Hostname, lastConfProto, cfg.Hostname, thisConfProto) + } + + if !cfg.Enabled { + continue + } + } + + return nil +} + // ConfigGetter gets a Config keyed by key. type ConfigGetter func(c *caddy.Controller) *Config diff --git a/caddytls/config_test.go b/caddytls/config_test.go index 2ca6547c2..6440de43a 100644 --- a/caddytls/config_test.go +++ b/caddytls/config_test.go @@ -10,14 +10,12 @@ import ( func TestMakeTLSConfigProtocolVersions(t *testing.T) { // same min and max protocol versions - configs := []*Config{ - { - Enabled: true, - ProtocolMinVersion: tls.VersionTLS12, - ProtocolMaxVersion: tls.VersionTLS12, - }, + config := Config{ + Enabled: true, + ProtocolMinVersion: tls.VersionTLS12, + ProtocolMaxVersion: tls.VersionTLS12, } - result, err := MakeTLSConfig(configs) + result, err := config.build() if err != nil { t.Fatalf("Did not expect an error, but got %v", err) } @@ -31,28 +29,14 @@ func TestMakeTLSConfigProtocolVersions(t *testing.T) { func TestMakeTLSConfigPreferServerCipherSuites(t *testing.T) { // prefer server cipher suites - configs := []*Config{{Enabled: true, PreferServerCipherSuites: true}} - result, err := MakeTLSConfig(configs) + config := Config{Enabled: true, PreferServerCipherSuites: true} + result, err := config.build() if err != nil { t.Fatalf("Did not expect an error, but got %v", err) } if got, want := result.PreferServerCipherSuites, true; got != want { t.Errorf("Expected PreferServerCipherSuites==%v but got %v", want, got) } - - // make sure we don't get an error if there's a conflict - // when both of the configs have TLS disabled - configs = []*Config{ - {Enabled: false, PreferServerCipherSuites: false}, - {Enabled: false, PreferServerCipherSuites: true}, - } - result, err = MakeTLSConfig(configs) - if err != nil { - t.Fatalf("Did not expect an error when TLS is disabled, but got '%v'", err) - } - if result != nil { - t.Errorf("Expected nil result because TLS disabled, got: %+v", err) - } } func TestMakeTLSConfigTLSEnabledDisabled(t *testing.T) { @@ -61,20 +45,10 @@ func TestMakeTLSConfigTLSEnabledDisabled(t *testing.T) { {Enabled: true}, {Enabled: false}, } - _, err := MakeTLSConfig(configs) + err := CheckConfigs(configs) if err == nil { t.Fatalf("Expected an error, but got %v", err) } - - // verify that when disabled, a nil pair is returned - configs = []*Config{{}, {}} - result, err := MakeTLSConfig(configs) - if err != nil { - t.Errorf("Did not expect an error, but got %v", err) - } - if result != nil { - t.Errorf("Expected a nil *tls.Config result, got %+v", result) - } } func TestMakeTLSConfigCipherSuites(t *testing.T) { @@ -83,25 +57,22 @@ func TestMakeTLSConfigCipherSuites(t *testing.T) { configs := []*Config{ {Enabled: true, Ciphers: []uint16{0xc02c, 0xc030}}, {Enabled: true, Ciphers: []uint16{0xc012, 0xc030, 0xc00a}}, - } - result, err := MakeTLSConfig(configs) - if err != nil { - t.Fatalf("Did not expect an error, but got %v", err) - } - expected := []uint16{tls.TLS_FALLBACK_SCSV, 0xc02c, 0xc030, 0xc012, 0xc00a} - if !reflect.DeepEqual(result.CipherSuites, expected) { - t.Errorf("Expected ciphers %v but got %v", expected, result.CipherSuites) + {Enabled: true, Ciphers: nil}, } - // use default suites if none specified - configs = []*Config{{Enabled: true}} - result, err = MakeTLSConfig(configs) - if err != nil { - t.Fatalf("Did not expect an error, but got %v", err) + expectedCiphers := [][]uint16{ + {tls.TLS_FALLBACK_SCSV, 0xc02c, 0xc030}, + {tls.TLS_FALLBACK_SCSV, 0xc012, 0xc030, 0xc00a}, + append([]uint16{tls.TLS_FALLBACK_SCSV}, defaultCiphers...), } - expected = append([]uint16{tls.TLS_FALLBACK_SCSV}, defaultCiphers...) - if !reflect.DeepEqual(result.CipherSuites, expected) { - t.Errorf("Expected default ciphers %v but got %v", expected, result.CipherSuites) + + for i, config := range configs { + cfg, _ := config.build() + + if !reflect.DeepEqual(cfg.CipherSuites, expectedCiphers[i]) { + t.Errorf("Expected ciphers %v but got %v", expectedCiphers[i], cfg.CipherSuites) + } + } } diff --git a/caddytls/handshake.go b/caddytls/handshake.go index 3f1cd1c8b..eaf6422ff 100644 --- a/caddytls/handshake.go +++ b/caddytls/handshake.go @@ -15,7 +15,7 @@ import ( // (hostnames can have wildcard characters; use the getConfig // method to get a config by matching its hostname). Its // GetCertificate function can be used with tls.Config. -type configGroup map[string]*Config +type ConfigGroup map[string]*Config // getConfig gets the config by the first key match for name. // In other words, "sub.foo.bar" will get the config for "*.foo.bar" @@ -24,7 +24,7 @@ type configGroup map[string]*Config // // This function follows nearly the same logic to lookup // a hostname as the getCertificate function uses. -func (cg configGroup) getConfig(name string) *Config { +func (cg ConfigGroup) getConfig(name string) *Config { name = strings.ToLower(name) // exact match? great, let's use it @@ -58,11 +58,27 @@ func (cg configGroup) getConfig(name string) *Config { // via ACME. // // This method is safe for use as a tls.Config.GetCertificate callback. -func (cg configGroup) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { +func (cg ConfigGroup) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { cert, err := cg.getCertDuringHandshake(strings.ToLower(clientHello.ServerName), true, true) return &cert.Certificate, err } +// GetConfigForClient gets a TLS configuration satisfying clientHello. In getting +// the configuration, it abides the rules and settings defined in the +// Config that matches clientHello.ServerName. +// +// This method is safe for use as a tls.Config.GetConfigForClient callback. +func (cg ConfigGroup) GetConfigForClient(clientHello *tls.ClientHelloInfo) (*tls.Config, error) { + + config := cg.getConfig(clientHello.ServerName) + + if config != nil { + return config.tlsConfig, nil + } + + return nil, nil +} + // getCertDuringHandshake will get a certificate for name. It first tries // the in-memory cache. If no certificate for name is in the cache, the // config most closely corresponding to name will be loaded. If that config @@ -74,7 +90,7 @@ func (cg configGroup) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Cer // certificate is available. // // This function is safe for concurrent use. -func (cg configGroup) getCertDuringHandshake(name string, loadIfNecessary, obtainIfNecessary bool) (Certificate, error) { +func (cg ConfigGroup) getCertDuringHandshake(name string, loadIfNecessary, obtainIfNecessary bool) (Certificate, error) { // First check our in-memory cache to see if we've already loaded it cert, matched, defaulted := getCertificate(name) if matched { @@ -127,7 +143,7 @@ func (cg configGroup) getCertDuringHandshake(name string, loadIfNecessary, obtai // now according to mitigating factors we keep track of and preferences the // user has set. If a non-nil error is returned, do not issue a new certificate // for name. -func (cg configGroup) checkLimitsForObtainingNewCerts(name string, cfg *Config) error { +func (cg ConfigGroup) checkLimitsForObtainingNewCerts(name string, cfg *Config) error { // User can set hard limit for number of certs for the process to issue if cfg.OnDemandState.MaxObtain > 0 && atomic.LoadInt32(&cfg.OnDemandState.ObtainedCount) >= cfg.OnDemandState.MaxObtain { @@ -160,7 +176,7 @@ func (cg configGroup) checkLimitsForObtainingNewCerts(name string, cfg *Config) // name, it will wait and use what the other goroutine obtained. // // This function is safe for use by multiple concurrent goroutines. -func (cg configGroup) obtainOnDemandCertificate(name string, cfg *Config) (Certificate, error) { +func (cg ConfigGroup) obtainOnDemandCertificate(name string, cfg *Config) (Certificate, error) { // We must protect this process from happening concurrently, so synchronize. obtainCertWaitChansMu.Lock() wait, ok := obtainCertWaitChans[name] @@ -219,7 +235,7 @@ func (cg configGroup) obtainOnDemandCertificate(name string, cfg *Config) (Certi // validity. // // This function is safe for use by multiple concurrent goroutines. -func (cg configGroup) handshakeMaintenance(name string, cert Certificate) (Certificate, error) { +func (cg ConfigGroup) handshakeMaintenance(name string, cert Certificate) (Certificate, error) { // Check cert expiration timeLeft := cert.NotAfter.Sub(time.Now().UTC()) if timeLeft < RenewDurationBefore { @@ -252,7 +268,7 @@ func (cg configGroup) handshakeMaintenance(name string, cert Certificate) (Certi // usable. name should already be lower-cased before calling this function. // // This function is safe for use by multiple concurrent goroutines. -func (cg configGroup) renewDynamicCertificate(name string, cfg *Config) (Certificate, error) { +func (cg ConfigGroup) renewDynamicCertificate(name string, cfg *Config) (Certificate, error) { obtainCertWaitChansMu.Lock() wait, ok := obtainCertWaitChans[name] if ok { diff --git a/caddytls/handshake_test.go b/caddytls/handshake_test.go index 6abfb767f..b7e35b3a9 100644 --- a/caddytls/handshake_test.go +++ b/caddytls/handshake_test.go @@ -9,7 +9,7 @@ import ( func TestGetCertificate(t *testing.T) { defer func() { certCache = make(map[string]Certificate) }() - cg := make(configGroup) + cg := make(ConfigGroup) hello := &tls.ClientHelloInfo{ServerName: "example.com"} helloSub := &tls.ClientHelloInfo{ServerName: "sub.example.com"} diff --git a/caddytls/setup.go b/caddytls/setup.go index d789674c1..3fb9d02b1 100644 --- a/caddytls/setup.go +++ b/caddytls/setup.go @@ -164,6 +164,20 @@ func setupTLS(c *caddy.Controller) error { return c.Errf("Unsupported Storage provider '%s'", args[0]) } config.StorageProvider = args[0] + + case "http2": + args := c.RemainingArgs() + if len(args) != 1 { + return c.ArgErr() + } + + switch args[0] { + case "off": + config.DisableHTTP2 = true + default: + c.ArgErr() + } + case "muststaple": config.MustStaple = true default: diff --git a/caddytls/setup_test.go b/caddytls/setup_test.go index a141afe81..ce0c9adb5 100644 --- a/caddytls/setup_test.go +++ b/caddytls/setup_test.go @@ -91,6 +91,10 @@ func TestSetupParseBasic(t *testing.T) { t.Error("Expected PreferServerCipherSuites = true, but was false") } + if cfg.DisableHTTP2 { + t.Error("Expected HTTP2 to be enabled by default") + } + // Ensure curve count is correct if len(cfg.CurvePreferences) != len(defaultCurves) { t.Errorf("Expected %v Curves, got %v", len(defaultCurves), len(cfg.CurvePreferences)) @@ -118,6 +122,7 @@ func TestSetupParseWithOptionalParams(t *testing.T) { protocols tls1.0 tls1.2 ciphers RSA-AES256-CBC-SHA ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 muststaple + http2 off }` cfg := new(Config) RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg }) @@ -141,7 +146,11 @@ func TestSetupParseWithOptionalParams(t *testing.T) { } if !cfg.MustStaple { - t.Errorf("Expected must staple to be true") + t.Error("Expected must staple to be true") + } + + if !cfg.DisableHTTP2 { + t.Error("Expected HTTP2 to be disabled") } } @@ -184,7 +193,7 @@ func TestSetupParseWithWrongOptionalParams(t *testing.T) { c = caddy.NewTestController("", params) err = setupTLS(c) if err == nil { - t.Errorf("Expected errors, but no error returned") + t.Error("Expected errors, but no error returned") } // Test key_type wrong params @@ -196,7 +205,7 @@ func TestSetupParseWithWrongOptionalParams(t *testing.T) { c = caddy.NewTestController("", params) err = setupTLS(c) if err == nil { - t.Errorf("Expected errors, but no error returned") + t.Error("Expected errors, but no error returned") } // Test curves wrong params @@ -208,7 +217,7 @@ func TestSetupParseWithWrongOptionalParams(t *testing.T) { c = caddy.NewTestController("", params) err = setupTLS(c) if err == nil { - t.Errorf("Expected errors, but no error returned") + t.Error("Expected errors, but no error returned") } } @@ -222,7 +231,7 @@ func TestSetupParseWithClientAuth(t *testing.T) { c := caddy.NewTestController("", params) err := setupTLS(c) if err == nil { - t.Errorf("Expected an error, but no error returned") + t.Error("Expected an error, but no error returned") } noCAs, twoCAs := []string{}, []string{"client_ca.crt", "client2_ca.crt"}