Completely refactor certificates and implement renewal & cleanup

This commit is contained in:
Moritz Marquardt 2021-11-20 15:30:58 +01:00
commit 2aaac2c52b
Signed by: momar
GPG key ID: D5788327BEE388B6

View file

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width">
<title>%status</title>
<link rel="stylesheet" href="https://design.codeberg-test.org/design-kit/codeberg.css" />
<link rel="stylesheet" href="https://design.codeberg.org/design-kit/codeberg.css" />
<link href="https://fonts.codeberg.org/dist/inter/Inter%20Web/inter.css" rel="stylesheet" />
<link href="https://fonts.codeberg.org/dist/fontawesome5/css/all.min.css" rel="stylesheet" />
@ -29,7 +29,7 @@
Sorry, this page doesn't exist or is inaccessible for other reasons (%status)
</h5>
<small class="text-muted">
<img src="https://design.codeberg-test.org/logo-kit/icon.svg" class="align-top">
<img src="https://design.codeberg.org/logo-kit/icon.svg" class="align-top">
Static pages made easy - <a href="https://codeberg.page">Codeberg Pages</a>
</small>
</body>

View file

@ -1,11 +1,13 @@
## Environment
- `HOST` & `PORT` (default: `[::]` & `443`): listen address.
- `PAGES_DOMAIN` (default: `codeberg.page`): main domain for pages.
- `RAW_DOMAIN` (default: `raw.codeberg.org`): domain for raw resources.
- `GITEA_ROOT` (default: `https://codeberg.org`): root of the upstream Gitea instance.
- `REDIRECT_BROKEN_DNS` (default: "https://docs.codeberg.org/pages/custom-domains/"): info page for setting up DNS, shown for invalid DNS setups.
- `REDIRECT_BROKEN_DNS` (default: https://docs.codeberg.org/pages/custom-domains/): info page for setting up DNS, shown for invalid DNS setups.
- `REDIRECT_RAW_INFO` (default: https://docs.codeberg.org/pages/raw-content/): info page for raw resources, shown if no resource is provided.
- `ACME_API` (default: https://acme-v02.api.letsencrypt.org/directory): Set this to "https://acme-staging-v02.api.letsencrypt.org/directory" to use the staging API of Let's Encrypt instead.
- `ACME_API` (default: https://acme.zerossl.com/v2/DV90): set this to https://acme.mock.director to use invalid certificates without any verification (great for debugging). ZeroSSL is used as it doesn't have rate limits and doesn't clash with the official Codeberg certificates (which are using Let's Encrypt).
- `ACME_EMAIL` (default: `noreply@example.email`): Set this to "true" to accept the Terms of Service of your ACME provider.
- `ACME_ACCEPT_TERMS` (default: use self-signed certificate): Set this to "true" to accept the Terms of Service of your ACME provider.
- `DNS_PROVIDER` (default: use self-signed certificate): Code of the ACME DNS provider for the main domain wildcard.
See https://go-acme.github.io/lego/dns/ for available values & additional environment variables.

File diff suppressed because it is too large Load diff

View file

@ -68,10 +68,16 @@ var CanonicalDomainCacheTimeout = 15*time.Minute
var canonicalDomainCache = mcache.New()
// checkCanonicalDomain returns the canonical domain specified in the repo (using the file `.canonical-domain`).
func checkCanonicalDomain(targetOwner, targetRepo, targetBranch string) (canonicalDomain string) {
// Check if the canonical domain matches
func checkCanonicalDomain(targetOwner, targetRepo, targetBranch, actualDomain string) (canonicalDomain string, valid bool) {
domains := []string{}
if cachedValue, ok := canonicalDomainCache.Get(targetOwner + "/" + targetRepo + "/" + targetBranch); ok {
canonicalDomain = cachedValue.(string)
domains = cachedValue.([]string)
for _, domain := range domains {
if domain == actualDomain {
valid = true
break
}
}
} else {
req := fasthttp.AcquireRequest()
req.SetRequestURI(string(GiteaRoot) + "/api/v1/repos/" + targetOwner + "/" + targetRepo + "/raw/" + targetBranch + "/.domains")
@ -79,18 +85,28 @@ func checkCanonicalDomain(targetOwner, targetRepo, targetBranch string) (canonic
err := upstreamClient.Do(req, res)
if err == nil && res.StatusCode() == fasthttp.StatusOK {
canonicalDomain = strings.TrimSpace(string(res.Body()))
if strings.Contains(canonicalDomain, "/") {
canonicalDomain = ""
for _, domain := range strings.Split(string(res.Body()), "\n") {
domain = strings.ToLower(domain)
domain = strings.TrimSpace(domain)
domain = strings.TrimPrefix(domain, "http://")
domain = strings.TrimPrefix(domain, "https://")
if len(domain) > 0 && !strings.HasPrefix(domain, "#") && !strings.ContainsAny(domain, "\t /") && strings.ContainsRune(domain, '.') {
domains = append(domains, domain)
}
if domain == actualDomain {
valid = true
}
}
}
if canonicalDomain == "" {
canonicalDomain = targetOwner + string(MainDomainSuffix)
if targetRepo != "" && targetRepo != "pages" {
canonicalDomain += "/" + targetRepo
}
domains = append(domains, targetOwner + string(MainDomainSuffix))
if domains[len(domains) - 1] == actualDomain {
valid = true
}
_ = canonicalDomainCache.Set(targetOwner + "/" + targetRepo + "/" + targetBranch, canonicalDomain, CanonicalDomainCacheTimeout)
if targetRepo != "" && targetRepo != "pages" {
domains[len(domains) - 1] += "/" + targetRepo
}
_ = canonicalDomainCache.Set(targetOwner + "/" + targetRepo + "/" + targetBranch, domains, CanonicalDomainCacheTimeout)
}
canonicalDomain = domains[0]
return
}

View file

@ -28,8 +28,10 @@ func handler(ctx *fasthttp.RequestCtx) {
// Enable caching, but require revalidation to reduce confusion
ctx.Response.Header.Set("Cache-Control", "must-revalidate")
trimmedHost := TrimHostPort(ctx.Request.Host())
// Add HSTS for RawDomain and MainDomainSuffix
if hsts := GetHSTSHeader(ctx.Host()); hsts != "" {
if hsts := GetHSTSHeader(trimmedHost); hsts != "" {
ctx.Response.Header.Set("Strict-Transport-Security", hsts)
}
@ -52,7 +54,7 @@ func handler(ctx *fasthttp.RequestCtx) {
if ctx.IsOptions() {
allowCors := false
for _, allowedCorsDomain := range AllowedCorsDomains {
if bytes.Equal(ctx.Request.Host(), allowedCorsDomain) {
if bytes.Equal(trimmedHost, allowedCorsDomain) {
allowCors = true
break
}
@ -109,8 +111,8 @@ func handler(ctx *fasthttp.RequestCtx) {
// tryUpstream forwards the target request to the Gitea API, and shows an error page on failure.
var tryUpstream = func() {
// check if a canonical domain exists on a request on MainDomain
if bytes.HasSuffix(ctx.Request.Host(), MainDomainSuffix) {
canonicalDomain := checkCanonicalDomain(targetOwner, targetRepo, targetBranch)
if bytes.HasSuffix(trimmedHost, MainDomainSuffix) {
canonicalDomain, _ := checkCanonicalDomain(targetOwner, targetRepo, targetBranch, "")
if !strings.HasSuffix(strings.SplitN(canonicalDomain, "/", 2)[0], string(MainDomainSuffix)) {
canonicalPath := string(ctx.RequestURI())
if targetRepo != "pages" {
@ -129,7 +131,7 @@ func handler(ctx *fasthttp.RequestCtx) {
s.Step("preparations")
if RawDomain != nil && bytes.Equal(ctx.Request.Host(), RawDomain) {
if RawDomain != nil && bytes.Equal(trimmedHost, RawDomain) {
// Serve raw content from RawDomain
s.Debug("raw domain")
@ -169,12 +171,12 @@ func handler(ctx *fasthttp.RequestCtx) {
return
}
} else if bytes.HasSuffix(ctx.Request.Host(), MainDomainSuffix) {
} else if bytes.HasSuffix(trimmedHost, MainDomainSuffix) {
// Serve pages from subdomains of MainDomainSuffix
s.Debug("main domain suffix")
pathElements := strings.Split(string(bytes.Trim(ctx.Request.URI().Path(), "/")), "/")
targetOwner = string(bytes.TrimSuffix(ctx.Request.Host(), MainDomainSuffix))
targetOwner = string(bytes.TrimSuffix(trimmedHost, MainDomainSuffix))
targetRepo = pathElements[0]
targetPath = strings.Trim(strings.Join(pathElements[1:], "/"), "/")
@ -235,8 +237,10 @@ func handler(ctx *fasthttp.RequestCtx) {
returnErrorPage(ctx, fasthttp.StatusFailedDependency)
return
} else {
trimmedHostStr := string(trimmedHost)
// Serve pages from external domains
targetOwner, targetRepo, targetBranch = getTargetFromDNS(string(ctx.Request.Host()))
targetOwner, targetRepo, targetBranch = getTargetFromDNS(trimmedHostStr)
if targetOwner == "" {
ctx.Redirect(BrokenDNSPage, fasthttp.StatusTemporaryRedirect)
return
@ -253,8 +257,11 @@ func handler(ctx *fasthttp.RequestCtx) {
// Try to use the given repo on the given branch or the default branch
s.Step("custom domain preparations, now trying with details from DNS")
if tryBranch(targetRepo, targetBranch, pathElements, canonicalLink) {
canonicalDomain := checkCanonicalDomain(targetOwner, targetRepo, targetBranch)
if canonicalDomain != string(ctx.Request.Host()) {
canonicalDomain, valid := checkCanonicalDomain(targetOwner, targetRepo, targetBranch, trimmedHostStr)
if !valid {
returnErrorPage(ctx, fasthttp.StatusMisdirectedRequest)
return
} else if canonicalDomain != trimmedHostStr {
// only redirect if the target is also a codeberg page!
targetOwner, _, _ = getTargetFromDNS(strings.SplitN(canonicalDomain, "/", 2)[0])
if targetOwner != "" {
@ -282,6 +289,9 @@ func returnErrorPage(ctx *fasthttp.RequestCtx, code int) {
ctx.Response.SetStatusCode(code)
ctx.Response.Header.SetContentType("text/html; charset=utf-8")
message := fasthttp.StatusMessage(code)
if code == fasthttp.StatusMisdirectedRequest {
message += " - domain not specified in <code>.domains</code> file"
}
if code == fasthttp.StatusFailedDependency {
message += " - owner, repo or branch doesn't exist"
}

21
helpers.go Normal file
View file

@ -0,0 +1,21 @@
package main
import "bytes"
// 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 ""
}
}
func TrimHostPort(host []byte) []byte {
i := bytes.IndexByte(host, ':')
if i >= 0 {
return host[:i]
}
return host
}

View file

@ -94,7 +94,6 @@ func main() {
Concurrency: 1024 * 32, // TODO: adjust bottlenecks for best performance with Gitea!
MaxConnsPerIP: 100,
}
//fasthttp2.ConfigureServerAndConfig(server, tlsConfig)
// Setup listener and TLS
listener, err := net.Listen("tcp", address)