From 9151bfb3be188ebd254b6e1a2a0f9988723795cd Mon Sep 17 00:00:00 2001 From: mstar Date: Tue, 27 May 2025 14:06:08 +0200 Subject: [PATCH] Add initial tls support - Uses certificate and key files provided by admin in config - Let's Encrypt integration planned, but not even close to working - Initial HTTP3 Support added --- auth-new/tls.go | 117 +++++++++++++++++++++------ config/config.go | 12 ++- go.mod | 6 ++ go.sum | 18 +++++ web/public/middleware/httpUpgrade.go | 40 +++++++++ web/public/server.go | 99 +++++++++++++++++++---- 6 files changed, 250 insertions(+), 42 deletions(-) create mode 100644 web/public/middleware/httpUpgrade.go diff --git a/auth-new/tls.go b/auth-new/tls.go index d8135df..9c18afb 100644 --- a/auth-new/tls.go +++ b/auth-new/tls.go @@ -5,8 +5,10 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" - "crypto/tls" "crypto/x509" + "database/sql" + "errors" + "os" "strings" "time" @@ -40,62 +42,99 @@ func (l *leUser) GetPrivateKey() crypto.PrivateKey { return l.Key } -func TlsFromConfig() (*tls.Config, error) { +// Return the path to the certificate file (.crt), the key file (.key) and any errors +func TlsFromConfig() (string, string, error) { if config.GlobalConfig.SSL.UseLetsEncrypt != nil && *config.GlobalConfig.SSL.UseLetsEncrypt { return tlsFromLetsEncrypt() + } else { + return tlsFromFile() } - panic("Not implemented") } -func tlsFromLetsEncrypt() (*tls.Config, error) { +// Verifies that the relevant config entries are set and are paths to readable files +func tlsFromFile() (string, string, error) { + sslConf := config.GlobalConfig.SSL + if sslConf.CertificateFile == nil { + return "", "", errors.New("need a path to a certificate file to use for tls") + } + if sslConf.CertKeyFile == nil { + return "", "", errors.New("need a path to a certificate key file to use for tls") + } + certInfo, err := os.Stat(*sslConf.CertificateFile) + if err != nil { + return "", "", err + } + if certInfo.IsDir() { + return "", "", errors.New("certificate file is a directory, needs to be a file") + } + keyInfo, err := os.Stat(*sslConf.CertKeyFile) + if err != nil { + return "", "", err + } + if keyInfo.IsDir() { + return "", "", errors.New("key file is a directory, needs to be a file") + } + return *sslConf.CertificateFile, *sslConf.CertKeyFile, nil +} + +// Get the LetsEncrypt generated certifiates and store them in temporary files for the server to use +// If the certificate is too old, requests a new one. +func tlsFromLetsEncrypt() (string, string, error) { metadata, err := dbgen.ServerMetadata.First() if err != nil { - return nil, err + return "", "", err } if !metadata.LELastUpdate.Valid { - return generateLetsEncryptCert(metadata) + if err = generateLetsEncryptCert(metadata); err != nil { + return "", "", err + } } else if metadata.LELastUpdate.Time.Add(time.Hour * 24 * 60).Before(time.Now()) { // Let's Encrypt certificates last for 90 days. It is recommended to renew every 60 days // See https://letsencrypt.org/docs/faq/#what-is-the-lifetime-for-let-s-encrypt-certificates-for-how-long-are-they-valid - return generateLetsEncryptCert(metadata) + if err = generateLetsEncryptCert(metadata); err != nil { + return "", "", err + } } panic("Not implemented") } -// (Re-)Generate a Lets Encrypt certificate using the provided details in the config -// Automatically stores the newly generated certificate in the db, encrypting beforehand -func generateLetsEncryptCert(metadata *models.ServerMetadata) (*tls.Config, error) { +// (Re-)Generate a Lets Encrypt certificate using the provided details in the config. +// Automatically stores the newly generated certificate in the db, encrypting beforehand. +// Also updates the metadata with the new data. +// Does not write the cert to a temporary file. +func generateLetsEncryptCert(metadata *models.ServerMetadata) error { + encKey := []byte(config.GlobalConfig.Storage.EncryptionKey) // Follow https://go-acme.github.io/lego/usage/library/index.html var userPrivateKey *ecdsa.PrivateKey var err error if len(metadata.LEUserPrivKey) == 0 { userPrivateKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { - return nil, err + return err } marshalled, err := x509.MarshalECPrivateKey(userPrivateKey) if err != nil { - return nil, err + return err } encryptedKey, err := Encrypt( - []byte(config.GlobalConfig.Storage.EncryptionKey), + encKey, marshalled, ) if err != nil { - return nil, err + return err } metadata.LEUserPrivKey = encryptedKey } else { - decryptedKey, err := Decrypt([]byte(config.GlobalConfig.Storage.EncryptionKey), metadata.LEUserPrivKey) + decryptedKey, err := Decrypt(encKey, metadata.LEUserPrivKey) if err != nil { - return nil, err + return err } userPrivateKey, err = x509.ParseECPrivateKey(decryptedKey) if err != nil { - return nil, err + return err } } - user := leUser{Email: *config.GlobalConfig.SSL.AdminMail} + user := leUser{Email: *config.GlobalConfig.SSL.AdminMail, Key: userPrivateKey} leConfig := lego.NewConfig(&user) leConfig.CADirURL = strings.Replace( config.GlobalConfig.General.GetFullPublicUrl(), @@ -107,22 +146,22 @@ func generateLetsEncryptCert(metadata *models.ServerMetadata) (*tls.Config, erro leClient, err := lego.NewClient(leConfig) if err != nil { - return nil, err + return err } // TODO: Figure out these servers err = leClient.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "5002")) if err != nil { - return nil, err + return err } err = leClient.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("", "5001")) if err != nil { - return nil, err + return err } reg, err := leClient.Registration.Register( registration.RegisterOptions{TermsOfServiceAgreed: true}, ) if err != nil { - return nil, err + return err } user.Registration = reg req := certificate.ObtainRequest{ @@ -131,9 +170,35 @@ func generateLetsEncryptCert(metadata *models.ServerMetadata) (*tls.Config, erro } certificates, err := leClient.Certificate.Obtain(req) if err != nil { - return nil, err + return err } - _ = certificates - // TODO: Do something with certificates here - panic("Not implemented") + metadata.LELastUpdate = sql.NullTime{Valid: true, Time: time.Now()} + metadata.LEDomain = certificates.Domain + metadata.LECertStableUrl = certificates.CertStableURL + metadata.LECertUrl = certificates.CertURL + metadata.LECSR, err = Encrypt(encKey, certificates.CSR) + if err != nil { + return err + } + metadata.LECertificate, err = Encrypt(encKey, certificates.Certificate) + if err != nil { + return err + } + metadata.LECertificate, err = Encrypt(encKey, certificates.Certificate) + if err != nil { + return err + } + metadata.LEPrivateKey, err = Encrypt(encKey, certificates.PrivateKey) + if err != nil { + return err + } + metadata.LEIssuerCertificate, err = Encrypt(encKey, certificates.IssuerCertificate) + if err != nil { + return err + } + err = dbgen.ServerMetadata.Save(metadata) + if err != nil { + return err + } + return nil } diff --git a/config/config.go b/config/config.go index 9f4c950..83399bd 100644 --- a/config/config.go +++ b/config/config.go @@ -14,8 +14,10 @@ type ConfigSSL struct { HandleSSL bool `toml:"handle_ssl"` // Whether Linstrom should handle SSL encryption itself in case of http1/2 // If Linstrom is to handle SSL, whether it should use LetsEncrypt for certificates UseLetsEncrypt *bool `toml:"use_lets_encrypt"` - // Path to the certificate if Linstrom is to handle SSL while not using LetsEncrypt + // Path to the certificate file (.crt) if Linstrom is to handle SSL while not using LetsEncrypt CertificateFile *string `toml:"certificate_file"` + // Path to the certificate key file (.key) to handle SSL while not using LetsEncrypt + CertKeyFile *string `toml:"cert_key_file"` // Mail adress to use in case of using LetsEncrypt AdminMail *string `toml:"admin_mail"` } @@ -251,6 +253,14 @@ func (gc *ConfigGeneral) GetFullPublicUrl() string { return str } +func (gc *ConfigGeneral) GetFinalPublicPort() int { + if gc.PublicPort != nil { + return *gc.PublicPort + } else { + return gc.PrivatePort + } +} + func (sc *ConfigStorage) BuildPostgresDSN() string { dsn := fmt.Sprintf( "host=%s user=%s password=%s dbname=%s port=%d", diff --git a/go.mod b/go.mod index 86adc0e..9de8c8c 100644 --- a/go.mod +++ b/go.mod @@ -33,11 +33,13 @@ require ( github.com/fxamacker/cbor/v2 v2.8.0 // indirect github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/go-sql-driver/mysql v1.9.2 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-webauthn/x v0.1.20 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-tpm v0.9.3 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect @@ -52,8 +54,12 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/miekg/dns v1.1.64 // indirect + github.com/onsi/ginkgo/v2 v2.9.5 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.52.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/x448/float16 v0.8.4 // indirect + go.uber.org/mock v0.5.0 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/net v0.39.0 // indirect golang.org/x/sync v0.13.0 // indirect diff --git a/go.sum b/go.sum index 1b08a74..4cedc56 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,12 @@ github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8 github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= @@ -27,6 +31,8 @@ github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JS github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-webauthn/webauthn v0.12.3 h1:hHQl1xkUuabUU9uS+ISNCMLs9z50p9mDUZI/FmkayNE= github.com/go-webauthn/webauthn v0.12.3/go.mod h1:4JRe8Z3W7HIw8NGEWn2fnUwecoDzkkeach/NnvhkqGY= github.com/go-webauthn/x v0.1.20 h1:brEBDqfiPtNNCdS/peu8gARtq8fIPsHz0VzpPjGvgiw= @@ -44,8 +50,11 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc= github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -88,12 +97,18 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/nrednav/cuid2 v1.0.1 h1:aYLDCmGxEij7xCdiV6GVSPSlqFOS6sqHKKvBeKjddVY= github.com/nrednav/cuid2 v1.0.1/go.mod h1:nH9lUYqbtoVsnpy20etw5q1guTjE99Xy4EpmnK5nKm0= +github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA= +github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= @@ -114,6 +129,8 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY github.com/yaronf/httpsign v0.3.2 h1:rBYYx9eHm60noI4oC24JAD2tJuM8AUDh9ErJO/FfzBs= github.com/yaronf/httpsign v0.3.2/go.mod h1:cYB/6toJrJnf4JTLVoo6IHzFH9/Zu1dcKmah4xfX2WA= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= @@ -134,6 +151,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/web/public/middleware/httpUpgrade.go b/web/public/middleware/httpUpgrade.go new file mode 100644 index 0000000..3876548 --- /dev/null +++ b/web/public/middleware/httpUpgrade.go @@ -0,0 +1,40 @@ +package webmiddleware + +import ( + "fmt" + "net/http" + + "git.mstar.dev/mstar/linstrom/config" +) + +func AddUpgradeHeader(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.ProtoMajor { + case 1: + // Always offer upgrade to http2 + headerText := fmt.Sprintf( + "h2=\":%d\"; ma=3600", + config.GlobalConfig.General.GetFinalPublicPort(), + ) + // Offer upgrade to http3 if enabled + if config.GlobalConfig.Experimental.Http3Support && config.GlobalConfig.SSL.HandleSSL { + headerText = fmt.Sprintf( + "h3=\":%d\"; ma=3600, %s", + config.GlobalConfig.General.GetFinalPublicPort(), + headerText, + ) + } + w.Header().Add("Alt-Svc", headerText) + case 2: + // Offer upgrade to http3 if enabled + if config.GlobalConfig.Experimental.Http3Support && config.GlobalConfig.SSL.HandleSSL { + headerText := fmt.Sprintf( + "h3=\":%d\"; ma=3600", + config.GlobalConfig.General.GetFinalPublicPort(), + ) + w.Header().Add("Alt-Svc", headerText) + } + } + h.ServeHTTP(w, r) + }) +} diff --git a/web/public/server.go b/web/public/server.go index dd5a782..5520212 100644 --- a/web/public/server.go +++ b/web/public/server.go @@ -32,13 +32,18 @@ import ( "net/http" webutils "git.mstar.dev/mstar/goutils/http" + "github.com/quic-go/quic-go/http3" + "github.com/rs/zerolog/log" + "git.mstar.dev/mstar/linstrom/auth-new" + "git.mstar.dev/mstar/linstrom/config" "git.mstar.dev/mstar/linstrom/web/public/api" webmiddleware "git.mstar.dev/mstar/linstrom/web/public/middleware" ) type Server struct { - server *http.Server + server *http.Server + quicServer *http3.Server } func New(addr string, duckImg *string, duckFs fs.FS) *Server { @@ -51,28 +56,92 @@ func New(addr string, duckImg *string, duckFs fs.FS) *Server { handler.HandleFunc("GET /errors/{name}", errorTypeHandler) handler.HandleFunc("GET /default-image", buildServeDefaultImage(duckImg, duckFs)) handler.HandleFunc("GET /default-image.webp", buildServeDefaultImage(duckImg, duckFs)) - server := http.Server{ - Handler: webutils.ChainMiddlewares( - handler, - webutils.BuildLoggingMiddleware( - true, - []string{"/assets"}, - map[string]string{"server": "public"}, - ), - webmiddleware.AppendFullPathMiddleware, - webmiddleware.TraceRequestInfoMiddleware, + rootHandler := webutils.ChainMiddlewares( + handler, + webutils.BuildLoggingMiddleware( + true, + []string{"/assets"}, + map[string]string{"server": "public"}, ), - Addr: addr, + webmiddleware.AppendFullPathMiddleware, + webmiddleware.TraceRequestInfoMiddleware, + webmiddleware.AddUpgradeHeader, + ) + server := http.Server{ + Handler: rootHandler, + Addr: addr, } - return &Server{&server} + quicServer := http3.Server{ + Handler: rootHandler, + Addr: addr, + } + if config.GlobalConfig.General.PublicPort != nil { + quicServer.Port = *config.GlobalConfig.General.PublicPort + } + return &Server{server: &server, quicServer: &quicServer} } func (s *Server) Start() error { - return s.server.ListenAndServe() + if config.GlobalConfig.SSL.HandleSSL { + certFile, keyFile, err := auth.TlsFromConfig() + if err != nil { + return err + } + if config.GlobalConfig.Experimental.Http3Support { + errChan := make(chan error, 2) + go func() { + errChan <- s.server.ListenAndServeTLS(certFile, keyFile) + }() + go func() { + errChan <- s.quicServer.ListenAndServeTLS(certFile, keyFile) + }() + + err := <-errChan + if err != nil && err != http.ErrServerClosed { + return err + } + // Always gets two values back on the channel since both servers are running + err = <-errChan + if err != nil && err != http.ErrServerClosed { + return err + } + return nil + } else { + return s.server.ListenAndServeTLS(certFile, keyFile) + } + } else { + // But refuse http3 support here, since it doesn't work without tls + return s.server.ListenAndServe() + } } func (s *Server) Stop() error { - return s.server.Shutdown(context.Background()) + if config.GlobalConfig.SSL.HandleSSL && config.GlobalConfig.Experimental.Http3Support { + errChan := make(chan error, 2) + go func() { + log.Debug().Msg("Stopping tcp server") + errChan <- s.server.Shutdown(context.Background()) + log.Debug().Msg("Tcp server stopped") + }() + go func() { + log.Debug().Msg("Stopping udp server") + errChan <- s.quicServer.Shutdown(context.Background()) + log.Debug().Msg("Udp server stopped") + }() + + err := <-errChan + if err != nil && err != http.ErrServerClosed { + return err + } + // Always gets two values back on the channel since both servers are running + err = <-errChan + if err != nil && err != http.ErrServerClosed { + return err + } + return nil + } else { + return s.server.Shutdown(context.Background()) + } } func buildServeDefaultImage(