From 46316f9e2f472b070d187ee1e06742dea0739413 Mon Sep 17 00:00:00 2001 From: crystal Date: Sat, 11 Feb 2023 03:12:42 +0000 Subject: [PATCH 1/5] Fix raw domain for branches with custom domains and index.html (#159) fix #156 fix #157 Co-authored-by: 6543 <6543@obermui.de> Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/159 Reviewed-by: 6543 <6543@obermui.de> Co-authored-by: crystal Co-committed-by: crystal --- integration/get_test.go | 28 ++++++++++++++++++++++++++++ server/handler/try.go | 2 +- server/upstream/upstream.go | 2 +- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/integration/get_test.go b/integration/get_test.go index 55e6d12..b70eeed 100644 --- a/integration/get_test.go +++ b/integration/get_test.go @@ -109,6 +109,34 @@ func TestCustomDomainRedirects(t *testing.T) { assert.EqualValues(t, "https://mock-pages.codeberg-test.org/README.md", resp.Header.Get("Location")) } +func TestRawCustomDomain(t *testing.T) { + log.Println("=== TestRawCustomDomain ===") + // test raw domain response for custom domain branch + resp, err := getTestHTTPSClient().Get("https://raw.localhost.mock.directory:4430/cb_pages_tests/raw-test/example") // need cb_pages_tests fork + assert.NoError(t, err) + if !assert.NotNil(t, resp) { + t.FailNow() + } + assert.EqualValues(t, http.StatusOK, resp.StatusCode) + assert.EqualValues(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type")) + assert.EqualValues(t, "76", resp.Header.Get("Content-Length")) + assert.EqualValues(t, 76, getSize(resp.Body)) +} + +func TestRawIndex(t *testing.T) { + log.Println("=== TestRawCustomDomain ===") + // test raw domain response for index.html + resp, err := getTestHTTPSClient().Get("https://raw.localhost.mock.directory:4430/cb_pages_tests/raw-test/@branch-test/index.html") // need cb_pages_tests fork + assert.NoError(t, err) + if !assert.NotNil(t, resp) { + t.FailNow() + } + assert.EqualValues(t, http.StatusOK, resp.StatusCode) + assert.EqualValues(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type")) + assert.EqualValues(t, "597", resp.Header.Get("Content-Length")) + assert.EqualValues(t, 597, getSize(resp.Body)) +} + func TestGetNotFound(t *testing.T) { log.Println("=== TestGetNotFound ===") // test custom not found pages diff --git a/server/handler/try.go b/server/handler/try.go index 5a09b91..5c65138 100644 --- a/server/handler/try.go +++ b/server/handler/try.go @@ -20,7 +20,7 @@ func tryUpstream(ctx *context.Context, giteaClient *gitea.Client, canonicalDomainCache cache.SetGetKey, ) { // check if a canonical domain exists on a request on MainDomain - if strings.HasSuffix(trimmedHost, mainDomainSuffix) { + if strings.HasSuffix(trimmedHost, mainDomainSuffix) && !options.ServeRaw { canonicalDomain, _ := options.CheckCanonicalDomain(giteaClient, "", mainDomainSuffix, canonicalDomainCache) if !strings.HasSuffix(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix) { canonicalPath := ctx.Req.RequestURI diff --git a/server/upstream/upstream.go b/server/upstream/upstream.go index 7c3c848..3845969 100644 --- a/server/upstream/upstream.go +++ b/server/upstream/upstream.go @@ -168,7 +168,7 @@ func (o *Options) Upstream(ctx *context.Context, giteaClient *gitea.Client) (fin ctx.Redirect(ctx.Path()+"/", http.StatusTemporaryRedirect) return true } - if strings.HasSuffix(ctx.Path(), "/index.html") { + if strings.HasSuffix(ctx.Path(), "/index.html") && !o.ServeRaw { ctx.Redirect(strings.TrimSuffix(ctx.Path(), "index.html"), http.StatusTemporaryRedirect) return true } From 9a3d1c36dce7c7d9c460dfdb684f6e4bae5848f9 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Mon, 13 Feb 2023 20:14:45 +0000 Subject: [PATCH 2/5] Document more flags & make http port customizable (#183) Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/183 --- .gitignore | 1 + Justfile | 3 ++ cmd/flags.go | 34 +++++++++++------- cmd/main.go | 40 +++++++++++---------- cmd/setup.go | 5 +++ server/certificates/acme_config.go | 2 ++ server/certificates/cached_challengers.go | 19 ++++++++++ server/certificates/certificates.go | 43 +++++++++++++---------- server/setup.go | 27 -------------- 9 files changed, 97 insertions(+), 77 deletions(-) delete mode 100644 server/setup.go diff --git a/.gitignore b/.gitignore index 8745935..3035107 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ build/ vendor/ pages certs.sqlite +.bash_history diff --git a/Justfile b/Justfile index 0db7845..9ee7eb3 100644 --- a/Justfile +++ b/Justfile @@ -50,3 +50,6 @@ integration: integration-run TEST: go test -race -tags 'integration {{TAGS}}' -run "^{{TEST}}$" codeberg.org/codeberg/pages/integration/... + +docker: + docker run --rm -it --user $(id -u) -v $(pwd):/work --workdir /work -e HOME=/work codeberg.org/6543/docker-images/golang_just diff --git a/cmd/flags.go b/cmd/flags.go index 8052421..5bc638b 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -8,11 +8,13 @@ var ( CertStorageFlags = []cli.Flag{ &cli.StringFlag{ Name: "db-type", + Usage: "Specify the database driver. Valid options are \"sqlite3\", \"mysql\" and \"postgres\". Read more at https://xorm.io", Value: "sqlite3", EnvVars: []string{"DB_TYPE"}, }, &cli.StringFlag{ Name: "db-conn", + Usage: "Specify the database connection. For \"sqlite3\" it's the filepath. Read more at https://go.dev/doc/tutorial/database-access", Value: "certs.sqlite", EnvVars: []string{"DB_CONN"}, }, @@ -87,15 +89,21 @@ var ( EnvVars: []string{"HOST"}, Value: "[::]", }, - &cli.StringFlag{ + &cli.UintFlag{ Name: "port", - Usage: "specifies port of listening address", - EnvVars: []string{"PORT"}, - Value: "443", + Usage: "specifies the https port to listen to ssl requests", + EnvVars: []string{"PORT", "HTTPS_PORT"}, + Value: 443, + }, + &cli.UintFlag{ + Name: "http-port", + Usage: "specifies the http port, you also have to enable http server via ENABLE_HTTP_SERVER=true", + EnvVars: []string{"HTTP_PORT"}, + Value: 80, }, &cli.BoolFlag{ - Name: "enable-http-server", - // TODO: desc + Name: "enable-http-server", + Usage: "start a http server to redirect to https and respond to http acme challenges", EnvVars: []string{"ENABLE_HTTP_SERVER"}, }, &cli.StringFlag{ @@ -125,23 +133,23 @@ var ( Value: true, }, &cli.BoolFlag{ - Name: "acme-accept-terms", - // TODO: Usage + Name: "acme-accept-terms", + Usage: "To accept the ACME ToS", EnvVars: []string{"ACME_ACCEPT_TERMS"}, }, &cli.StringFlag{ - Name: "acme-eab-kid", - // TODO: Usage + Name: "acme-eab-kid", + Usage: "Register the current account to the ACME server with external binding.", EnvVars: []string{"ACME_EAB_KID"}, }, &cli.StringFlag{ - Name: "acme-eab-hmac", - // TODO: Usage + Name: "acme-eab-hmac", + Usage: "Register the current account to the ACME server with external binding.", EnvVars: []string{"ACME_EAB_HMAC"}, }, &cli.StringFlag{ Name: "dns-provider", - Usage: "Use DNS-Challenge for main domain\n\nRead more at: https://go-acme.github.io/lego/dns/", + Usage: "Use DNS-Challenge for main domain. Read more at: https://go-acme.github.io/lego/dns/", EnvVars: []string{"DNS_PROVIDER"}, }, &cli.StringFlag{ diff --git a/cmd/main.go b/cmd/main.go index a1c3b97..8a65d43 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -14,7 +14,6 @@ import ( "github.com/rs/zerolog/log" "github.com/urfave/cli/v2" - "codeberg.org/codeberg/pages/server" "codeberg.org/codeberg/pages/server/cache" "codeberg.org/codeberg/pages/server/certificates" "codeberg.org/codeberg/pages/server/gitea" @@ -48,7 +47,9 @@ func Serve(ctx *cli.Context) error { rawDomain := ctx.String("raw-domain") mainDomainSuffix := ctx.String("pages-domain") rawInfoPage := ctx.String("raw-info-page") - listeningAddress := fmt.Sprintf("%s:%s", ctx.String("host"), ctx.String("port")) + listeningHost := ctx.String("host") + listeningSSLAddress := fmt.Sprintf("%s:%d", listeningHost, ctx.Uint("port")) + listeningHTTPAddress := fmt.Sprintf("%s:%d", listeningHost, ctx.Uint("http-port")) enableHTTPServer := ctx.Bool("enable-http-server") allowedCorsDomains := AllowedCorsDomains @@ -91,22 +92,14 @@ func Serve(ctx *cli.Context) error { return err } - // Create handler based on settings - httpsHandler := handler.Handler(mainDomainSuffix, rawDomain, - giteaClient, - rawInfoPage, - BlacklistedPaths, allowedCorsDomains, - dnsLookupCache, canonicalDomainCache) - - httpHandler := server.SetupHTTPACMEChallengeServer(challengeCache) - - // Setup listener and TLS - log.Info().Msgf("Listening on https://%s", listeningAddress) - listener, err := net.Listen("tcp", listeningAddress) + // Create listener for SSL connections + log.Info().Msgf("Listening on https://%s", listeningSSLAddress) + listener, err := net.Listen("tcp", listeningSSLAddress) if err != nil { return fmt.Errorf("couldn't create listener: %v", err) } + // Setup listener for SSL connections listener = tls.NewListener(listener, certificates.TLSConfig(mainDomainSuffix, giteaClient, acmeClient, @@ -119,18 +112,29 @@ func Serve(ctx *cli.Context) error { go certificates.MaintainCertDB(certMaintainCtx, interval, acmeClient, mainDomainSuffix, certDB) if enableHTTPServer { + // Create handler for http->https redirect and http acme challenges + httpHandler := certificates.SetupHTTPACMEChallengeServer(challengeCache) + + // Create listener for http and start listening go func() { - log.Info().Msg("Start HTTP server listening on :80") - err := http.ListenAndServe("[::]:80", httpHandler) + log.Info().Msgf("Start HTTP server listening on %s", listeningHTTPAddress) + err := http.ListenAndServe(listeningHTTPAddress, httpHandler) if err != nil { log.Panic().Err(err).Msg("Couldn't start HTTP fastServer") } }() } - // Start the web fastServer + // Create ssl handler based on settings + sslHandler := handler.Handler(mainDomainSuffix, rawDomain, + giteaClient, + rawInfoPage, + BlacklistedPaths, allowedCorsDomains, + dnsLookupCache, canonicalDomainCache) + + // Start the ssl listener log.Info().Msgf("Start listening on %s", listener.Addr()) - if err := http.Serve(listener, httpsHandler); err != nil { + if err := http.Serve(listener, sslHandler); err != nil { log.Panic().Err(err).Msg("Couldn't start fastServer") } diff --git a/cmd/setup.go b/cmd/setup.go index bb9f8cb..cde4bc9 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -43,6 +43,11 @@ func createAcmeClient(ctx *cli.Context, enableHTTPServer bool, challengeCache ca if (!acmeAcceptTerms || dnsProvider == "") && acmeAPI != "https://acme.mock.directory" { return nil, fmt.Errorf("%w: you must set $ACME_ACCEPT_TERMS and $DNS_PROVIDER, unless $ACME_API is set to https://acme.mock.directory", ErrAcmeMissConfig) } + if acmeEabHmac != "" && acmeEabKID == "" { + return nil, fmt.Errorf("%w: ACME_EAB_HMAC also needs ACME_EAB_KID to be set", ErrAcmeMissConfig) + } else if acmeEabHmac == "" && acmeEabKID != "" { + return nil, fmt.Errorf("%w: ACME_EAB_KID also needs ACME_EAB_HMAC to be set", ErrAcmeMissConfig) + } return certificates.NewAcmeClient( acmeAccountConf, diff --git a/server/certificates/acme_config.go b/server/certificates/acme_config.go index 69568e6..12ad7c6 100644 --- a/server/certificates/acme_config.go +++ b/server/certificates/acme_config.go @@ -14,6 +14,8 @@ import ( "github.com/rs/zerolog/log" ) +const challengePath = "/.well-known/acme-challenge/" + func setupAcmeConfig(configFile, acmeAPI, acmeMail, acmeEabHmac, acmeEabKID string, acmeAcceptTerms bool) (*lego.Config, error) { var myAcmeAccount AcmeAccount var myAcmeConfig *lego.Config diff --git a/server/certificates/cached_challengers.go b/server/certificates/cached_challengers.go index 6ce6e67..02474b3 100644 --- a/server/certificates/cached_challengers.go +++ b/server/certificates/cached_challengers.go @@ -1,11 +1,15 @@ package certificates import ( + "net/http" + "strings" "time" "github.com/go-acme/lego/v4/challenge" "codeberg.org/codeberg/pages/server/cache" + "codeberg.org/codeberg/pages/server/context" + "codeberg.org/codeberg/pages/server/utils" ) type AcmeTLSChallengeProvider struct { @@ -39,3 +43,18 @@ func (a AcmeHTTPChallengeProvider) CleanUp(domain, token, _ string) error { a.challengeCache.Remove(domain + "/" + token) return nil } + +func SetupHTTPACMEChallengeServer(challengeCache cache.SetGetKey) http.HandlerFunc { + 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()) + "/" + 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://"+ctx.Host()+ctx.Path(), http.StatusMovedPermanently) + } + } +} diff --git a/server/certificates/certificates.go b/server/certificates/certificates.go index 3ea440f..707672c 100644 --- a/server/certificates/certificates.go +++ b/server/certificates/certificates.go @@ -36,22 +36,23 @@ func TLSConfig(mainDomainSuffix string, return &tls.Config{ // check DNS name & get certificate from Let's Encrypt GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { - sni := strings.ToLower(strings.TrimSpace(info.ServerName)) - if len(sni) < 1 { - return nil, errors.New("missing sni") + domain := strings.ToLower(strings.TrimSpace(info.ServerName)) + if len(domain) < 1 { + return nil, errors.New("missing domain info via SNI (RFC 4366, Section 3.1)") } + // https request init is actually a acme challenge if info.SupportedProtos != nil { for _, proto := range info.SupportedProtos { if proto != tlsalpn01.ACMETLS1Protocol { continue } - challenge, ok := challengeCache.Get(sni) + challenge, ok := challengeCache.Get(domain) if !ok { return nil, errors.New("no challenge for this domain") } - cert, err := tlsalpn01.ChallengeCert(sni, challenge.(string)) + cert, err := tlsalpn01.ChallengeCert(domain, challenge.(string)) if err != nil { return nil, err } @@ -61,22 +62,22 @@ func TLSConfig(mainDomainSuffix string, targetOwner := "" mayObtainCert := true - if strings.HasSuffix(sni, mainDomainSuffix) || strings.EqualFold(sni, mainDomainSuffix[1:]) { + if strings.HasSuffix(domain, mainDomainSuffix) || strings.EqualFold(domain, mainDomainSuffix[1:]) { // deliver default certificate for the main domain (*.codeberg.page) - sni = mainDomainSuffix + domain = mainDomainSuffix } else { var targetRepo, targetBranch string - targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(sni, mainDomainSuffix, dnsLookupCache) + targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(domain, mainDomainSuffix, dnsLookupCache) if targetOwner == "" { // DNS not set up, return main certificate to redirect to the docs - sni = mainDomainSuffix + domain = mainDomainSuffix } else { targetOpt := &upstream.Options{ TargetOwner: targetOwner, TargetRepo: targetRepo, TargetBranch: targetBranch, } - _, valid := targetOpt.CheckCanonicalDomain(giteaClient, sni, mainDomainSuffix, canonicalDomainCache) + _, valid := targetOpt.CheckCanonicalDomain(giteaClient, domain, mainDomainSuffix, canonicalDomainCache) if !valid { // We shouldn't obtain a certificate when we cannot check if the // repository has specified this domain in the `.domains` file. @@ -85,30 +86,34 @@ func TLSConfig(mainDomainSuffix string, } } - if tlsCertificate, ok := keyCache.Get(sni); ok { + if tlsCertificate, ok := keyCache.Get(domain); ok { // we can use an existing certificate object return tlsCertificate.(*tls.Certificate), nil } var tlsCertificate *tls.Certificate var err error - if tlsCertificate, err = acmeClient.retrieveCertFromDB(sni, mainDomainSuffix, false, certDB); err != nil { - // request a new certificate - if strings.EqualFold(sni, mainDomainSuffix) { + if tlsCertificate, err = acmeClient.retrieveCertFromDB(domain, mainDomainSuffix, false, certDB); err != nil { + if !errors.Is(err, database.ErrNotFound) { + return nil, err + } + // we could not find a cert in db, request a new certificate + + // first check if we are allowed to obtain a cert for this domain + if strings.EqualFold(domain, mainDomainSuffix) { return nil, errors.New("won't request certificate for main domain, something really bad has happened") } - if !mayObtainCert { - return nil, fmt.Errorf("won't request certificate for %q", sni) + return nil, fmt.Errorf("won't request certificate for %q", domain) } - tlsCertificate, err = acmeClient.obtainCert(acmeClient.legoClient, []string{sni}, nil, targetOwner, false, mainDomainSuffix, certDB) + tlsCertificate, err = acmeClient.obtainCert(acmeClient.legoClient, []string{domain}, nil, targetOwner, false, mainDomainSuffix, certDB) if err != nil { return nil, err } } - if err := keyCache.Set(sni, tlsCertificate, 15*time.Minute); err != nil { + if err := keyCache.Set(domain, tlsCertificate, 15*time.Minute); err != nil { return nil, err } return tlsCertificate, nil @@ -164,7 +169,7 @@ func (c *AcmeClient) retrieveCertFromDB(sni, mainDomainSuffix string, useDnsProv if !strings.EqualFold(sni, mainDomainSuffix) { tlsCertificate.Leaf, err = x509.ParseCertificate(tlsCertificate.Certificate[0]) if err != nil { - return nil, fmt.Errorf("error parsin leaf tlsCert: %w", err) + return nil, fmt.Errorf("error parsing leaf tlsCert: %w", err) } // renew certificates 7 days before they expire diff --git a/server/setup.go b/server/setup.go deleted file mode 100644 index 282e692..0000000 --- a/server/setup.go +++ /dev/null @@ -1,27 +0,0 @@ -package server - -import ( - "net/http" - "strings" - - "codeberg.org/codeberg/pages/server/cache" - "codeberg.org/codeberg/pages/server/context" - "codeberg.org/codeberg/pages/server/utils" -) - -func SetupHTTPACMEChallengeServer(challengeCache cache.SetGetKey) http.HandlerFunc { - challengePath := "/.well-known/acme-challenge/" - - 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()) + "/" + 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://"+ctx.Host()+ctx.Path(), http.StatusMovedPermanently) - } - } -} From 42b3f8d1b7933e67a2a24867d1189d522f2fb884 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Mon, 13 Feb 2023 23:13:30 +0000 Subject: [PATCH 3/5] use mockery for mock code generation (#185) close #181 Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/185 --- server/certificates/mock_test.go | 6 +- server/database/interface.go | 3 + server/database/mock.go | 139 +++++++++++++++++++++++-------- 3 files changed, 113 insertions(+), 35 deletions(-) diff --git a/server/certificates/mock_test.go b/server/certificates/mock_test.go index 5d0dde0..644e8a9 100644 --- a/server/certificates/mock_test.go +++ b/server/certificates/mock_test.go @@ -4,13 +4,15 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "codeberg.org/codeberg/pages/server/database" ) func TestMockCert(t *testing.T) { - db, err := database.NewTmpDB() - assert.NoError(t, err) + db := database.NewMockCertDB(t) + db.Mock.On("Put", mock.Anything, mock.Anything).Return(nil) + cert, err := mockCert("example.com", "some error msg", "codeberg.page", db) assert.NoError(t, err) if assert.NotEmpty(t, cert) { diff --git a/server/database/interface.go b/server/database/interface.go index 92068c5..7fdbae7 100644 --- a/server/database/interface.go +++ b/server/database/interface.go @@ -8,6 +8,9 @@ import ( "github.com/rs/zerolog/log" ) +//go:generate go install github.com/vektra/mockery/v2@latest +//go:generate mockery --name CertDB --output . --filename mock.go --inpackage --case underscore + type CertDB interface { Close() error Put(name string, cert *certificate.Resource) error diff --git a/server/database/mock.go b/server/database/mock.go index 6148287..e7e2c38 100644 --- a/server/database/mock.go +++ b/server/database/mock.go @@ -1,49 +1,122 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + package database import ( - "fmt" - "time" - - "github.com/OrlovEvgeny/go-mcache" - "github.com/go-acme/lego/v4/certificate" + certificate "github.com/go-acme/lego/v4/certificate" + mock "github.com/stretchr/testify/mock" ) -var _ CertDB = tmpDB{} - -type tmpDB struct { - intern *mcache.CacheDriver - ttl time.Duration +// MockCertDB is an autogenerated mock type for the CertDB type +type MockCertDB struct { + mock.Mock } -func (p tmpDB) Close() error { - _ = p.intern.Close() - return nil -} +// Close provides a mock function with given fields: +func (_m *MockCertDB) Close() error { + ret := _m.Called() -func (p tmpDB) Put(name string, cert *certificate.Resource) error { - return p.intern.Set(name, cert, p.ttl) -} - -func (p tmpDB) Get(name string) (*certificate.Resource, error) { - cert, has := p.intern.Get(name) - if !has { - return nil, fmt.Errorf("cert for %q not found", name) + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) } - return cert.(*certificate.Resource), nil + + return r0 } -func (p tmpDB) Delete(key string) error { - p.intern.Remove(key) - return nil +// Delete provides a mock function with given fields: key +func (_m *MockCertDB) Delete(key string) error { + ret := _m.Called(key) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(key) + } else { + r0 = ret.Error(0) + } + + return r0 } -func (p tmpDB) Items(page, pageSize int) ([]*Cert, error) { - return nil, fmt.Errorf("items not implemented for tmpDB") +// Get provides a mock function with given fields: name +func (_m *MockCertDB) Get(name string) (*certificate.Resource, error) { + ret := _m.Called(name) + + var r0 *certificate.Resource + var r1 error + if rf, ok := ret.Get(0).(func(string) (*certificate.Resource, error)); ok { + return rf(name) + } + if rf, ok := ret.Get(0).(func(string) *certificate.Resource); ok { + r0 = rf(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*certificate.Resource) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func NewTmpDB() (CertDB, error) { - return &tmpDB{ - intern: mcache.New(), - ttl: time.Minute, - }, nil +// Items provides a mock function with given fields: page, pageSize +func (_m *MockCertDB) Items(page int, pageSize int) ([]*Cert, error) { + ret := _m.Called(page, pageSize) + + var r0 []*Cert + var r1 error + if rf, ok := ret.Get(0).(func(int, int) ([]*Cert, error)); ok { + return rf(page, pageSize) + } + if rf, ok := ret.Get(0).(func(int, int) []*Cert); ok { + r0 = rf(page, pageSize) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*Cert) + } + } + + if rf, ok := ret.Get(1).(func(int, int) error); ok { + r1 = rf(page, pageSize) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Put provides a mock function with given fields: name, cert +func (_m *MockCertDB) Put(name string, cert *certificate.Resource) error { + ret := _m.Called(name, cert) + + var r0 error + if rf, ok := ret.Get(0).(func(string, *certificate.Resource) error); ok { + r0 = rf(name, cert) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewMockCertDB interface { + mock.TestingT + Cleanup(func()) +} + +// NewMockCertDB creates a new instance of MockCertDB. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewMockCertDB(t mockConstructorTestingTNewMockCertDB) *MockCertDB { + mock := &MockCertDB{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock } From 0adac9a5b15746a139e76e1de3996d4a20e791fa Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Tue, 14 Feb 2023 02:23:28 +0000 Subject: [PATCH 4/5] fix http -> https redirect and add integration tests for it (#184) and more logging Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/184 --- Justfile | 2 ++ cmd/main.go | 9 ++++--- integration/get_test.go | 12 +++++++++ integration/main_test.go | 2 ++ server/certificates/cached_challengers.go | 33 +++++++++++++++++++---- server/certificates/certificates.go | 1 + 6 files changed, 50 insertions(+), 9 deletions(-) diff --git a/Justfile b/Justfile index 9ee7eb3..0b8f814 100644 --- a/Justfile +++ b/Justfile @@ -9,6 +9,8 @@ dev: export PAGES_DOMAIN=localhost.mock.directory export RAW_DOMAIN=raw.localhost.mock.directory export PORT=4430 + export HTTP_PORT=8880 + export ENABLE_HTTP_SERVER=true export LOG_LEVEL=trace go run -tags '{{TAGS}}' . diff --git a/cmd/main.go b/cmd/main.go index 8a65d43..aa00f54 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -48,7 +48,8 @@ func Serve(ctx *cli.Context) error { mainDomainSuffix := ctx.String("pages-domain") rawInfoPage := ctx.String("raw-info-page") listeningHost := ctx.String("host") - listeningSSLAddress := fmt.Sprintf("%s:%d", listeningHost, ctx.Uint("port")) + listeningSSLPort := ctx.Uint("port") + listeningSSLAddress := fmt.Sprintf("%s:%d", listeningHost, listeningSSLPort) listeningHTTPAddress := fmt.Sprintf("%s:%d", listeningHost, ctx.Uint("http-port")) enableHTTPServer := ctx.Bool("enable-http-server") @@ -93,7 +94,7 @@ func Serve(ctx *cli.Context) error { } // Create listener for SSL connections - log.Info().Msgf("Listening on https://%s", listeningSSLAddress) + log.Info().Msgf("Create TCP listener for SSL on %s", listeningSSLAddress) listener, err := net.Listen("tcp", listeningSSLAddress) if err != nil { return fmt.Errorf("couldn't create listener: %v", err) @@ -113,7 +114,7 @@ func Serve(ctx *cli.Context) error { if enableHTTPServer { // Create handler for http->https redirect and http acme challenges - httpHandler := certificates.SetupHTTPACMEChallengeServer(challengeCache) + httpHandler := certificates.SetupHTTPACMEChallengeServer(challengeCache, listeningSSLPort) // Create listener for http and start listening go func() { @@ -133,7 +134,7 @@ func Serve(ctx *cli.Context) error { dnsLookupCache, canonicalDomainCache) // Start the ssl listener - log.Info().Msgf("Start listening on %s", listener.Addr()) + log.Info().Msgf("Start SSL server using TCP listener on %s", listener.Addr()) if err := http.Serve(listener, sslHandler); err != nil { log.Panic().Err(err).Msg("Couldn't start fastServer") } diff --git a/integration/get_test.go b/integration/get_test.go index b70eeed..3a7190a 100644 --- a/integration/get_test.go +++ b/integration/get_test.go @@ -193,6 +193,18 @@ func TestGetOptions(t *testing.T) { assert.EqualValues(t, "GET, HEAD, OPTIONS", resp.Header.Get("Allow")) } +func TestHttpRedirect(t *testing.T) { + log.Println("=== TestHttpRedirect ===") + resp, err := getTestHTTPSClient().Get("http://mock-pages.codeberg-test.org:8880/README.md") + assert.NoError(t, err) + if !assert.NotNil(t, resp) { + t.FailNow() + } + assert.EqualValues(t, http.StatusMovedPermanently, resp.StatusCode) + assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type")) + assert.EqualValues(t, "https://mock-pages.codeberg-test.org:4430/README.md", resp.Header.Get("Location")) +} + func getTestHTTPSClient() *http.Client { cookieJar, _ := cookiejar.New(nil) return &http.Client{ diff --git a/integration/main_test.go b/integration/main_test.go index 3e0e187..a6579fd 100644 --- a/integration/main_test.go +++ b/integration/main_test.go @@ -40,6 +40,8 @@ func startServer(ctx context.Context) error { setEnvIfNotSet("PAGES_DOMAIN", "localhost.mock.directory") setEnvIfNotSet("RAW_DOMAIN", "raw.localhost.mock.directory") setEnvIfNotSet("PORT", "4430") + setEnvIfNotSet("HTTP_PORT", "8880") + setEnvIfNotSet("ENABLE_HTTP_SERVER", "true") setEnvIfNotSet("DB_TYPE", "sqlite3") app := cli.NewApp() diff --git a/server/certificates/cached_challengers.go b/server/certificates/cached_challengers.go index 02474b3..bc9ea67 100644 --- a/server/certificates/cached_challengers.go +++ b/server/certificates/cached_challengers.go @@ -1,15 +1,17 @@ package certificates import ( + "fmt" "net/http" + "net/url" "strings" "time" "github.com/go-acme/lego/v4/challenge" + "github.com/rs/zerolog/log" "codeberg.org/codeberg/pages/server/cache" "codeberg.org/codeberg/pages/server/context" - "codeberg.org/codeberg/pages/server/utils" ) type AcmeTLSChallengeProvider struct { @@ -44,17 +46,38 @@ func (a AcmeHTTPChallengeProvider) CleanUp(domain, token, _ string) error { return nil } -func SetupHTTPACMEChallengeServer(challengeCache cache.SetGetKey) http.HandlerFunc { +func SetupHTTPACMEChallengeServer(challengeCache cache.SetGetKey, sslPort uint) http.HandlerFunc { + // handle custom-ssl-ports to be added on https redirects + portPart := "" + if sslPort != 443 { + portPart = fmt.Sprintf(":%d", sslPort) + } + return func(w http.ResponseWriter, req *http.Request) { ctx := context.New(w, req) + domain := ctx.TrimHostPort() + + // it's an acme request if strings.HasPrefix(ctx.Path(), challengePath) { - challenge, ok := challengeCache.Get(utils.TrimHostPort(ctx.Host()) + "/" + strings.TrimPrefix(ctx.Path(), challengePath)) + challenge, ok := challengeCache.Get(domain + "/" + strings.TrimPrefix(ctx.Path(), challengePath)) if !ok || challenge == nil { + log.Info().Msgf("HTTP-ACME challenge for '%s' failed: token not found", domain) ctx.String("no challenge for this token", http.StatusNotFound) } + log.Info().Msgf("HTTP-ACME challenge for '%s' succeeded", domain) ctx.String(challenge.(string)) - } else { - ctx.Redirect("https://"+ctx.Host()+ctx.Path(), http.StatusMovedPermanently) + return } + + // it's a normal http request that needs to be redirected + u, err := url.Parse(fmt.Sprintf("https://%s%s%s", domain, portPart, ctx.Path())) + if err != nil { + log.Error().Err(err).Msg("could not craft http to https redirect") + ctx.String("", http.StatusInternalServerError) + } + + newURL := u.String() + log.Debug().Msgf("redirect http to https: %s", newURL) + ctx.Redirect(newURL, http.StatusMovedPermanently) } } diff --git a/server/certificates/certificates.go b/server/certificates/certificates.go index 707672c..ce1420d 100644 --- a/server/certificates/certificates.go +++ b/server/certificates/certificates.go @@ -47,6 +47,7 @@ func TLSConfig(mainDomainSuffix string, if proto != tlsalpn01.ACMETLS1Protocol { continue } + log.Info().Msgf("Detect ACME-TLS1 challenge for '%s'", domain) challenge, ok := challengeCache.Get(domain) if !ok { From 42d5802b9ba77556dfa529e20caf0442ac6b1fa4 Mon Sep 17 00:00:00 2001 From: deblan Date: Tue, 14 Feb 2023 03:03:00 +0000 Subject: [PATCH 5/5] Allow to define default branches (#125) This try to address #115 Co-authored-by: Simon Vieille Co-authored-by: 6543 <6543@obermui.de> Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/125 Reviewed-by: 6543 <6543@obermui.de> Co-authored-by: deblan Co-committed-by: deblan --- cmd/flags.go | 7 ++++ cmd/main.go | 7 ++++ go.mod | 3 +- go.sum | 4 +++ integration/main_test.go | 1 + server/certificates/certificates.go | 3 +- server/dns/dns.go | 10 +++--- server/handler/handler.go | 4 ++- server/handler/handler_custom_domain.go | 5 +-- server/handler/handler_sub_domain.go | 46 ++++++++++++++++++++----- server/handler/handler_test.go | 1 + 11 files changed, 73 insertions(+), 18 deletions(-) diff --git a/cmd/flags.go b/cmd/flags.go index 5bc638b..a71dd35 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -112,6 +112,13 @@ var ( Usage: "specify at which log level should be logged. Possible options: info, warn, error, fatal", EnvVars: []string{"LOG_LEVEL"}, }, + // Default branches to fetch assets from + &cli.StringSliceFlag{ + Name: "pages-branch", + Usage: "define a branch to fetch assets from", + EnvVars: []string{"PAGES_BRANCHES"}, + Value: cli.NewStringSlice("pages"), + }, // ############################ // ### ACME Client Settings ### diff --git a/cmd/main.go b/cmd/main.go index aa00f54..45e151d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -45,6 +45,7 @@ func Serve(ctx *cli.Context) error { giteaRoot := ctx.String("gitea-root") giteaAPIToken := ctx.String("gitea-api-token") rawDomain := ctx.String("raw-domain") + defaultBranches := ctx.StringSlice("pages-branch") mainDomainSuffix := ctx.String("pages-domain") rawInfoPage := ctx.String("raw-info-page") listeningHost := ctx.String("host") @@ -63,6 +64,10 @@ func Serve(ctx *cli.Context) error { mainDomainSuffix = "." + mainDomainSuffix } + if len(defaultBranches) == 0 { + return fmt.Errorf("no default branches set (PAGES_BRANCHES)") + } + // Init ssl cert database certDB, closeFn, err := openCertDB(ctx) if err != nil { @@ -104,6 +109,7 @@ func Serve(ctx *cli.Context) error { listener = tls.NewListener(listener, certificates.TLSConfig(mainDomainSuffix, giteaClient, acmeClient, + defaultBranches[0], keyCache, challengeCache, dnsLookupCache, canonicalDomainCache, certDB)) @@ -131,6 +137,7 @@ func Serve(ctx *cli.Context) error { giteaClient, rawInfoPage, BlacklistedPaths, allowedCorsDomains, + defaultBranches, dnsLookupCache, canonicalDomainCache) // Start the ssl listener diff --git a/go.mod b/go.mod index 944e2ad..bfde7f7 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/rs/zerolog v1.27.0 github.com/stretchr/testify v1.7.0 github.com/urfave/cli/v2 v2.3.0 + golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb xorm.io/xorm v1.3.2 ) @@ -117,7 +118,7 @@ require ( golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect - golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect + golang.org/x/sys v0.1.0 // indirect golang.org/x/text v0.3.6 // indirect golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 // indirect google.golang.org/api v0.20.0 // indirect diff --git a/go.sum b/go.sum index b5a7568..b10305c 100644 --- a/go.sum +++ b/go.sum @@ -768,6 +768,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w= +golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -899,6 +901,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/integration/main_test.go b/integration/main_test.go index a6579fd..a397110 100644 --- a/integration/main_test.go +++ b/integration/main_test.go @@ -39,6 +39,7 @@ func startServer(ctx context.Context) error { setEnvIfNotSet("ACME_API", "https://acme.mock.directory") setEnvIfNotSet("PAGES_DOMAIN", "localhost.mock.directory") setEnvIfNotSet("RAW_DOMAIN", "raw.localhost.mock.directory") + setEnvIfNotSet("PAGES_BRANCHES", "pages,main,master") setEnvIfNotSet("PORT", "4430") setEnvIfNotSet("HTTP_PORT", "8880") setEnvIfNotSet("ENABLE_HTTP_SERVER", "true") diff --git a/server/certificates/certificates.go b/server/certificates/certificates.go index ce1420d..3ae891a 100644 --- a/server/certificates/certificates.go +++ b/server/certificates/certificates.go @@ -30,6 +30,7 @@ var ErrUserRateLimitExceeded = errors.New("rate limit exceeded: 10 certificates func TLSConfig(mainDomainSuffix string, giteaClient *gitea.Client, acmeClient *AcmeClient, + firstDefaultBranch string, keyCache, challengeCache, dnsLookupCache, canonicalDomainCache cache.SetGetKey, certDB database.CertDB, ) *tls.Config { @@ -68,7 +69,7 @@ func TLSConfig(mainDomainSuffix string, domain = mainDomainSuffix } else { var targetRepo, targetBranch string - targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(domain, mainDomainSuffix, dnsLookupCache) + targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch, dnsLookupCache) if targetOwner == "" { // DNS not set up, return main certificate to redirect to the docs domain = mainDomainSuffix diff --git a/server/dns/dns.go b/server/dns/dns.go index 2719d4d..c11b278 100644 --- a/server/dns/dns.go +++ b/server/dns/dns.go @@ -11,9 +11,11 @@ import ( // lookupCacheTimeout specifies the timeout for the DNS lookup cache. var lookupCacheTimeout = 15 * time.Minute +var defaultPagesRepo = "pages" + // GetTargetFromDNS searches for CNAME or TXT entries on the request domain ending with MainDomainSuffix. // If everything is fine, it returns the target data. -func GetTargetFromDNS(domain, mainDomainSuffix string, dnsLookupCache cache.SetGetKey) (targetOwner, targetRepo, targetBranch string) { +func GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch string, dnsLookupCache cache.SetGetKey) (targetOwner, targetRepo, targetBranch string) { // Get CNAME or TXT var cname string var err error @@ -50,10 +52,10 @@ func GetTargetFromDNS(domain, mainDomainSuffix string, dnsLookupCache cache.SetG targetBranch = cnameParts[len(cnameParts)-3] } if targetRepo == "" { - targetRepo = "pages" + targetRepo = defaultPagesRepo } - if targetBranch == "" && targetRepo != "pages" { - targetBranch = "pages" + if targetBranch == "" && targetRepo != defaultPagesRepo { + targetBranch = firstDefaultBranch } // if targetBranch is still empty, the caller must find the default branch return diff --git a/server/handler/handler.go b/server/handler/handler.go index 78301e9..a944c7e 100644 --- a/server/handler/handler.go +++ b/server/handler/handler.go @@ -17,7 +17,6 @@ const ( headerAccessControlAllowOrigin = "Access-Control-Allow-Origin" headerAccessControlAllowMethods = "Access-Control-Allow-Methods" defaultPagesRepo = "pages" - defaultPagesBranch = "pages" ) // Handler handles a single HTTP request to the web server. @@ -25,6 +24,7 @@ func Handler(mainDomainSuffix, rawDomain string, giteaClient *gitea.Client, rawInfoPage string, blacklistedPaths, allowedCorsDomains []string, + defaultPagesBranches []string, dnsLookupCache, canonicalDomainCache cache.SetGetKey, ) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { @@ -98,6 +98,7 @@ func Handler(mainDomainSuffix, rawDomain string, log.Debug().Msg("subdomain request detecded") handleSubDomain(log, ctx, giteaClient, mainDomainSuffix, + defaultPagesBranches, trimmedHost, pathElements, canonicalDomainCache) @@ -107,6 +108,7 @@ func Handler(mainDomainSuffix, rawDomain string, mainDomainSuffix, trimmedHost, pathElements, + defaultPagesBranches[0], dnsLookupCache, canonicalDomainCache) } } diff --git a/server/handler/handler_custom_domain.go b/server/handler/handler_custom_domain.go index a541b74..1b85f62 100644 --- a/server/handler/handler_custom_domain.go +++ b/server/handler/handler_custom_domain.go @@ -18,10 +18,11 @@ func handleCustomDomain(log zerolog.Logger, ctx *context.Context, giteaClient *g mainDomainSuffix string, trimmedHost string, pathElements []string, + firstDefaultBranch string, dnsLookupCache, canonicalDomainCache cache.SetGetKey, ) { // Serve pages from custom domains - targetOwner, targetRepo, targetBranch := dns.GetTargetFromDNS(trimmedHost, mainDomainSuffix, dnsLookupCache) + targetOwner, targetRepo, targetBranch := dns.GetTargetFromDNS(trimmedHost, mainDomainSuffix, firstDefaultBranch, dnsLookupCache) if targetOwner == "" { html.ReturnErrorPage(ctx, "could not obtain repo owner from custom domain", @@ -52,7 +53,7 @@ func handleCustomDomain(log zerolog.Logger, ctx *context.Context, giteaClient *g return } else if canonicalDomain != trimmedHost { // only redirect if the target is also a codeberg page! - targetOwner, _, _ = dns.GetTargetFromDNS(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix, dnsLookupCache) + targetOwner, _, _ = dns.GetTargetFromDNS(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix, firstDefaultBranch, dnsLookupCache) if targetOwner != "" { 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 2a75e9f..68f4822 100644 --- a/server/handler/handler_sub_domain.go +++ b/server/handler/handler_sub_domain.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/rs/zerolog" + "golang.org/x/exp/slices" "codeberg.org/codeberg/pages/html" "codeberg.org/codeberg/pages/server/cache" @@ -17,6 +18,7 @@ import ( func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gitea.Client, mainDomainSuffix string, + defaultPagesBranches []string, trimmedHost string, pathElements []string, canonicalDomainCache cache.SetGetKey, @@ -63,12 +65,21 @@ func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gite // Check if the first directory is a branch for the defaultPagesRepo // example.codeberg.page/@main/index.html if strings.HasPrefix(pathElements[0], "@") { + targetBranch := pathElements[0][1:] + + // if the default pages branch can be determined exactly, it does not need to be set + if len(defaultPagesBranches) == 1 && slices.Contains(defaultPagesBranches, targetBranch) { + // example.codeberg.org/@pages/... redirects to example.codeberg.org/... + ctx.Redirect("/"+strings.Join(pathElements[1:], "/"), http.StatusTemporaryRedirect) + return + } + log.Debug().Msg("main domain preparations, now trying with specified branch") if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{ TryIndexPages: true, TargetOwner: targetOwner, TargetRepo: defaultPagesRepo, - TargetBranch: pathElements[0][1:], + TargetBranch: targetBranch, TargetPath: path.Join(pathElements[1:]...), }, true); works { log.Trace().Msg("tryUpstream: serve default pages repo with specified branch") @@ -81,19 +92,36 @@ func handleSubDomain(log zerolog.Logger, ctx *context.Context, giteaClient *gite return } - // Check if the first directory is a repo with a defaultPagesRepo branch - // example.codeberg.page/myrepo/index.html - // example.codeberg.page/pages/... is not allowed here. - log.Debug().Msg("main domain preparations, now trying with specified repo") - if pathElements[0] != defaultPagesRepo { + for _, defaultPagesBranch := range defaultPagesBranches { + // Check if the first directory is a repo with a default pages branch + // example.codeberg.page/myrepo/index.html + // example.codeberg.page/{PAGES_BRANCHE}/... is not allowed here. + log.Debug().Msg("main domain preparations, now trying with specified repo") + if pathElements[0] != defaultPagesBranch { + if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{ + TryIndexPages: true, + TargetOwner: targetOwner, + TargetRepo: pathElements[0], + TargetBranch: defaultPagesBranch, + TargetPath: path.Join(pathElements[1:]...), + }, false); works { + log.Debug().Msg("tryBranch, now trying upstream 5") + tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache) + return + } + } + + // Try to use the defaultPagesRepo on an default pages branch + // example.codeberg.page/index.html + log.Debug().Msg("main domain preparations, now trying with default repo") if targetOpt, works := tryBranch(log, ctx, giteaClient, &upstream.Options{ TryIndexPages: true, TargetOwner: targetOwner, - TargetRepo: pathElements[0], + TargetRepo: defaultPagesRepo, TargetBranch: defaultPagesBranch, - TargetPath: path.Join(pathElements[1:]...), + TargetPath: path.Join(pathElements...), }, false); works { - log.Debug().Msg("tryBranch, now trying upstream 5") + log.Debug().Msg("tryBranch, now trying upstream 6") tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, targetOpt, canonicalDomainCache) return } diff --git a/server/handler/handler_test.go b/server/handler/handler_test.go index 626564a..ed063b2 100644 --- a/server/handler/handler_test.go +++ b/server/handler/handler_test.go @@ -18,6 +18,7 @@ func TestHandlerPerformance(t *testing.T) { "https://docs.codeberg.org/pages/raw-content/", []string{"/.well-known/acme-challenge/"}, []string{"raw.codeberg.org", "fonts.codeberg.org", "design.codeberg.org"}, + []string{"pages"}, cache.NewKeyValueCache(), cache.NewKeyValueCache(), )