blob: d565d2d665fd22d28e2013d0e61a85ed2040c4c7 [file] [log] [blame]
// Copyright 2016 The Upspin Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package https provides a helper for starting an HTTPS server.
package https // import "upspin.io/cloud/https"
import (
"crypto/tls"
"go/build"
"net"
"net/http"
"os"
"path/filepath"
"time"
"golang.org/x/crypto/acme/autocert"
"upspin.io/access"
"upspin.io/errors"
"upspin.io/flags"
"upspin.io/log"
"upspin.io/serverutil"
"upspin.io/shutdown"
)
// Options permits the configuration of TLS certificates for servers running
// outside GCE. The default is the self-signed certificate in
// upspin.io/rpc/testdata.
type Options struct {
// Addr specifies the host and port on which the server should serve
// HTTPS requests (or HTTP requests if InsecureHTTP is set).
// If empty, ":443" is used.
Addr string
// HTTPAddr specifies the host and port on which the server should
// serve HTTP requests. If empty and InsecureHTTP is true, Addr is
// used. If empty otherwise, ":80" is used.
HTTPAddr string
// AutocertCache provides a cache for use with Let's Encrypt.
// If non-nil, enables Let's Encrypt certificates for this server.
// See the comment on ErrAutocertCacheMiss before usin this feature.
AutocertCache AutocertCache
// LetsEncryptCache specifies the cache file for Let's Encrypt.
// If non-empty, enables Let's Encrypt certificates for this server.
LetsEncryptCache string
// LetsEncryptHosts specifies the list of hosts for which we should
// obtain TLS certificates through Let's Encrypt. If LetsEncryptCache
// is specified this should be specified also.
LetsEncryptHosts []string
// CertFile and KeyFile specifies the TLS certificates to use.
// It has no effect if LetsEncryptCache is non-empty.
CertFile string
KeyFile string
// InsecureHTTP specifies whether to serve insecure HTTP without TLS.
// An error occurs if this is attempted with a non-loopback address.
InsecureHTTP bool
}
// AutocertCache is a copy of the autocert.Cache interface, provided here so
// that implementers need not import the autocert package directly.
// See ErrAutocertCacheMiss for more details.
type AutocertCache interface {
autocert.Cache
}
// ErrAutocertCacheMiss is a copy of the autocert.ErrCacheMiss variable that
// must be used by any AutocertCache implementations used in the Options
// struct. This is because the autocert package is vendored by the upspin.io
// repository, and so an outside implementation that returns ErrCacheMiss from
// another version of the package will return an error value that is not
// recognized by the autocert package.
var ErrAutocertCacheMiss = autocert.ErrCacheMiss
var defaultOptions = &Options{
CertFile: filepath.Join(testKeyDir, "cert.pem"),
KeyFile: filepath.Join(testKeyDir, "key.pem"),
}
var testKeyDir = findTestKeyDir() // Do this just once.
// findTestKeyDir locates the "rpc/testdata" directory within the upspin.io
// repository in a Go workspace and returns its absolute path.
// If the upspin.io repository cannot be found, it returns ".".
func findTestKeyDir() string {
p, err := build.Import("upspin.io/rpc/testdata", "", build.FindOnly)
if err != nil {
return "."
}
return p.Dir
}
func (opt *Options) applyDefaults() {
if opt.Addr == "" {
opt.Addr = ":443"
}
if opt.HTTPAddr == "" {
if opt.InsecureHTTP {
opt.HTTPAddr = opt.Addr
} else {
opt.HTTPAddr = ":80"
}
}
if opt.CertFile == "" {
opt.CertFile = defaultOptions.CertFile
}
if opt.KeyFile == "" {
opt.KeyFile = defaultOptions.KeyFile
}
}
// OptionsFromFlags returns Options derived from the command-line flags present
// in the upspin.io/flags package.
func OptionsFromFlags() *Options {
var hosts []string
if host := string(flags.NetAddr); host != "" {
// Make an effort to trim the :port suffix.
if h, _, err := net.SplitHostPort(host); err == nil {
host = h
}
hosts = []string{host}
}
addr := flags.HTTPSAddr
if flags.InsecureHTTP {
addr = flags.HTTPAddr
}
return &Options{
Addr: addr,
HTTPAddr: flags.HTTPAddr,
LetsEncryptCache: flags.LetsEncryptCache,
LetsEncryptHosts: hosts,
CertFile: flags.TLSCertFile,
KeyFile: flags.TLSKeyFile,
InsecureHTTP: flags.InsecureHTTP,
}
}
// ListenAndServeFromFlags is the same as ListenAndServe, but it determines the
// listen address and Options from command-line flags in the flags package.
func ListenAndServeFromFlags(ready chan<- struct{}) {
ListenAndServe(ready, OptionsFromFlags())
}
// ListenAndServe serves the http.DefaultServeMux by HTTPS (and HTTP,
// redirecting to HTTPS) using the provided options.
//
// The given channel, if any, is closed when the TCP listener has succeeded.
// It may be used to signal that the server is ready to start serving requests.
//
// ListenAndServe does not return. It exits the program when the server is
// shut down (via SIGTERM or due to an error) and calls shutdown.Shutdown.
func ListenAndServe(ready chan<- struct{}, opt *Options) {
if opt == nil {
opt = defaultOptions
} else {
opt.applyDefaults()
}
hasLetsEncryptCache := opt.LetsEncryptCache != ""
hasAutocertCache := opt.AutocertCache != nil
hasCert := opt.CertFile != defaultOptions.CertFile || opt.KeyFile != defaultOptions.KeyFile
var manager autocert.Manager
manager.Prompt = autocert.AcceptTOS
if h := opt.LetsEncryptHosts; len(h) > 0 {
manager.HostPolicy = autocert.HostWhitelist(h...)
}
addr := opt.Addr
var config *tls.Config
switch {
case opt.InsecureHTTP:
log.Info.Printf("https: serving insecure HTTP on %q", addr)
host, _, err := net.SplitHostPort(addr)
if err != nil {
log.Fatalf("https: couldn't parse address: %v", err)
}
if !serverutil.IsLoopback(host) {
log.Error.Printf("https: WARNING: serving insecure HTTP on non-loopback address %q", addr)
}
case hasLetsEncryptCache && !hasAutocertCache && !hasCert:
// The -letscache has a default value, so only take this path
// if the other options are not selected.
dir := opt.LetsEncryptCache
log.Info.Printf("https: serving HTTPS on %q using Let's Encrypt certificates", addr)
log.Info.Printf("https: caching Let's Encrypt certificates in %v", dir)
if err := os.MkdirAll(dir, 0700); err != nil {
log.Fatalf("https: could not create -letscache directory: %v", err)
}
manager.Cache = autocert.DirCache(dir)
config = &tls.Config{GetCertificate: manager.GetCertificate}
case hasAutocertCache:
log.Info.Printf("https: serving HTTPS on %q using Let's Encrypt certificates", addr)
manager.Cache = opt.AutocertCache
config = &tls.Config{GetCertificate: manager.GetCertificate}
default:
log.Info.Printf("https: serving HTTPS on %q using provided certificates", addr)
if opt.CertFile == defaultOptions.CertFile || opt.KeyFile == defaultOptions.KeyFile {
log.Error.Print("https: WARNING: using self-signed test certificates.")
}
var err error
config, err = newDefaultTLSConfig(opt.CertFile, opt.KeyFile)
if err != nil {
log.Fatalf("https: setting up TLS config: %v", err)
}
}
// Set up the main listener for HTTPS (or HTTP if InsecureHTTP is set).
ln, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("https: %v", err)
}
shutdown.Handle(func() { ln.Close() })
httpLogger := log.NewStdLogger(log.Info)
if manager.Cache != nil {
// If we're using LetsEncrypt then we need to serve the http-01
// challenge by plain HTTP. We also serve a redirect to HTTPS
// for all other requests.
httpLn, err := net.Listen("tcp", opt.HTTPAddr)
if err != nil {
log.Fatalf("https: %v", err)
}
shutdown.Handle(func() { httpLn.Close() })
httpServer := &http.Server{
Handler: manager.HTTPHandler(nil),
ErrorLog: httpLogger,
}
go func() {
err := httpServer.Serve(httpLn)
log.Printf("https: %v", err)
shutdown.Now(1)
}()
}
if ready != nil {
// Notify the calling packages that
// we're ready to accept requests.
close(ready)
}
// If we're serving HTTPS then wrap the listener with a TLS listener.
if !opt.InsecureHTTP {
ln = tls.NewListener(ln, config)
}
// Set up the main server.
server := &http.Server{
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
// WriteTimeout is set to 0 because it also pertains to
// streaming replies, e.g., the DirServer.Watch interface.
WriteTimeout: 0,
IdleTimeout: 60 * time.Second,
TLSConfig: config,
ErrorLog: httpLogger,
}
// TODO(adg): enable HTTP/2 once it's fast enough
//err := http2.ConfigureServer(server, nil)
//if err != nil {
// log.Fatalf("https: %v", err)
//}
err = server.Serve(ln)
log.Printf("https: %v", err)
shutdown.Now(1)
}
// newDefaultTLSConfig creates a new TLS config based on the certificate files given.
func newDefaultTLSConfig(certFile string, certKeyFile string) (*tls.Config, error) {
const op errors.Op = "cloud/https.newDefaultTLSConfig"
certReadable, err := isReadableFile(certFile)
if err != nil {
return nil, errors.E(op, errors.Invalid, errors.Errorf("SSL certificate in %q: %q", certFile, err))
}
if !certReadable {
return nil, errors.E(op, errors.Invalid, errors.Errorf("certificate file %q not readable", certFile))
}
keyReadable, err := isReadableFile(certKeyFile)
if err != nil {
return nil, errors.E(op, errors.Invalid, errors.Errorf("SSL key in %q: %v", certKeyFile, err))
}
if !keyReadable {
return nil, errors.E(op, errors.Invalid, errors.Errorf("certificate key file %q not readable", certKeyFile))
}
cert, err := tls.LoadX509KeyPair(certFile, certKeyFile)
if err != nil {
return nil, errors.E(op, err)
}
tlsConfig := &tls.Config{
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
MinVersion: tls.VersionTLS12,
PreferServerCipherSuites: true, // Use our choice, not the client's choice
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256, tls.X25519},
Certificates: []tls.Certificate{cert},
}
tlsConfig.BuildNameToCertificate()
return tlsConfig, nil
}
// isReadableFile reports whether the file exists and is readable.
// If the error is non-nil, it means there might be a file or directory
// with that name but we cannot read it.
func isReadableFile(path string) (bool, error) {
// Is it stattable and is it a plain file?
info, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return false, nil // Item does not exist.
}
return false, err // Item is problematic.
}
if info.IsDir() {
return false, errors.Str("is directory")
}
// Is it readable?
fd, err := os.Open(path)
if err != nil {
return false, access.ErrPermissionDenied
}
fd.Close()
return true, nil // Item exists and is readable.
}