Commit 5f72b743 authored by Matthew Holt's avatar Matthew Holt

Created app package, and better TLS compatibility with HTTP/2

parent ea960730
// 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
}
package config package config
import ( import (
"errors"
"fmt"
"io" "io"
"log" "log"
"net"
"github.com/mholt/caddy/app"
"github.com/mholt/caddy/config/parse" "github.com/mholt/caddy/config/parse"
"github.com/mholt/caddy/config/setup" "github.com/mholt/caddy/config/setup"
"github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware"
...@@ -41,8 +45,8 @@ func Load(filename string, input io.Reader) ([]server.Config, error) { ...@@ -41,8 +45,8 @@ func Load(filename string, input io.Reader) ([]server.Config, error) {
Root: Root, Root: Root,
Middleware: make(map[string][]middleware.Middleware), Middleware: make(map[string][]middleware.Middleware),
ConfigFile: filename, ConfigFile: filename,
AppName: AppName, AppName: app.Name,
AppVersion: AppVersion, AppVersion: app.Version,
} }
// It is crucial that directives are executed in the proper order. // 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) { ...@@ -81,6 +85,46 @@ func Load(filename string, input io.Reader) ([]server.Config, error) {
return configs, nil 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 // validDirective returns true if d is a valid
// directive; false otherwise. // directive; false otherwise.
func validDirective(d string) bool { func validDirective(d string) bool {
...@@ -109,7 +153,3 @@ var ( ...@@ -109,7 +153,3 @@ var (
Host = DefaultHost Host = DefaultHost
Port = DefaultPort Port = DefaultPort
) )
// The application should set these so that various middlewares
// can access the proper information for their own needs.
var AppName, AppVersion string
...@@ -6,33 +6,10 @@ import ( ...@@ -6,33 +6,10 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/mholt/caddy/app"
"github.com/mholt/caddy/middleware" "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) { func TLS(c *Controller) (middleware.Middleware, error) {
c.TLS.Enabled = true c.TLS.Enabled = true
if c.Port == "http" { if c.Port == "http" {
...@@ -79,6 +56,9 @@ func TLS(c *Controller) (middleware.Middleware, error) { ...@@ -79,6 +56,9 @@ func TLS(c *Controller) (middleware.Middleware, error) {
if !ok { if !ok {
return nil, c.Errf("Wrong cipher name or cipher not supported '%s'", c.Val()) 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) c.TLS.Ciphers = append(c.TLS.Ciphers, value)
} }
case "cache": case "cache":
...@@ -87,7 +67,7 @@ func TLS(c *Controller) (middleware.Middleware, error) { ...@@ -87,7 +67,7 @@ func TLS(c *Controller) (middleware.Middleware, error) {
} }
size, err := strconv.Atoi(c.Val()) size, err := strconv.Atoi(c.Val())
if err != nil { 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 c.TLS.CacheSize = size
default: default:
...@@ -96,12 +76,14 @@ func TLS(c *Controller) (middleware.Middleware, error) { ...@@ -96,12 +76,14 @@ 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 { if len(c.TLS.Ciphers) == 0 {
for _, v := range supportedCiphers { for _, v := range supportedCiphers {
if _, ok := http2CipherSuites[v]; !app.Http2 || ok {
c.TLS.Ciphers = append(c.TLS.Ciphers, v) c.TLS.Ciphers = append(c.TLS.Ciphers, v)
} }
} }
}
// If no ProtocolMin provided, set default MinVersion to TLSv1.1 for security reasons // If no ProtocolMin provided, set default MinVersion to TLSv1.1 for security reasons
if c.TLS.ProtocolMinVersion == 0 { if c.TLS.ProtocolMinVersion == 0 {
...@@ -114,9 +96,43 @@ func TLS(c *Controller) (middleware.Middleware, error) { ...@@ -114,9 +96,43 @@ func TLS(c *Controller) (middleware.Middleware, error) {
} }
//If no cachesize provided, set default to 64 //If no cachesize provided, set default to 64
if c.TLS.CacheSize == 0 { if c.TLS.CacheSize <= 0 {
c.TLS.CacheSize = 64 c.TLS.CacheSize = 64
} }
return nil, nil 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{}{},
}
...@@ -3,8 +3,29 @@ package setup ...@@ -3,8 +3,29 @@ package setup
import ( import (
"crypto/tls" "crypto/tls"
"testing" "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) { func TestTLSParseNoOptional(t *testing.T) {
c := newTestController(`tls cert.crt cert.key`) c := newTestController(`tls cert.crt cert.key`)
...@@ -44,7 +65,6 @@ func TestTLSParseIncompleteParams(t *testing.T) { ...@@ -44,7 +65,6 @@ func TestTLSParseIncompleteParams(t *testing.T) {
if err == nil { if err == nil {
t.Errorf("Expected errors, but no error returned") t.Errorf("Expected errors, but no error returned")
} }
} }
func TestTLSParseWithOptionalParams(t *testing.T) { func TestTLSParseWithOptionalParams(t *testing.T) {
...@@ -107,3 +127,43 @@ func TestTLSParseWithWrongOptionalParams(t *testing.T) { ...@@ -107,3 +127,43 @@ func TestTLSParseWithWrongOptionalParams(t *testing.T) {
t.Errorf("Expected errors, but no error returned") 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))
}
}
...@@ -2,58 +2,49 @@ package main ...@@ -2,58 +2,49 @@ package main
import ( import (
"bytes" "bytes"
"errors"
"flag" "flag"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"net"
"os" "os"
"os/exec" "os/exec"
"path" "path"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"sync"
"github.com/mholt/caddy/app"
"github.com/mholt/caddy/config" "github.com/mholt/caddy/config"
"github.com/mholt/caddy/server" "github.com/mholt/caddy/server"
) )
var ( var (
conf string conf string
http2 bool // TODO: temporary flag until http2 is standard
quiet bool
cpu string cpu string
version bool version bool
) )
func init() { func init() {
flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+config.DefaultConfigFile+")") 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(&app.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.Quiet, "quiet", false, "Quiet mode (no initialization output)")
flag.StringVar(&cpu, "cpu", "100%", "CPU cap") flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
flag.StringVar(&config.Root, "root", config.DefaultRoot, "Root path to default site") flag.StringVar(&config.Root, "root", config.DefaultRoot, "Root path to default site")
flag.StringVar(&config.Host, "host", config.DefaultHost, "Default host") flag.StringVar(&config.Host, "host", config.DefaultHost, "Default host")
flag.StringVar(&config.Port, "port", config.DefaultPort, "Default port") flag.StringVar(&config.Port, "port", config.DefaultPort, "Default port")
flag.BoolVar(&version, "version", false, "Show version") flag.BoolVar(&version, "version", false, "Show version")
config.AppName = "Caddy"
config.AppVersion = "0.6.0"
} }
func main() { func main() {
flag.Parse() flag.Parse()
if version { if version {
fmt.Printf("%s %s\n", config.AppName, config.AppVersion) fmt.Printf("%s %s\n", app.Name, app.Version)
os.Exit(0) os.Exit(0)
} }
var wg sync.WaitGroup
// Set CPU cap // Set CPU cap
err := setCPU(cpu) err := app.SetCPU(cpu)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
...@@ -65,7 +56,7 @@ func main() { ...@@ -65,7 +56,7 @@ func main() {
} }
// Group by address (virtual hosts) // Group by address (virtual hosts)
addresses, err := arrangeBindings(allConfigs) addresses, err := config.ArrangeBindings(allConfigs)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
...@@ -76,18 +67,21 @@ func main() { ...@@ -76,18 +67,21 @@ func main() {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
s.HTTP2 = http2 // TODO: This setting is temporary s.HTTP2 = app.Http2 // TODO: This setting is temporary
wg.Add(1) app.Wg.Add(1)
go func(s *server.Server) { go func(s *server.Server) {
defer wg.Done() defer app.Wg.Done()
err := s.Serve() err := s.Serve()
if err != nil { if err != nil {
log.Fatal(err) // kill whole process to avoid a half-alive zombie server log.Fatal(err) // kill whole process to avoid a half-alive zombie server
} }
}(s) }(s)
if !quiet { app.Servers = append(app.Servers, s)
if !app.Quiet {
var checkedFdLimit bool var checkedFdLimit bool
for addr, configs := range addresses { for addr, configs := range addresses {
for _, conf := range configs { for _, conf := range configs {
// Print address of site // Print address of site
...@@ -117,7 +111,7 @@ func main() { ...@@ -117,7 +111,7 @@ func main() {
} }
} }
wg.Wait() app.Wg.Wait()
} }
func isLocalhost(s string) bool { func isLocalhost(s string) bool {
...@@ -169,76 +163,3 @@ func loadConfigs() ([]server.Config, error) { ...@@ -169,76 +163,3 @@ func loadConfigs() ([]server.Config, error) {
return config.Load(config.DefaultConfigFile, file) 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
}
...@@ -132,17 +132,11 @@ func ListenAndServeTLSWithSNI(srv *http.Server, tlsConfigs []TLSConfig) error { ...@@ -132,17 +132,11 @@ func ListenAndServeTLSWithSNI(srv *http.Server, tlsConfigs []TLSConfig) error {
} }
config.BuildNameToCertificate() config.BuildNameToCertificate()
// Here we change some crypto/tls defaults based on caddyfile // Customize our TLS configuration
// If no config provided, we set defaults focused in security
// Add a session cache LRU algorithm
config.ClientSessionCache = tls.NewLRUClientSessionCache(tlsConfigs[0].CacheSize) config.ClientSessionCache = tls.NewLRUClientSessionCache(tlsConfigs[0].CacheSize)
config.MinVersion = tlsConfigs[0].ProtocolMinVersion config.MinVersion = tlsConfigs[0].ProtocolMinVersion
config.MaxVersion = tlsConfigs[0].ProtocolMaxVersion config.MaxVersion = tlsConfigs[0].ProtocolMaxVersion
config.CipherSuites = tlsConfigs[0].Ciphers config.CipherSuites = tlsConfigs[0].Ciphers
// Server ciphers have priority over client ciphers
config.PreferServerCipherSuites = true config.PreferServerCipherSuites = true
conn, err := net.Listen("tcp", addr) conn, err := net.Listen("tcp", addr)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment