195 lines
4.0 KiB
Go
195 lines
4.0 KiB
Go
package https
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/caddyserver/certmagic"
|
|
"github.com/khlieng/dispatch/pkg/netutil"
|
|
"github.com/klauspost/cpuid"
|
|
)
|
|
|
|
type Config struct {
|
|
Addr string
|
|
PortHTTP string
|
|
PortHTTPS string
|
|
HTTPOnly bool
|
|
|
|
StoragePath string
|
|
Domain string
|
|
Email string
|
|
|
|
Cert string
|
|
Key string
|
|
}
|
|
|
|
func Serve(handler http.Handler, cfg Config) error {
|
|
errCh := make(chan error, 1)
|
|
|
|
httpSrv := &http.Server{
|
|
Addr: net.JoinHostPort(cfg.Addr, cfg.PortHTTP),
|
|
}
|
|
|
|
if !cfg.HTTPOnly {
|
|
httpSrv.ReadTimeout = 5 * time.Second
|
|
httpSrv.WriteTimeout = 5 * time.Second
|
|
|
|
httpsSrv := &http.Server{
|
|
Addr: net.JoinHostPort(cfg.Addr, cfg.PortHTTPS),
|
|
ReadTimeout: 5 * time.Second,
|
|
IdleTimeout: 120 * time.Second,
|
|
Handler: handler,
|
|
}
|
|
|
|
redirect := HTTPSRedirect(cfg.PortHTTPS, handler)
|
|
|
|
if cfg.Cert != "" || cfg.Key != "" {
|
|
httpSrv.Handler = redirect
|
|
httpsSrv.TLSConfig = TLSConfig(nil)
|
|
|
|
go func() {
|
|
errCh <- httpSrv.ListenAndServe()
|
|
}()
|
|
|
|
go func() {
|
|
errCh <- httpsSrv.ListenAndServeTLS(cfg.Cert, cfg.Key)
|
|
}()
|
|
} else {
|
|
if cfg.StoragePath != "" {
|
|
certmagic.Default.Storage = &certmagic.FileStorage{
|
|
Path: cfg.StoragePath,
|
|
}
|
|
}
|
|
|
|
certmagic.Default.MustStaple = true
|
|
|
|
magic := certmagic.NewDefault()
|
|
|
|
acme := certmagic.NewACMEManager(magic, certmagic.ACMEManager{
|
|
Agreed: true,
|
|
Email: cfg.Email,
|
|
})
|
|
|
|
magic.Issuer = acme
|
|
|
|
domains := []string{cfg.Domain}
|
|
if cfg.Domain == "" {
|
|
domains = []string{}
|
|
magic.OnDemand = maxObtain(3)
|
|
}
|
|
|
|
err := magic.ManageSync(domains)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
httpSrv.Handler = acme.HTTPChallengeHandler(redirect)
|
|
httpsSrv.TLSConfig = TLSConfig(magic.TLSConfig())
|
|
|
|
go func() {
|
|
errCh <- httpSrv.ListenAndServe()
|
|
}()
|
|
|
|
go func() {
|
|
errCh <- httpsSrv.ListenAndServeTLS("", "")
|
|
}()
|
|
}
|
|
} else {
|
|
httpSrv.ReadTimeout = 5 * time.Second
|
|
httpSrv.IdleTimeout = 120 * time.Second
|
|
httpSrv.Handler = handler
|
|
|
|
return httpSrv.ListenAndServe()
|
|
}
|
|
|
|
return <-errCh
|
|
}
|
|
|
|
func HTTPSRedirect(portHTTPS string, fallback http.Handler) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
host, _, err := net.SplitHostPort(r.Host)
|
|
if err != nil {
|
|
host = r.Host
|
|
}
|
|
|
|
if fallback != nil && netutil.IsPrivate(host) {
|
|
fallback.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
u := url.URL{
|
|
Scheme: "https",
|
|
Host: net.JoinHostPort(host, portHTTPS),
|
|
Path: r.RequestURI,
|
|
}
|
|
|
|
w.Header().Set("Connection", "close")
|
|
w.Header().Set("Location", u.String())
|
|
w.WriteHeader(http.StatusMovedPermanently)
|
|
}
|
|
}
|
|
|
|
func TLSConfig(tlsConfig *tls.Config) *tls.Config {
|
|
if tlsConfig == nil {
|
|
tlsConfig = &tls.Config{}
|
|
}
|
|
|
|
tlsConfig.MinVersion = tls.VersionTLS12
|
|
tlsConfig.CipherSuites = defaultCipherSuites()
|
|
tlsConfig.CurvePreferences = []tls.CurveID{
|
|
tls.X25519,
|
|
tls.CurveP256,
|
|
}
|
|
tlsConfig.PreferServerCipherSuites = true
|
|
|
|
return tlsConfig
|
|
}
|
|
|
|
func defaultCipherSuites() []uint16 {
|
|
if cpuid.CPU.AesNi() {
|
|
return []uint16{
|
|
tls.TLS_FALLBACK_SCSV,
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
|
}
|
|
}
|
|
|
|
return []uint16{
|
|
tls.TLS_FALLBACK_SCSV,
|
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}
|
|
}
|
|
|
|
func maxObtain(limit int) *certmagic.OnDemandConfig {
|
|
requested := []string{}
|
|
|
|
return &certmagic.OnDemandConfig{
|
|
DecisionFunc: func(name string) error {
|
|
for _, n := range requested {
|
|
if name == n {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if len(requested) == limit {
|
|
return fmt.Errorf("OnDemand cert limit reached")
|
|
}
|
|
|
|
requested = append(requested, name)
|
|
return nil
|
|
},
|
|
}
|
|
}
|