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
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
......@@ -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{}{},
}
......@@ -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))
}
}
......@@ -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
}
......@@ -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)
......
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