From 5f72b7438a7ebf6999f6f8c155a77becf0259bce Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 21 May 2015 00:06:53 -0600 Subject: [PATCH] Created app package, and better TLS compatibility with HTTP/2 --- app/app.go | 76 +++++++++++++++++++++++++++ config/config.go | 52 ++++++++++++++++--- config/setup/tls.go | 72 ++++++++++++++++---------- config/setup/tls_test.go | 62 ++++++++++++++++++++++- main.go | 107 +++++---------------------------------- server/server.go | 8 +-- 6 files changed, 242 insertions(+), 135 deletions(-) create mode 100644 app/app.go diff --git a/app/app.go b/app/app.go new file mode 100644 index 00000000..0463dcb8 --- /dev/null +++ b/app/app.go @@ -0,0 +1,76 @@ +// Package app holds application-global state to make it accessible +// by other packages in the application. +// +// This package differs from config in that the things in app aren't +// really related to server configuration. +package app + +import ( + "errors" + "runtime" + "strconv" + "strings" + "sync" + + "github.com/mholt/caddy/server" +) + +const ( + // Program name + Name = "Caddy" + + // Program version + Version = "0.6.0" +) + +var ( + // Servers is a list of all the currently-listening servers + Servers []*server.Server + + // This mutex protects the Servers slice during changes + ServersMutex sync.Mutex + + // Waiting on Wg will block until all listeners have shut down. + Wg sync.WaitGroup + + // Whether HTTP2 is enabled or not + Http2 bool // TODO: temporary flag until http2 is standard + + // Quiet mode hides non-error initialization output + Quiet bool +) + +// SetCPU parses string cpu and sets GOMAXPROCS +// according to its value. It accepts either +// a number (e.g. 3) or a percent (e.g. 50%). +func SetCPU(cpu string) error { + var numCPU int + + availCPU := runtime.NumCPU() + + if strings.HasSuffix(cpu, "%") { + // Percent + var percent float32 + pctStr := cpu[:len(cpu)-1] + pctInt, err := strconv.Atoi(pctStr) + if err != nil || pctInt < 1 || pctInt > 100 { + return errors.New("Invalid CPU value: percentage must be between 1-100") + } + percent = float32(pctInt) / 100 + numCPU = int(float32(availCPU) * percent) + } else { + // Number + num, err := strconv.Atoi(cpu) + if err != nil || num < 1 { + return errors.New("Invalid CPU value: provide a number or percent greater than 0") + } + numCPU = num + } + + if numCPU > availCPU { + numCPU = availCPU + } + + runtime.GOMAXPROCS(numCPU) + return nil +} diff --git a/config/config.go b/config/config.go index dbc8978b..ce3d550c 100644 --- a/config/config.go +++ b/config/config.go @@ -1,9 +1,13 @@ package config import ( + "errors" + "fmt" "io" "log" + "net" + "github.com/mholt/caddy/app" "github.com/mholt/caddy/config/parse" "github.com/mholt/caddy/config/setup" "github.com/mholt/caddy/middleware" @@ -41,8 +45,8 @@ func Load(filename string, input io.Reader) ([]server.Config, error) { Root: Root, Middleware: make(map[string][]middleware.Middleware), ConfigFile: filename, - AppName: AppName, - AppVersion: AppVersion, + AppName: app.Name, + AppVersion: app.Version, } // It is crucial that directives are executed in the proper order. @@ -81,6 +85,46 @@ func Load(filename string, input io.Reader) ([]server.Config, error) { return configs, nil } +// ArrangeBindings groups configurations by their bind address. For example, +// a server that should listen on localhost and another on 127.0.0.1 will +// be grouped into the same address: 127.0.0.1. It will return an error +// if the address lookup fails or if a TLS listener is configured on the +// same address as a plaintext HTTP listener. The return value is a map of +// bind address to list of configs that would become VirtualHosts on that +// server. +func ArrangeBindings(allConfigs []server.Config) (map[*net.TCPAddr][]server.Config, error) { + addresses := make(map[*net.TCPAddr][]server.Config) + + // Group configs by bind address + for _, conf := range allConfigs { + addr, err := net.ResolveTCPAddr("tcp", conf.Address()) + if err != nil { + return addresses, errors.New("Could not serve " + conf.Address() + " - " + err.Error()) + } + addresses[addr] = append(addresses[addr], conf) + } + + // Don't allow HTTP and HTTPS to be served on the same address + for _, configs := range addresses { + isTLS := configs[0].TLS.Enabled + for _, config := range configs { + if config.TLS.Enabled != isTLS { + thisConfigProto, otherConfigProto := "HTTP", "HTTP" + if config.TLS.Enabled { + thisConfigProto = "HTTPS" + } + if configs[0].TLS.Enabled { + otherConfigProto = "HTTPS" + } + return addresses, fmt.Errorf("Configuration error: Cannot multiplex %s (%s) and %s (%s) on same address", + configs[0].Address(), otherConfigProto, config.Address(), thisConfigProto) + } + } + } + + return addresses, nil +} + // validDirective returns true if d is a valid // directive; false otherwise. func validDirective(d string) bool { @@ -109,7 +153,3 @@ var ( Host = DefaultHost Port = DefaultPort ) - -// The application should set these so that various middlewares -// can access the proper information for their own needs. -var AppName, AppVersion string diff --git a/config/setup/tls.go b/config/setup/tls.go index 74732310..efd6d798 100644 --- a/config/setup/tls.go +++ b/config/setup/tls.go @@ -6,33 +6,10 @@ import ( "strconv" "strings" + "github.com/mholt/caddy/app" "github.com/mholt/caddy/middleware" ) -// Map of supported protocols -// SSLv3 will be not supported in next release -var supportedProtocols = map[string]uint16{ - "ssl3.0": tls.VersionSSL30, - "tls1.0": tls.VersionTLS10, - "tls1.1": tls.VersionTLS11, - "tls1.2": tls.VersionTLS12, -} - -// Map of supported ciphers -// For security reasons caddy will not support RC4 ciphers -var supportedCiphers = map[string]uint16{ - "ECDHE-RSA-AES128-GCM-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - "ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - "ECDHE-RSA-AES128-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, - "ECDHE-RSA-AES256-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - "ECDHE-ECDSA-AES256-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - "ECDHE-ECDSA-AES128-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - "RSA-AES128-CBC-SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, - "RSA-AES256-CBC-SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, - "ECDHE-RSA-3DES-EDE-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, - "RSA-3DES-EDE-CBC-SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, -} - func TLS(c *Controller) (middleware.Middleware, error) { c.TLS.Enabled = true if c.Port == "http" { @@ -79,6 +56,9 @@ func TLS(c *Controller) (middleware.Middleware, error) { if !ok { return nil, c.Errf("Wrong cipher name or cipher not supported '%s'", c.Val()) } + if _, ok := http2CipherSuites[value]; app.Http2 && !ok { + return nil, c.Errf("Cipher suite %s is not allowed for HTTP/2", c.Val()) + } c.TLS.Ciphers = append(c.TLS.Ciphers, value) } case "cache": @@ -87,7 +67,7 @@ func TLS(c *Controller) (middleware.Middleware, error) { } size, err := strconv.Atoi(c.Val()) if err != nil { - return nil, c.Errf("Cache parameter should be an number '%s': %v", c.Val(), err) + return nil, c.Errf("Cache parameter must be an number '%s': %v", c.Val(), err) } c.TLS.CacheSize = size default: @@ -96,10 +76,12 @@ func TLS(c *Controller) (middleware.Middleware, error) { } } - // If no Ciphers provided, use all caddy supportedCiphers + // If no ciphers provided, use all that Caddy supports for the protocol if len(c.TLS.Ciphers) == 0 { for _, v := range supportedCiphers { - c.TLS.Ciphers = append(c.TLS.Ciphers, v) + if _, ok := http2CipherSuites[v]; !app.Http2 || ok { + c.TLS.Ciphers = append(c.TLS.Ciphers, v) + } } } @@ -114,9 +96,43 @@ func TLS(c *Controller) (middleware.Middleware, error) { } //If no cachesize provided, set default to 64 - if c.TLS.CacheSize == 0 { + if c.TLS.CacheSize <= 0 { c.TLS.CacheSize = 64 } return nil, nil } + +// Map of supported protocols +// SSLv3 will be not supported in next release +// HTTP/2 only supports TLS 1.2 and higher +var supportedProtocols = map[string]uint16{ + "ssl3.0": tls.VersionSSL30, + "tls1.0": tls.VersionTLS10, + "tls1.1": tls.VersionTLS11, + "tls1.2": tls.VersionTLS12, +} + +// Map of supported ciphers. +// +// Note that, at time of writing, HTTP/2 blacklists 276 cipher suites, +// including all but two of the suites below (the two GCM suites). +var supportedCiphers = map[string]uint16{ + "ECDHE-RSA-AES128-GCM-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "ECDHE-RSA-AES128-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "ECDHE-RSA-AES256-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + "ECDHE-ECDSA-AES256-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "ECDHE-ECDSA-AES128-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "RSA-AES128-CBC-SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, + "RSA-AES256-CBC-SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, + "ECDHE-RSA-3DES-EDE-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + "RSA-3DES-EDE-CBC-SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, +} + +// Set of cipher suites not blacklisted by HTTP/2 spec. +// See https://http2.github.io/http2-spec/#BadCipherSuites +var http2CipherSuites = map[uint16]struct{}{ + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: struct{}{}, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: struct{}{}, +} diff --git a/config/setup/tls_test.go b/config/setup/tls_test.go index 7ba337de..0daf7f60 100644 --- a/config/setup/tls_test.go +++ b/config/setup/tls_test.go @@ -3,8 +3,29 @@ package setup import ( "crypto/tls" "testing" + + "github.com/mholt/caddy/app" ) +func TestTLSParseBasic(t *testing.T) { + c := newTestController(`tls cert.pem key.pem`) + + _, err := TLS(c) + if err != nil { + t.Error("Expected no errors, but had an error") + } + + if c.TLS.Certificate != "cert.pem" { + t.Errorf("Expected certificate arg to be 'cert.pem', was '%s'", c.TLS.Certificate) + } + if c.TLS.Key != "key.pem" { + t.Errorf("Expected key arg to be 'key.pem', was '%s'", c.TLS.Key) + } + if !c.TLS.Enabled { + t.Error("Expected TLS Enabled=true, but was false") + } +} + func TestTLSParseNoOptional(t *testing.T) { c := newTestController(`tls cert.crt cert.key`) @@ -44,7 +65,6 @@ func TestTLSParseIncompleteParams(t *testing.T) { if err == nil { t.Errorf("Expected errors, but no error returned") } - } func TestTLSParseWithOptionalParams(t *testing.T) { @@ -107,3 +127,43 @@ func TestTLSParseWithWrongOptionalParams(t *testing.T) { t.Errorf("Expected errors, but no error returned") } } + +func TestTLSParseWithHTTP2Requirements(t *testing.T) { + params := `tls cert.crt cert.key` + c := newTestController(params) + + // With HTTP2, cipher suites should be limited + app.Http2 = true + _, err := TLS(c) + if err != nil { + t.Errorf("Expected no errors, got: %v", err) + } + if len(c.TLS.Ciphers) != len(http2CipherSuites) { + t.Errorf("With HTTP/2 on, expected %d supported ciphers, got %d", + len(http2CipherSuites), len(c.TLS.Ciphers)) + } + + params = `tls cert.crt cert.key { + ciphers RSA-AES128-CBC-SHA + }` + c = newTestController(params) + // Should not be able to specify a blacklisted cipher suite with HTTP2 on + _, err = TLS(c) + if err == nil { + t.Error("Expected an error because cipher suite is invalid for HTTP/2") + } + + params = `tls cert.crt cert.key` + c = newTestController(params) + + // Without HTTP2, cipher suites should not be as restricted + app.Http2 = false + _, err = TLS(c) + if err != nil { + t.Errorf("Expected no errors, got: %v", err) + } + if len(c.TLS.Ciphers) != len(supportedCiphers) { + t.Errorf("With HTTP/2 off, expected %d supported ciphers, got %d", + len(supportedCiphers), len(c.TLS.Ciphers)) + } +} diff --git a/main.go b/main.go index 4947c3d4..c66bf867 100644 --- a/main.go +++ b/main.go @@ -2,58 +2,49 @@ package main import ( "bytes" - "errors" "flag" "fmt" "io/ioutil" "log" - "net" "os" "os/exec" "path" "runtime" "strconv" "strings" - "sync" + "github.com/mholt/caddy/app" "github.com/mholt/caddy/config" "github.com/mholt/caddy/server" ) var ( conf string - http2 bool // TODO: temporary flag until http2 is standard - quiet bool cpu string version bool ) func init() { flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+config.DefaultConfigFile+")") - flag.BoolVar(&http2, "http2", true, "Enable HTTP/2 support") // TODO: temporary flag until http2 merged into std lib - flag.BoolVar(&quiet, "quiet", false, "Quiet mode (no initialization output)") + flag.BoolVar(&app.Http2, "http2", true, "Enable HTTP/2 support") // TODO: temporary flag until http2 merged into std lib + flag.BoolVar(&app.Quiet, "quiet", false, "Quiet mode (no initialization output)") flag.StringVar(&cpu, "cpu", "100%", "CPU cap") flag.StringVar(&config.Root, "root", config.DefaultRoot, "Root path to default site") flag.StringVar(&config.Host, "host", config.DefaultHost, "Default host") flag.StringVar(&config.Port, "port", config.DefaultPort, "Default port") flag.BoolVar(&version, "version", false, "Show version") - - config.AppName = "Caddy" - config.AppVersion = "0.6.0" } func main() { flag.Parse() if version { - fmt.Printf("%s %s\n", config.AppName, config.AppVersion) + fmt.Printf("%s %s\n", app.Name, app.Version) os.Exit(0) } - var wg sync.WaitGroup - // Set CPU cap - err := setCPU(cpu) + err := app.SetCPU(cpu) if err != nil { log.Fatal(err) } @@ -65,7 +56,7 @@ func main() { } // Group by address (virtual hosts) - addresses, err := arrangeBindings(allConfigs) + addresses, err := config.ArrangeBindings(allConfigs) if err != nil { log.Fatal(err) } @@ -76,18 +67,21 @@ func main() { if err != nil { log.Fatal(err) } - s.HTTP2 = http2 // TODO: This setting is temporary - wg.Add(1) + s.HTTP2 = app.Http2 // TODO: This setting is temporary + app.Wg.Add(1) go func(s *server.Server) { - defer wg.Done() + defer app.Wg.Done() err := s.Serve() if err != nil { log.Fatal(err) // kill whole process to avoid a half-alive zombie server } }(s) - if !quiet { + app.Servers = append(app.Servers, s) + + if !app.Quiet { var checkedFdLimit bool + for addr, configs := range addresses { for _, conf := range configs { // Print address of site @@ -117,7 +111,7 @@ func main() { } } - wg.Wait() + app.Wg.Wait() } func isLocalhost(s string) bool { @@ -169,76 +163,3 @@ func loadConfigs() ([]server.Config, error) { return config.Load(config.DefaultConfigFile, file) } - -// arrangeBindings groups configurations by their bind address. For example, -// a server that should listen on localhost and another on 127.0.0.1 will -// be grouped into the same address: 127.0.0.1. It will return an error -// if the address lookup fails or if a TLS listener is configured on the -// same address as a plaintext HTTP listener. -func arrangeBindings(allConfigs []server.Config) (map[*net.TCPAddr][]server.Config, error) { - addresses := make(map[*net.TCPAddr][]server.Config) - - // Group configs by bind address - for _, conf := range allConfigs { - addr, err := net.ResolveTCPAddr("tcp", conf.Address()) - if err != nil { - return addresses, errors.New("Could not serve " + conf.Address() + " - " + err.Error()) - } - addresses[addr] = append(addresses[addr], conf) - } - - // Don't allow HTTP and HTTPS to be served on the same address - for _, configs := range addresses { - isTLS := configs[0].TLS.Enabled - for _, config := range configs { - if config.TLS.Enabled != isTLS { - thisConfigProto, otherConfigProto := "HTTP", "HTTP" - if config.TLS.Enabled { - thisConfigProto = "HTTPS" - } - if configs[0].TLS.Enabled { - otherConfigProto = "HTTPS" - } - return addresses, fmt.Errorf("Configuration error: Cannot multiplex %s (%s) and %s (%s) on same address", - configs[0].Address(), otherConfigProto, config.Address(), thisConfigProto) - } - } - } - - return addresses, nil -} - -// setCPU parses string cpu and sets GOMAXPROCS -// according to its value. It accepts either -// a number (e.g. 3) or a percent (e.g. 50%). -func setCPU(cpu string) error { - var numCPU int - - availCPU := runtime.NumCPU() - - if strings.HasSuffix(cpu, "%") { - // Percent - var percent float32 - pctStr := cpu[:len(cpu)-1] - pctInt, err := strconv.Atoi(pctStr) - if err != nil || pctInt < 1 || pctInt > 100 { - return errors.New("Invalid CPU value: percentage must be between 1-100") - } - percent = float32(pctInt) / 100 - numCPU = int(float32(availCPU) * percent) - } else { - // Number - num, err := strconv.Atoi(cpu) - if err != nil || num < 1 { - return errors.New("Invalid CPU value: provide a number or percent greater than 0") - } - numCPU = num - } - - if numCPU > availCPU { - numCPU = availCPU - } - - runtime.GOMAXPROCS(numCPU) - return nil -} diff --git a/server/server.go b/server/server.go index e139e760..8290e9f5 100644 --- a/server/server.go +++ b/server/server.go @@ -132,17 +132,11 @@ func ListenAndServeTLSWithSNI(srv *http.Server, tlsConfigs []TLSConfig) error { } config.BuildNameToCertificate() - // Here we change some crypto/tls defaults based on caddyfile - // If no config provided, we set defaults focused in security - - // Add a session cache LRU algorithm + // Customize our TLS configuration config.ClientSessionCache = tls.NewLRUClientSessionCache(tlsConfigs[0].CacheSize) - config.MinVersion = tlsConfigs[0].ProtocolMinVersion config.MaxVersion = tlsConfigs[0].ProtocolMaxVersion config.CipherSuites = tlsConfigs[0].Ciphers - - // Server ciphers have priority over client ciphers config.PreferServerCipherSuites = true conn, err := net.Listen("tcp", addr)