Automatic TLS certificate'ing with Let's Encrypt
To use Let's Encrypt: - Set `acme_domain_name` to the domain name of the machine. - Set both `listen` and `listen_https` to the ports Flamenco Manager should be listening to. By default these are `:8080` and `:8443`. - Configure your firewall or user-facing proxy to forward ports 80 and 443 to respectively 8080 and 8443. Other changes: - Added setting `listen_https` which is used for serving HTTPS traffic (default `:8443`). If you are using the `tlskey`/`tlscert` settings, you need to move `listen` to `listen_https`. - Changed the default value for `listen` to `:8080` (was `:8083`). The changes to the default were somewhat necessary to get to more standard port numbers; it would be silly to add the standard port number 8443 and still keep using the nonstandard 8083. A new webserver wrapper was introduced that manages both the HTTP and HTTPS servers as a single unit. When using ACME/Let's Encrypt it is necessary to have both HTTP (for the ACME web authentication) and HTTPS (for regular traffic). All other HTTP traffic is redirected to HTTPS on port 443. This does *not* redirect to the configured `listen_https` port because firewall-based redirection or reverse proxies may be in use. Actually, this is recommended because then Flamenco Manager doesn't need to be run as root.
This commit is contained in:
176
httpserver/httpserver.go
Normal file
176
httpserver/httpserver.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/armadillica/flamenco-manager/flamenco"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
// Constants for the HTTP servers.
|
||||
const (
|
||||
ReadTimeout = 15 * time.Second
|
||||
)
|
||||
|
||||
// Server acts as a http.Server
|
||||
type Server interface {
|
||||
Shutdown(ctx context.Context)
|
||||
ListenAndServe() error
|
||||
Done() <-chan struct{}
|
||||
}
|
||||
|
||||
// Combined has one or two HTTP servers.
|
||||
type Combined struct {
|
||||
httpServer *http.Server
|
||||
httpsServer *http.Server
|
||||
|
||||
tlsKey string
|
||||
tlsCert string
|
||||
|
||||
expectShutdown bool
|
||||
mutex sync.Mutex
|
||||
shutdownComplete chan struct{} // closed when ListenAndServe stops serving.
|
||||
}
|
||||
|
||||
// New returns a new HTTP server that can handle HTTP, HTTPS, and both for ACME.
|
||||
func New(config flamenco.Conf, handler http.Handler) Server {
|
||||
server := Combined{
|
||||
mutex: sync.Mutex{},
|
||||
shutdownComplete: make(chan struct{}),
|
||||
}
|
||||
|
||||
switch {
|
||||
case config.HasCustomTLS():
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"tlscert": config.TLSCert,
|
||||
"tlskey": config.TLSKey,
|
||||
"listen_https": config.ListenHTTPS,
|
||||
}).Info("creating HTTPS-enabled server")
|
||||
server.httpsServer = &http.Server{
|
||||
Addr: config.ListenHTTPS,
|
||||
Handler: handler,
|
||||
ReadTimeout: ReadTimeout,
|
||||
}
|
||||
server.tlsKey = config.TLSKey
|
||||
server.tlsCert = config.TLSCert
|
||||
|
||||
case config.ACMEDomainName != "":
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"acme_domain_name": config.ACMEDomainName,
|
||||
"listen": config.Listen,
|
||||
"listen_https": config.ListenHTTPS,
|
||||
}).Info("creating ACME/Let's Encrypt enabled server")
|
||||
mgr := autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
HostPolicy: autocert.HostWhitelist(config.ACMEDomainName),
|
||||
Cache: autocert.DirCache("tlscerts"),
|
||||
}
|
||||
|
||||
server.httpServer = &http.Server{
|
||||
Addr: config.Listen,
|
||||
Handler: mgr.HTTPHandler(nil),
|
||||
}
|
||||
|
||||
server.httpsServer = &http.Server{
|
||||
Addr: config.ListenHTTPS,
|
||||
Handler: handler,
|
||||
ReadTimeout: ReadTimeout,
|
||||
TLSConfig: mgr.TLSConfig(),
|
||||
}
|
||||
|
||||
default:
|
||||
logrus.WithField("listen", config.Listen).Info("creating insecure server")
|
||||
server.httpServer = &http.Server{
|
||||
Addr: config.Listen,
|
||||
Handler: handler,
|
||||
ReadTimeout: ReadTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
return &server
|
||||
}
|
||||
|
||||
// Shutdown shuts down both HTTP and HTTPS server.
|
||||
func (s *Combined) Shutdown(ctx context.Context) {
|
||||
s.mutex.Lock()
|
||||
s.expectShutdown = true
|
||||
s.mutex.Unlock()
|
||||
|
||||
if s.httpServer != nil {
|
||||
s.httpServer.Shutdown(ctx)
|
||||
}
|
||||
if s.httpsServer != nil {
|
||||
s.httpsServer.Shutdown(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Done returns a channel that is closed when the server is done serving.
|
||||
func (s *Combined) Done() <-chan struct{} {
|
||||
return s.shutdownComplete
|
||||
}
|
||||
|
||||
func (s *Combined) mustBeFresh() {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if s.expectShutdown {
|
||||
panic("this HTTP server was already shut down, unable to restart")
|
||||
}
|
||||
}
|
||||
|
||||
// ListenAndServe listens on both HTTP and HTTPS servers.
|
||||
func (s *Combined) ListenAndServe() error {
|
||||
s.mustBeFresh()
|
||||
defer close(s.shutdownComplete)
|
||||
|
||||
var httpError, httpsError error
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
if s.httpServer != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
logger := logrus.WithField("listen", s.httpServer.Addr)
|
||||
logger.Debug("starting HTTP server")
|
||||
err := s.httpServer.ListenAndServe()
|
||||
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if !s.expectShutdown {
|
||||
logger.WithError(httpError).Error("HTTP server unexpectedly stopped")
|
||||
httpError = err
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
if s.httpsServer != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
logger := logrus.WithField("listen_https", s.httpsServer.Addr)
|
||||
logger.Debug("starting HTTPS server")
|
||||
err := s.httpsServer.ListenAndServeTLS(s.tlsCert, s.tlsKey)
|
||||
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if !s.expectShutdown {
|
||||
logger.WithError(err).Error("HTTPS server unexpectedly stopped")
|
||||
httpsError = err
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// We can only return one error.
|
||||
if httpsError != nil {
|
||||
return httpsError
|
||||
}
|
||||
return httpError
|
||||
}
|
||||
Reference in New Issue
Block a user