From f2f943c0d800c513025a2fecfb1a48729052fdc1 Mon Sep 17 00:00:00 2001
From: Gusted <gusted@noreply.codeberg.org>
Date: Tue, 15 Nov 2022 16:15:11 +0100
Subject: [PATCH 1/8] Remove unnecessary conversion (#139)

- Remove unnecessary type conversion.
- Enforce via CI

Co-authored-by: Gusted <williamzijl7@hotmail.com>
Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/139
Reviewed-by: 6543 <6543@obermui.de>
Co-authored-by: Gusted <gusted@noreply.codeberg.org>
Co-committed-by: Gusted <gusted@noreply.codeberg.org>
---
 .golangci.yml                           | 20 ++++++++++
 cmd/main.go                             |  2 +-
 integration/get_test.go                 |  2 +-
 integration/main_test.go                |  2 +-
 server/certificates/certificates.go     | 50 +++++++++++++------------
 server/database/setup.go                |  2 +-
 server/gitea/cache.go                   |  5 +--
 server/handler/handler.go               |  2 +-
 server/handler/handler_custom_domain.go |  2 +-
 server/handler/handler_sub_domain.go    |  2 +-
 server/handler/try.go                   |  8 ++--
 server/setup.go                         |  4 +-
 server/upstream/helper.go               |  2 +-
 server/upstream/upstream.go             |  4 +-
 14 files changed, 64 insertions(+), 43 deletions(-)
 create mode 100644 .golangci.yml

diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..6d9b95a
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,20 @@
+linters-settings:
+  gocritic:
+    enabled-tags:
+      - diagnostic
+      - experimental
+      - opinionated
+      - performance
+      - style
+    disabled-checks:
+      - importShadow
+      - ifElseChain
+      - hugeParam
+
+linters:
+  enable:
+    - unconvert
+    - gocritic
+
+run:
+  timeout: 5m
diff --git a/cmd/main.go b/cmd/main.go
index 6ad1aa8..b72013a 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -65,7 +65,7 @@ func Serve(ctx *cli.Context) error {
 	}
 
 	allowedCorsDomains := AllowedCorsDomains
-	if len(rawDomain) != 0 {
+	if rawDomain != "" {
 		allowedCorsDomains = append(allowedCorsDomains, rawDomain)
 	}
 
diff --git a/integration/get_test.go b/integration/get_test.go
index 8794651..81d8488 100644
--- a/integration/get_test.go
+++ b/integration/get_test.go
@@ -124,7 +124,7 @@ func TestLFSSupport(t *testing.T) {
 
 func TestGetOptions(t *testing.T) {
 	log.Println("=== TestGetOptions ===")
-	req, _ := http.NewRequest(http.MethodOptions, "https://mock-pages.codeberg-test.org:4430/README.md", nil)
+	req, _ := http.NewRequest(http.MethodOptions, "https://mock-pages.codeberg-test.org:4430/README.md", http.NoBody)
 	resp, err := getTestHTTPSClient().Do(req)
 	assert.NoError(t, err)
 	if !assert.NotNil(t, resp) {
diff --git a/integration/main_test.go b/integration/main_test.go
index 06d553f..406b33a 100644
--- a/integration/main_test.go
+++ b/integration/main_test.go
@@ -28,7 +28,7 @@ func TestMain(m *testing.M) {
 
 	time.Sleep(10 * time.Second)
 
-	os.Exit(m.Run())
+	m.Run()
 }
 
 func startServer(ctx context.Context) error {
diff --git a/server/certificates/certificates.go b/server/certificates/certificates.go
index 11cf0df..8af4be5 100644
--- a/server/certificates/certificates.go
+++ b/server/certificates/certificates.go
@@ -53,17 +53,19 @@ func TLSConfig(mainDomainSuffix string,
 
 			if info.SupportedProtos != nil {
 				for _, proto := range info.SupportedProtos {
-					if proto == tlsalpn01.ACMETLS1Protocol {
-						challenge, ok := challengeCache.Get(sni)
-						if !ok {
-							return nil, errors.New("no challenge for this domain")
-						}
-						cert, err := tlsalpn01.ChallengeCert(sni, challenge.(string))
-						if err != nil {
-							return nil, err
-						}
-						return cert, nil
+					if proto != tlsalpn01.ACMETLS1Protocol {
+						continue
 					}
+
+					challenge, ok := challengeCache.Get(sni)
+					if !ok {
+						return nil, errors.New("no challenge for this domain")
+					}
+					cert, err := tlsalpn01.ChallengeCert(sni, challenge.(string))
+					if err != nil {
+						return nil, err
+					}
+					return cert, nil
 				}
 			}
 
@@ -195,7 +197,7 @@ func (a AcmeHTTPChallengeProvider) CleanUp(domain, token, _ string) error {
 
 func retrieveCertFromDB(sni, mainDomainSuffix, dnsProvider string, acmeUseRateLimits bool, certDB database.CertDB) (tls.Certificate, bool) {
 	// parse certificate from database
-	res, err := certDB.Get(string(sni))
+	res, err := certDB.Get(sni)
 	if err != nil {
 		panic(err) // TODO: no panic
 	}
@@ -216,7 +218,7 @@ func retrieveCertFromDB(sni, mainDomainSuffix, dnsProvider string, acmeUseRateLi
 		}
 
 		// renew certificates 7 days before they expire
-		if !tlsCertificate.Leaf.NotAfter.After(time.Now().Add(7 * 24 * time.Hour)) {
+		if tlsCertificate.Leaf.NotAfter.Before(time.Now().Add(7 * 24 * time.Hour)) {
 			// TODO: add ValidUntil to custom res struct
 			if res.CSR != nil && len(res.CSR) > 0 {
 				// CSR stores the time when the renewal shall be tried again
@@ -227,9 +229,9 @@ func retrieveCertFromDB(sni, mainDomainSuffix, dnsProvider string, acmeUseRateLi
 			}
 			go (func() {
 				res.CSR = nil // acme client doesn't like CSR to be set
-				tlsCertificate, err = obtainCert(acmeClient, []string{string(sni)}, res, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB)
+				tlsCertificate, err = obtainCert(acmeClient, []string{sni}, res, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB)
 				if err != nil {
-					log.Error().Msgf("Couldn't renew certificate for %s: %v", string(sni), err)
+					log.Error().Msgf("Couldn't renew certificate for %s: %v", sni, err)
 				}
 			})()
 		}
@@ -262,7 +264,7 @@ func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Re
 	defer obtainLocks.Delete(name)
 
 	if acmeClient == nil {
-		return mockCert(domains[0], "ACME client uninitialized. This is a server error, please report!", string(mainDomainSuffix), keyDatabase), nil
+		return mockCert(domains[0], "ACME client uninitialized. This is a server error, please report!", mainDomainSuffix, keyDatabase), nil
 	}
 
 	// request actual cert
@@ -305,12 +307,12 @@ func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Re
 				// avoid sending a mock cert instead of a still valid cert, instead abuse CSR field to store time to try again at
 				renew.CSR = []byte(strconv.FormatInt(time.Now().Add(6*time.Hour).Unix(), 10))
 				if err := keyDatabase.Put(name, renew); err != nil {
-					return mockCert(domains[0], err.Error(), string(mainDomainSuffix), keyDatabase), err
+					return mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase), err
 				}
 				return tlsCertificate, nil
 			}
 		}
-		return mockCert(domains[0], err.Error(), string(mainDomainSuffix), keyDatabase), err
+		return mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase), err
 	}
 	log.Debug().Msgf("Obtained certificate for %v", domains)
 
@@ -408,7 +410,7 @@ func SetupAcmeConfig(acmeAPI, acmeMail, acmeEabHmac, acmeEabKID string, acmeAcce
 
 func SetupCertificates(mainDomainSuffix, dnsProvider string, acmeConfig *lego.Config, acmeUseRateLimits, enableHTTPServer bool, challengeCache cache.SetGetKey, certDB database.CertDB) error {
 	// getting main cert before ACME account so that we can fail here without hitting rate limits
-	mainCertBytes, err := certDB.Get(string(mainDomainSuffix))
+	mainCertBytes, err := certDB.Get(mainDomainSuffix)
 	if err != nil {
 		return fmt.Errorf("cert database is not working")
 	}
@@ -452,7 +454,7 @@ func SetupCertificates(mainDomainSuffix, dnsProvider string, acmeConfig *lego.Co
 	}
 
 	if mainCertBytes == nil {
-		_, err = obtainCert(mainDomainAcmeClient, []string{"*" + string(mainDomainSuffix), string(mainDomainSuffix[1:])}, nil, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB)
+		_, err = obtainCert(mainDomainAcmeClient, []string{"*" + mainDomainSuffix, mainDomainSuffix[1:]}, nil, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB)
 		if err != nil {
 			log.Error().Err(err).Msg("Couldn't renew main domain certificate, continuing with mock certs only")
 		}
@@ -479,7 +481,7 @@ func MaintainCertDB(ctx context.Context, interval time.Duration, mainDomainSuffi
 				}
 
 				tlsCertificates, err := certcrypto.ParsePEMBundle(res.Certificate)
-				if err != nil || !tlsCertificates[0].NotAfter.After(now) {
+				if err != nil || tlsCertificates[0].NotAfter.Before(now) {
 					err := certDB.Delete(string(key))
 					if err != nil {
 						log.Error().Err(err).Msgf("Deleting expired certificate for %q failed", string(key))
@@ -501,18 +503,18 @@ func MaintainCertDB(ctx context.Context, interval time.Duration, mainDomainSuffi
 		}
 
 		// update main cert
-		res, err := certDB.Get(string(mainDomainSuffix))
+		res, err := certDB.Get(mainDomainSuffix)
 		if err != nil {
 			log.Error().Msgf("Couldn't get cert for domain %q", mainDomainSuffix)
 		} else if res == nil {
-			log.Error().Msgf("Couldn't renew certificate for main domain %q expected main domain cert to exist, but it's missing - seems like the database is corrupted", string(mainDomainSuffix))
+			log.Error().Msgf("Couldn't renew certificate for main domain %q expected main domain cert to exist, but it's missing - seems like the database is corrupted", mainDomainSuffix)
 		} else {
 			tlsCertificates, err := certcrypto.ParsePEMBundle(res.Certificate)
 
 			// renew main certificate 30 days before it expires
-			if !tlsCertificates[0].NotAfter.After(time.Now().Add(30 * 24 * time.Hour)) {
+			if tlsCertificates[0].NotAfter.Before(time.Now().Add(30 * 24 * time.Hour)) {
 				go (func() {
-					_, err = obtainCert(mainDomainAcmeClient, []string{"*" + string(mainDomainSuffix), string(mainDomainSuffix[1:])}, res, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB)
+					_, err = obtainCert(mainDomainAcmeClient, []string{"*" + mainDomainSuffix, mainDomainSuffix[1:]}, res, "", dnsProvider, mainDomainSuffix, acmeUseRateLimits, certDB)
 					if err != nil {
 						log.Error().Err(err).Msg("Couldn't renew certificate for main domain")
 					}
diff --git a/server/database/setup.go b/server/database/setup.go
index 1c5a0af..097c63e 100644
--- a/server/database/setup.go
+++ b/server/database/setup.go
@@ -44,7 +44,7 @@ func (p aDB) Get(name string) (*certificate.Resource, error) {
 	if resBytes == nil {
 		return nil, nil
 	}
-	if err = gob.NewDecoder(bytes.NewBuffer(resBytes)).Decode(cert); err != nil {
+	if err := gob.NewDecoder(bytes.NewBuffer(resBytes)).Decode(cert); err != nil {
 		return nil, err
 	}
 	return cert, nil
diff --git a/server/gitea/cache.go b/server/gitea/cache.go
index b11a370..85cbcde 100644
--- a/server/gitea/cache.go
+++ b/server/gitea/cache.go
@@ -42,9 +42,8 @@ func (f FileResponse) IsEmpty() bool {
 	return len(f.Body) != 0
 }
 
-func (f FileResponse) createHttpResponse(cacheKey string) (http.Header, int) {
-	header := make(http.Header)
-	var statusCode int
+func (f FileResponse) createHttpResponse(cacheKey string) (header http.Header, statusCode int) {
+	header = make(http.Header)
 
 	if f.Exists {
 		statusCode = http.StatusOK
diff --git a/server/handler/handler.go b/server/handler/handler.go
index 1a0a285..78301e9 100644
--- a/server/handler/handler.go
+++ b/server/handler/handler.go
@@ -28,7 +28,7 @@ func Handler(mainDomainSuffix, rawDomain string,
 	dnsLookupCache, canonicalDomainCache cache.SetGetKey,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, req *http.Request) {
-		log := log.With().Strs("Handler", []string{string(req.Host), req.RequestURI}).Logger()
+		log := log.With().Strs("Handler", []string{req.Host, req.RequestURI}).Logger()
 		ctx := context.New(w, req)
 
 		ctx.RespWriter.Header().Set("Server", "CodebergPages/"+version.Version)
diff --git a/server/handler/handler_custom_domain.go b/server/handler/handler_custom_domain.go
index 5ea7649..2f98085 100644
--- a/server/handler/handler_custom_domain.go
+++ b/server/handler/handler_custom_domain.go
@@ -54,7 +54,7 @@ func handleCustomDomain(log zerolog.Logger, ctx *context.Context, giteaClient *g
 			// only redirect if the target is also a codeberg page!
 			targetOwner, _, _ = dns.GetTargetFromDNS(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix, dnsLookupCache)
 			if targetOwner != "" {
-				ctx.Redirect("https://"+canonicalDomain+string(targetOpt.TargetPath), http.StatusTemporaryRedirect)
+				ctx.Redirect("https://"+canonicalDomain+targetOpt.TargetPath, http.StatusTemporaryRedirect)
 				return
 			}
 
diff --git a/server/handler/handler_sub_domain.go b/server/handler/handler_sub_domain.go
index 73177b6..1d769d4 100644
--- a/server/handler/handler_sub_domain.go
+++ b/server/handler/handler_sub_domain.go
@@ -29,7 +29,7 @@ func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gite
 
 	if targetOwner == "www" {
 		// www.codeberg.page redirects to codeberg.page // TODO: rm hardcoded - use cname?
-		ctx.Redirect("https://"+string(mainDomainSuffix[1:])+string(ctx.Path()), http.StatusPermanentRedirect)
+		ctx.Redirect("https://"+mainDomainSuffix[1:]+ctx.Path(), http.StatusPermanentRedirect)
 		return
 	}
 
diff --git a/server/handler/try.go b/server/handler/try.go
index bae3c44..5a09b91 100644
--- a/server/handler/try.go
+++ b/server/handler/try.go
@@ -21,8 +21,8 @@ func tryUpstream(ctx *context.Context, giteaClient *gitea.Client,
 ) {
 	// check if a canonical domain exists on a request on MainDomain
 	if strings.HasSuffix(trimmedHost, mainDomainSuffix) {
-		canonicalDomain, _ := options.CheckCanonicalDomain(giteaClient, "", string(mainDomainSuffix), canonicalDomainCache)
-		if !strings.HasSuffix(strings.SplitN(canonicalDomain, "/", 2)[0], string(mainDomainSuffix)) {
+		canonicalDomain, _ := options.CheckCanonicalDomain(giteaClient, "", mainDomainSuffix, canonicalDomainCache)
+		if !strings.HasSuffix(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix) {
 			canonicalPath := ctx.Req.RequestURI
 			if options.TargetRepo != defaultPagesRepo {
 				path := strings.SplitN(canonicalPath, "/", 3)
@@ -35,8 +35,8 @@ func tryUpstream(ctx *context.Context, giteaClient *gitea.Client,
 		}
 	}
 
-	// add host for debugging
-	options.Host = string(trimmedHost)
+	// Add host for debugging.
+	options.Host = trimmedHost
 
 	// Try to request the file from the Gitea API
 	if !options.Upstream(ctx, giteaClient) {
diff --git a/server/setup.go b/server/setup.go
index e7194ed..282e692 100644
--- a/server/setup.go
+++ b/server/setup.go
@@ -15,13 +15,13 @@ func SetupHTTPACMEChallengeServer(challengeCache cache.SetGetKey) http.HandlerFu
 	return func(w http.ResponseWriter, req *http.Request) {
 		ctx := context.New(w, req)
 		if strings.HasPrefix(ctx.Path(), challengePath) {
-			challenge, ok := challengeCache.Get(utils.TrimHostPort(ctx.Host()) + "/" + string(strings.TrimPrefix(ctx.Path(), challengePath)))
+			challenge, ok := challengeCache.Get(utils.TrimHostPort(ctx.Host()) + "/" + strings.TrimPrefix(ctx.Path(), challengePath))
 			if !ok || challenge == nil {
 				ctx.String("no challenge for this token", http.StatusNotFound)
 			}
 			ctx.String(challenge.(string))
 		} else {
-			ctx.Redirect("https://"+string(ctx.Host())+string(ctx.Path()), http.StatusMovedPermanently)
+			ctx.Redirect("https://"+ctx.Host()+ctx.Path(), http.StatusMovedPermanently)
 		}
 	}
 }
diff --git a/server/upstream/helper.go b/server/upstream/helper.go
index 428976b..a84d4f0 100644
--- a/server/upstream/helper.go
+++ b/server/upstream/helper.go
@@ -13,7 +13,7 @@ import (
 func (o *Options) GetBranchTimestamp(giteaClient *gitea.Client) (bool, error) {
 	log := log.With().Strs("BranchInfo", []string{o.TargetOwner, o.TargetRepo, o.TargetBranch}).Logger()
 
-	if len(o.TargetBranch) == 0 {
+	if o.TargetBranch == "" {
 		// Get default branch
 		defaultBranch, err := giteaClient.GiteaGetRepoDefaultBranch(o.TargetOwner, o.TargetRepo)
 		if err != nil {
diff --git a/server/upstream/upstream.go b/server/upstream/upstream.go
index b76b8e6..7c3c848 100644
--- a/server/upstream/upstream.go
+++ b/server/upstream/upstream.go
@@ -82,8 +82,8 @@ func (o *Options) Upstream(ctx *context.Context, giteaClient *gitea.Client) (fin
 
 	// Check if the browser has a cached version
 	if ctx.Response() != nil {
-		if ifModifiedSince, err := time.Parse(time.RFC1123, string(ctx.Response().Header.Get(headerIfModifiedSince))); err == nil {
-			if !ifModifiedSince.Before(o.BranchTimestamp) {
+		if ifModifiedSince, err := time.Parse(time.RFC1123, ctx.Response().Header.Get(headerIfModifiedSince)); err == nil {
+			if ifModifiedSince.After(o.BranchTimestamp) {
 				ctx.RespWriter.WriteHeader(http.StatusNotModified)
 				log.Trace().Msg("check response against last modified: valid")
 				return true

From caeb1a4acb0d35c78988fcd4e30d9ac51847f13a Mon Sep 17 00:00:00 2001
From: jklippel <jklippel@noreply.codeberg.org>
Date: Tue, 22 Nov 2022 21:26:10 +0000
Subject: [PATCH 2/8] Return a 404 if there is no repository (#141)

If no repository is found the user expects a 404 status code
instead of a dependency failed status code (as it was before).

Signed-off-by: Jan Klippel <c0d3b3rg@kl1pp3l.de>

Fixes: https://codeberg.org/Codeberg/Community/issues/809

Co-authored-by: Jan Klippel <c0d3b3rg@kl1pp3l.de>
Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/141
Reviewed-by: 6543 <6543@obermui.de>
Co-authored-by: jklippel <jklippel@noreply.codeberg.org>
Co-committed-by: jklippel <jklippel@noreply.codeberg.org>
---
 server/handler/handler_sub_domain.go |  4 ++--
 server/handler/handler_test.go       | 35 ++++++++++++++--------------
 2 files changed, 20 insertions(+), 19 deletions(-)

diff --git a/server/handler/handler_sub_domain.go b/server/handler/handler_sub_domain.go
index 1d769d4..2a75e9f 100644
--- a/server/handler/handler_sub_domain.go
+++ b/server/handler/handler_sub_domain.go
@@ -115,6 +115,6 @@ func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gite
 
 	// Couldn't find a valid repo/branch
 	html.ReturnErrorPage(ctx,
-		fmt.Sprintf("couldn't find a valid repo[%s]", targetRepo),
-		http.StatusFailedDependency)
+		fmt.Sprintf("could not find a valid repository[%s]", targetRepo),
+		http.StatusNotFound)
 }
diff --git a/server/handler/handler_test.go b/server/handler/handler_test.go
index f5538c9..626564a 100644
--- a/server/handler/handler_test.go
+++ b/server/handler/handler_test.go
@@ -23,26 +23,27 @@ func TestHandlerPerformance(t *testing.T) {
 	)
 
 	testCase := func(uri string, status int) {
-		req := httptest.NewRequest("GET", uri, nil)
-		w := httptest.NewRecorder()
+		t.Run(uri, func(t *testing.T) {
+			req := httptest.NewRequest("GET", uri, nil)
+			w := httptest.NewRecorder()
 
-		log.Printf("Start: %v\n", time.Now())
-		start := time.Now()
-		testHandler(w, req)
-		end := time.Now()
-		log.Printf("Done: %v\n", time.Now())
+			log.Printf("Start: %v\n", time.Now())
+			start := time.Now()
+			testHandler(w, req)
+			end := time.Now()
+			log.Printf("Done: %v\n", time.Now())
 
-		resp := w.Result()
+			resp := w.Result()
 
-		if resp.StatusCode != status {
-			t.Errorf("request failed with status code %d", resp.StatusCode)
-		} else {
-			t.Logf("request took %d milliseconds", end.Sub(start).Milliseconds())
-		}
+			if resp.StatusCode != status {
+				t.Errorf("request failed with status code %d", resp.StatusCode)
+			} else {
+				t.Logf("request took %d milliseconds", end.Sub(start).Milliseconds())
+			}
+		})
 	}
 
-	testCase("https://mondstern.codeberg.page/", 424) // TODO: expect 200
-	testCase("https://mondstern.codeberg.page/", 424) // TODO: expect 200
-	testCase("https://example.momar.xyz/", 424)       // TODO: expect 200
-	testCase("https://codeberg.page/", 424)           // TODO: expect 200
+	testCase("https://mondstern.codeberg.page/", 404) // TODO: expect 200
+	testCase("https://codeberg.page/", 404)           // TODO: expect 200
+	testCase("https://example.momar.xyz/", 424)
 }

From 5e72753e916d1076cfd772d3d6c7f86474c6c87a Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Tue, 22 Nov 2022 22:30:53 +0100
Subject: [PATCH 3/8] ci: "docker-tag" use `tags`

---
 .woodpecker.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.woodpecker.yml b/.woodpecker.yml
index 20254fe..7e6d694 100644
--- a/.woodpecker.yml
+++ b/.woodpecker.yml
@@ -102,7 +102,7 @@ pipeline:
       registry: codeberg.org
       dockerfile: Dockerfile
       repo: codeberg.org/codeberg/pages-server
-      tag: [ latest, "${CI_COMMIT_TAG}" ]
+      tags: [ latest, "${CI_COMMIT_TAG}" ]
       username:
         from_secret: bot_user
       password:

From dcf03fc078228a286384757c3501ab5df5306062 Mon Sep 17 00:00:00 2001
From: crapStone <crapstone@noreply.codeberg.org>
Date: Fri, 2 Dec 2022 15:25:25 +0000
Subject: [PATCH 4/8] Fix error page (#144)

Co-authored-by: crapStone <crapstone01@gmail.com>
Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/144
Reviewed-by: 6543 <6543@obermui.de>
Co-authored-by: crapStone <crapstone@noreply.codeberg.org>
Co-committed-by: crapStone <crapstone@noreply.codeberg.org>
---
 html/error.go | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/html/error.go b/html/error.go
index 826c42b..ac222c4 100644
--- a/html/error.go
+++ b/html/error.go
@@ -1,6 +1,7 @@
 package html
 
 import (
+	"html/template"
 	"net/http"
 	"strconv"
 	"strings"
@@ -39,7 +40,8 @@ func errorMessage(statusCode int) string {
 
 // TODO: use template engine
 func errorBody(statusCode int) string {
-	return strings.ReplaceAll(NotFoundPage,
-		"%status%",
-		strconv.Itoa(statusCode)+" "+errorMessage(statusCode))
+	return template.HTMLEscapeString(
+		strings.ReplaceAll(NotFoundPage,
+			"%status%",
+			strconv.Itoa(statusCode)+" "+errorMessage(statusCode)))
 }

From 9d769aeee7bdbdce03bbb0f6fb213d4b7d45a701 Mon Sep 17 00:00:00 2001
From: crapStone <crapstone@noreply.codeberg.org>
Date: Sun, 4 Dec 2022 21:24:58 +0000
Subject: [PATCH 5/8] Fix error page generation (#145)

Co-authored-by: crapStone <crapstone01@gmail.com>
Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/145
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: 6543 <6543@obermui.de>
Co-authored-by: crapStone <crapstone@noreply.codeberg.org>
Co-committed-by: crapStone <crapstone@noreply.codeberg.org>
---
 Justfile           |  4 ++--
 html/error.go      | 31 +++++++++++++++++--------------
 html/error_test.go | 38 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 57 insertions(+), 16 deletions(-)
 create mode 100644 html/error_test.go

diff --git a/Justfile b/Justfile
index f7ea414..7d72fe8 100644
--- a/Justfile
+++ b/Justfile
@@ -38,10 +38,10 @@ tool-gofumpt:
     fi
 
 test:
-    go test -race codeberg.org/codeberg/pages/server/...
+    go test -race codeberg.org/codeberg/pages/server/... codeberg.org/codeberg/pages/html/
 
 test-run TEST:
-    go test -race -run "^{{TEST}}$" codeberg.org/codeberg/pages/server/...
+    go test -race -run "^{{TEST}}$" codeberg.org/codeberg/pages/server/... codeberg.org/codeberg/pages/html/
 
 integration:
     go test -race -tags integration codeberg.org/codeberg/pages/integration/...
diff --git a/html/error.go b/html/error.go
index ac222c4..206b123 100644
--- a/html/error.go
+++ b/html/error.go
@@ -15,16 +15,27 @@ func ReturnErrorPage(ctx *context.Context, msg string, statusCode int) {
 	ctx.RespWriter.Header().Set("Content-Type", "text/html; charset=utf-8")
 	ctx.RespWriter.WriteHeader(statusCode)
 
-	if msg == "" {
-		msg = errorBody(statusCode)
-	} else {
-		// TODO: use template engine
-		msg = strings.ReplaceAll(strings.ReplaceAll(ErrorPage, "%message%", msg), "%status%", http.StatusText(statusCode))
-	}
+	msg = generateResponse(msg, statusCode)
 
 	_, _ = ctx.RespWriter.Write([]byte(msg))
 }
 
+// TODO: use template engine
+func generateResponse(msg string, statusCode int) string {
+	if msg == "" {
+		msg = strings.ReplaceAll(NotFoundPage,
+			"%status%",
+			strconv.Itoa(statusCode)+" "+errorMessage(statusCode))
+	} else {
+		msg = strings.ReplaceAll(
+			strings.ReplaceAll(ErrorPage, "%message%", template.HTMLEscapeString(msg)),
+			"%status%",
+			http.StatusText(statusCode))
+	}
+
+	return msg
+}
+
 func errorMessage(statusCode int) string {
 	message := http.StatusText(statusCode)
 
@@ -37,11 +48,3 @@ func errorMessage(statusCode int) string {
 
 	return message
 }
-
-// TODO: use template engine
-func errorBody(statusCode int) string {
-	return template.HTMLEscapeString(
-		strings.ReplaceAll(NotFoundPage,
-			"%status%",
-			strconv.Itoa(statusCode)+" "+errorMessage(statusCode)))
-}
diff --git a/html/error_test.go b/html/error_test.go
new file mode 100644
index 0000000..f5da08c
--- /dev/null
+++ b/html/error_test.go
@@ -0,0 +1,38 @@
+package html
+
+import (
+	"net/http"
+	"strings"
+	"testing"
+)
+
+func TestValidMessage(t *testing.T) {
+	testString := "requested blacklisted path"
+	statusCode := http.StatusForbidden
+
+	expected := strings.ReplaceAll(
+		strings.ReplaceAll(ErrorPage, "%message%", testString),
+		"%status%",
+		http.StatusText(statusCode))
+	actual := generateResponse(testString, statusCode)
+
+	if expected != actual {
+		t.Errorf("generated response did not match: expected: '%s', got: '%s'", expected, actual)
+	}
+}
+
+func TestMessageWithHtml(t *testing.T) {
+	testString := `abc<img src=1 onerror=alert("xss");`
+	escapedString := "abc&lt;img src=1 onerror=alert(&#34;xss&#34;);"
+	statusCode := http.StatusNotFound
+
+	expected := strings.ReplaceAll(
+		strings.ReplaceAll(ErrorPage, "%message%", escapedString),
+		"%status%",
+		http.StatusText(statusCode))
+	actual := generateResponse(testString, statusCode)
+
+	if expected != actual {
+		t.Errorf("generated response did not match: expected: '%s', got: '%s'", expected, actual)
+	}
+}

From 98d198d4197f35af4a50dafab6ff6766eac06faf Mon Sep 17 00:00:00 2001
From: Gusted <gusted@noreply.codeberg.org>
Date: Wed, 4 Jan 2023 04:51:27 +0000
Subject: [PATCH 6/8] Safely get certificate's leaf (#150)

- It's not guaranteed that `tls.X509KeyPair` will set `c.Leaf`.
- This patch fixes this by using a wrapper that parses the leaf
certificate(in bytes) if `c.Leaf` wasn't set.
- Resolves #149

Co-authored-by: Gusted <postmaster@gusted.xyz>
Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/150
Reviewed-by: 6543 <6543@obermui.de>
Co-authored-by: Gusted <gusted@noreply.codeberg.org>
Co-committed-by: Gusted <gusted@noreply.codeberg.org>
---
 server/certificates/certificates.go | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/server/certificates/certificates.go b/server/certificates/certificates.go
index 8af4be5..e3cdbfb 100644
--- a/server/certificates/certificates.go
+++ b/server/certificates/certificates.go
@@ -303,7 +303,11 @@ func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Re
 		log.Error().Err(err).Msgf("Couldn't obtain again a certificate or %v", domains)
 		if renew != nil && renew.CertURL != "" {
 			tlsCertificate, err := tls.X509KeyPair(renew.Certificate, renew.PrivateKey)
-			if err == nil && tlsCertificate.Leaf.NotAfter.After(time.Now()) {
+			if err != nil {
+				return mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase), err
+			}
+			leaf, err := leaf(&tlsCertificate)
+			if err == nil && leaf.NotAfter.After(time.Now()) {
 				// avoid sending a mock cert instead of a still valid cert, instead abuse CSR field to store time to try again at
 				renew.CSR = []byte(strconv.FormatInt(time.Now().Add(6*time.Hour).Unix(), 10))
 				if err := keyDatabase.Put(name, renew); err != nil {
@@ -529,3 +533,12 @@ func MaintainCertDB(ctx context.Context, interval time.Duration, mainDomainSuffi
 		}
 	}
 }
+
+// leaf returns the parsed leaf certificate, either from c.leaf or by parsing
+// the corresponding c.Certificate[0].
+func leaf(c *tls.Certificate) (*x509.Certificate, error) {
+	if c.Leaf != nil {
+		return c.Leaf, nil
+	}
+	return x509.ParseCertificate(c.Certificate[0])
+}

From f7fad2a5ae0e4942848604a77199f4f1f8a1c3b4 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Wed, 4 Jan 2023 06:08:06 +0100
Subject: [PATCH 7/8] Integration Tests use https://codeberg.org/cb_pages_tests

---
 integration/get_test.go | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/integration/get_test.go b/integration/get_test.go
index 81d8488..f13ce8b 100644
--- a/integration/get_test.go
+++ b/integration/get_test.go
@@ -31,7 +31,7 @@ func TestGetRedirect(t *testing.T) {
 func TestGetContent(t *testing.T) {
 	log.Println("=== TestGetContent ===")
 	// test get image
-	resp, err := getTestHTTPSClient().Get("https://magiclike.localhost.mock.directory:4430/images/827679288a.jpg")
+	resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/images/827679288a.jpg")
 	assert.NoError(t, err)
 	if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) {
 		t.FailNow()
@@ -42,7 +42,7 @@ func TestGetContent(t *testing.T) {
 	assert.Len(t, resp.Header.Get("ETag"), 42)
 
 	// specify branch
-	resp, err = getTestHTTPSClient().Get("https://momar.localhost.mock.directory:4430/pag/@master/")
+	resp, err = getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/pag/@master/")
 	assert.NoError(t, err)
 	if !assert.NotNil(t, resp) {
 		t.FailNow()
@@ -53,7 +53,7 @@ func TestGetContent(t *testing.T) {
 	assert.Len(t, resp.Header.Get("ETag"), 44)
 
 	// access branch name contains '/'
-	resp, err = getTestHTTPSClient().Get("https://blumia.localhost.mock.directory:4430/pages-server-integration-tests/@docs~main/")
+	resp, err = getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/blumia/@docs~main/")
 	assert.NoError(t, err)
 	if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) {
 		t.FailNow()
@@ -81,7 +81,7 @@ func TestCustomDomain(t *testing.T) {
 func TestGetNotFound(t *testing.T) {
 	log.Println("=== TestGetNotFound ===")
 	// test custom not found pages
-	resp, err := getTestHTTPSClient().Get("https://crystal.localhost.mock.directory:4430/pages-404-demo/blah")
+	resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/pages-404-demo/blah")
 	assert.NoError(t, err)
 	if !assert.NotNil(t, resp) {
 		t.FailNow()
@@ -95,7 +95,7 @@ func TestGetNotFound(t *testing.T) {
 func TestFollowSymlink(t *testing.T) {
 	log.Printf("=== TestFollowSymlink ===\n")
 
-	resp, err := getTestHTTPSClient().Get("https://6543.localhost.mock.directory:4430/tests_for_pages-server/@main/link")
+	resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/tests_for_pages-server/@main/link")
 	assert.NoError(t, err)
 	if !assert.NotNil(t, resp) {
 		t.FailNow()
@@ -111,7 +111,7 @@ func TestFollowSymlink(t *testing.T) {
 func TestLFSSupport(t *testing.T) {
 	log.Printf("=== TestLFSSupport ===\n")
 
-	resp, err := getTestHTTPSClient().Get("https://6543.localhost.mock.directory:4430/tests_for_pages-server/@main/lfs.txt")
+	resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/tests_for_pages-server/@main/lfs.txt")
 	assert.NoError(t, err)
 	if !assert.NotNil(t, resp) {
 		t.FailNow()

From c286b3b1d0a21bc5cc447df7744da9894d857e6a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Felipe=20Leopoldo=20Sologuren=20Guti=C3=A9rrez?=
 <fsologureng@noreply.codeberg.org>
Date: Wed, 4 Jan 2023 05:26:14 +0000
Subject: [PATCH 8/8] Added TokenBucket to limit the rate of validation
 failures (#151)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Added new TockenBucket named `acmeClientFailLimit` to avoid being banned because of the [Failed validation limit](https://letsencrypt.org/docs/failed-validation-limit/) of Let's Encrypt.

The behaviour is similar to the other limiters blocking the `obtainCert` func ensuring rate under limit.

Co-authored-by: fsologureng <sologuren@estudiohum.cl>
Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/151
Reviewed-by: 6543 <6543@obermui.de>
Co-authored-by: Felipe Leopoldo Sologuren GutiƩrrez <fsologureng@noreply.codeberg.org>
Co-committed-by: Felipe Leopoldo Sologuren GutiƩrrez <fsologureng@noreply.codeberg.org>
---
 server/certificates/certificates.go | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/server/certificates/certificates.go b/server/certificates/certificates.go
index e3cdbfb..1aa90a0 100644
--- a/server/certificates/certificates.go
+++ b/server/certificates/certificates.go
@@ -163,6 +163,9 @@ var acmeClientOrderLimit = equalizer.NewTokenBucket(25, 15*time.Minute)
 // rate limit is 20 / second, we want 5 / second (especially as one cert takes at least two requests)
 var acmeClientRequestLimit = equalizer.NewTokenBucket(5, 1*time.Second)
 
+// rate limit is 5 / hour https://letsencrypt.org/docs/failed-validation-limit/
+var acmeClientFailLimit = equalizer.NewTokenBucket(5, 1*time.Hour)
+
 type AcmeTLSChallengeProvider struct {
 	challengeCache cache.SetGetKey
 }
@@ -278,6 +281,9 @@ func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Re
 		res, err = acmeClient.Certificate.Renew(*renew, true, false, "")
 		if err != nil {
 			log.Error().Err(err).Msgf("Couldn't renew certificate for %v, trying to request a new one", domains)
+			if acmeUseRateLimits {
+				acmeClientFailLimit.Take()
+			}
 			res = nil
 		}
 	}
@@ -298,6 +304,9 @@ func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Re
 			Bundle:     true,
 			MustStaple: false,
 		})
+		if acmeUseRateLimits && err != nil {
+			acmeClientFailLimit.Take()
+		}
 	}
 	if err != nil {
 		log.Error().Err(err).Msgf("Couldn't obtain again a certificate or %v", domains)