diff --git a/certificates.go b/certificates.go index 0c4ffdb..8f73210 100644 --- a/certificates.go +++ b/certificates.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "crypto/rand" "crypto/rsa" "crypto/tls" @@ -12,38 +13,85 @@ import ( "time" ) -var fallbackCertKey, _ = rsa.GenerateKey(rand.Reader, 1024) -var fallbackCertSpecification = &x509.Certificate{ - Subject: pkix.Name{ - CommonName: strings.TrimPrefix(string(MainDomainSuffix), "."), - }, - SerialNumber: big.NewInt(0), - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(100, 0, 0), -} -var fallbackCertBytes, _ = x509.CreateCertificate( - rand.Reader, - fallbackCertSpecification, - fallbackCertSpecification, - fallbackCertKey.Public(), - fallbackCertKey, -) -var fallbackCert, _ = tls.X509KeyPair(pem.EncodeToMemory(&pem.Block{ - Bytes: fallbackCertBytes, - Type: "CERTIFICATE", -}), pem.EncodeToMemory(&pem.Block{ - Bytes: x509.MarshalPKCS1PrivateKey(fallbackCertKey), - Type: "RSA PRIVATE KEY", -})) - // tlsConfig contains the configuration for generating, serving and cleaning up Let's Encrypt certificates. var tlsConfig = &tls.Config{ GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { // TODO: check DNS name & get certificate from Let's Encrypt - return &fallbackCert, nil + return FallbackCertificate(), nil }, PreferServerCipherSuites: true, - // TODO: optimize cipher suites, minimum TLS version, etc. + + // generated 2021-07-13, Mozilla Guideline v5.6, Go 1.14.4, intermediate configuration + // https://ssl-config.mozilla.org/#server=go&version=1.14.4&config=intermediate&guideline=5.6 + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + }, } -// TODO: HSTS header with includeSubdomains & preload for MainDomainSuffix and RawDomain +// GetHSTSHeader returns a HSTS header with includeSubdomains & preload for MainDomainSuffix and RawDomain, or an empty +// string for custom domains. +func GetHSTSHeader(host []byte) string { + if bytes.HasSuffix(host, MainDomainSuffix) || bytes.Equal(host, RawDomain) { + return "max-age=63072000; includeSubdomains; preload" + } else { + return "" + } +} + +var fallbackCertificate *tls.Certificate +// FallbackCertificate generates a new self-signed TLS certificate on demand. +func FallbackCertificate() *tls.Certificate { + if fallbackCertificate != nil { + return fallbackCertificate + } + + fallbackSerial, err := rand.Int(rand.Reader, (&big.Int{}).Lsh(big.NewInt(1), 159)) + if err != nil { + panic(err) + } + + fallbackCertKey, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + panic(err) + } + + fallbackCertSpecification := &x509.Certificate{ + Subject: pkix.Name{ + CommonName: strings.TrimPrefix(string(MainDomainSuffix), "."), + }, + SerialNumber: fallbackSerial, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(100, 0, 0), + } + + fallbackCertBytes, err := x509.CreateCertificate( + rand.Reader, + fallbackCertSpecification, + fallbackCertSpecification, + fallbackCertKey.Public(), + fallbackCertKey, + ) + if err != nil { + panic(err) + } + + fallbackCert, err := tls.X509KeyPair(pem.EncodeToMemory(&pem.Block{ + Bytes: fallbackCertBytes, + Type: "CERTIFICATE", + }), pem.EncodeToMemory(&pem.Block{ + Bytes: x509.MarshalPKCS1PrivateKey(fallbackCertKey), + Type: "RSA PRIVATE KEY", + })) + if err != nil { + panic(err) + } + + fallbackCertificate = &fallbackCert + return fallbackCertificate +} diff --git a/handler.go b/handler.go index 30ef031..909725f 100644 --- a/handler.go +++ b/handler.go @@ -28,6 +28,11 @@ func handler(ctx *fasthttp.RequestCtx) { // Enable caching, but require revalidation to reduce confusion ctx.Response.Header.Set("Cache-Control", "must-revalidate") + // Add HSTS for RawDomain and MainDomainSuffix + if hsts := GetHSTSHeader(ctx.Host()); hsts != "" { + ctx.Response.Header.Set("Strict-Transport-Security", hsts) + } + // Block all methods not required for static pages if !ctx.IsGet() && !ctx.IsHead() && !ctx.IsOptions() { ctx.Response.Header.Set("Allow", "GET, HEAD, OPTIONS") @@ -275,7 +280,11 @@ func handler(ctx *fasthttp.RequestCtx) { func returnErrorPage(ctx *fasthttp.RequestCtx, code int) { ctx.Response.SetStatusCode(code) ctx.Response.Header.SetContentType("text/html; charset=utf-8") - ctx.Response.SetBody(bytes.ReplaceAll(NotFoundPage, []byte("%status"), []byte(strconv.Itoa(code)+" "+fasthttp.StatusMessage(code)))) + message := fasthttp.StatusMessage(code) + if code == fasthttp.StatusFailedDependency { + message += " - owner, repo or branch doesn't exist" + } + ctx.Response.SetBody(bytes.ReplaceAll(NotFoundPage, []byte("%status"), []byte(strconv.Itoa(code)+" "+message))) } // BranchExistanceCacheTimeout specifies the timeout for the default branch cache. It can be quite long. diff --git a/main.go b/main.go index 23ab970..cc0b43f 100644 --- a/main.go +++ b/main.go @@ -79,7 +79,7 @@ func main() { GiteaRoot = bytes.TrimSuffix(GiteaRoot, []byte{'/'}) // Use HOST and PORT environment variables to determine listening address - address := fmt.Sprintf("%s:%s", envOr("HOST", "[::]"), envOr("PORT", "80")) + address := fmt.Sprintf("%s:%s", envOr("HOST", "[::]"), envOr("PORT", "443")) fmt.Printf("Listening on https://%s\n", address) // Enable compression by wrapping the handler() method with the compression function provided by FastHTTP