switch to std http implementation instead of fasthttp (#106)

close #100
close #109
close #113
close #28
close #63

Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/106
This commit is contained in:
6543 2022-11-12 20:37:20 +01:00
parent 69eabb248a
commit b9966487f6
28 changed files with 827 additions and 584 deletions

View file

@ -1,12 +1,12 @@
package cmd package cmd
import ( import (
"bytes"
"context" "context"
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"net" "net"
"net/http"
"os" "os"
"strings" "strings"
"time" "time"
@ -24,15 +24,15 @@ import (
// AllowedCorsDomains lists the domains for which Cross-Origin Resource Sharing is allowed. // AllowedCorsDomains lists the domains for which Cross-Origin Resource Sharing is allowed.
// TODO: make it a flag // TODO: make it a flag
var AllowedCorsDomains = [][]byte{ var AllowedCorsDomains = []string{
[]byte("fonts.codeberg.org"), "fonts.codeberg.org",
[]byte("design.codeberg.org"), "design.codeberg.org",
} }
// BlacklistedPaths specifies forbidden path prefixes for all Codeberg Pages. // BlacklistedPaths specifies forbidden path prefixes for all Codeberg Pages.
// TODO: Make it a flag too // TODO: Make it a flag too
var BlacklistedPaths = [][]byte{ var BlacklistedPaths = []string{
[]byte("/.well-known/acme-challenge/"), "/.well-known/acme-challenge/",
} }
// Serve sets up and starts the web server. // Serve sets up and starts the web server.
@ -47,7 +47,7 @@ func Serve(ctx *cli.Context) error {
giteaRoot := strings.TrimSuffix(ctx.String("gitea-root"), "/") giteaRoot := strings.TrimSuffix(ctx.String("gitea-root"), "/")
giteaAPIToken := ctx.String("gitea-api-token") giteaAPIToken := ctx.String("gitea-api-token")
rawDomain := ctx.String("raw-domain") rawDomain := ctx.String("raw-domain")
mainDomainSuffix := []byte(ctx.String("pages-domain")) mainDomainSuffix := ctx.String("pages-domain")
rawInfoPage := ctx.String("raw-info-page") rawInfoPage := ctx.String("raw-info-page")
listeningAddress := fmt.Sprintf("%s:%s", ctx.String("host"), ctx.String("port")) listeningAddress := fmt.Sprintf("%s:%s", ctx.String("host"), ctx.String("port"))
enableHTTPServer := ctx.Bool("enable-http-server") enableHTTPServer := ctx.Bool("enable-http-server")
@ -65,12 +65,12 @@ func Serve(ctx *cli.Context) error {
allowedCorsDomains := AllowedCorsDomains allowedCorsDomains := AllowedCorsDomains
if len(rawDomain) != 0 { if len(rawDomain) != 0 {
allowedCorsDomains = append(allowedCorsDomains, []byte(rawDomain)) allowedCorsDomains = append(allowedCorsDomains, rawDomain)
} }
// Make sure MainDomain has a trailing dot, and GiteaRoot has no trailing slash // Make sure MainDomain has a trailing dot, and GiteaRoot has no trailing slash
if !bytes.HasPrefix(mainDomainSuffix, []byte{'.'}) { if !strings.HasPrefix(mainDomainSuffix, ".") {
mainDomainSuffix = append([]byte{'.'}, mainDomainSuffix...) mainDomainSuffix = "." + mainDomainSuffix
} }
keyCache := cache.NewKeyValueCache() keyCache := cache.NewKeyValueCache()
@ -79,26 +79,22 @@ func Serve(ctx *cli.Context) error {
canonicalDomainCache := cache.NewKeyValueCache() canonicalDomainCache := cache.NewKeyValueCache()
// dnsLookupCache stores DNS lookups for custom domains // dnsLookupCache stores DNS lookups for custom domains
dnsLookupCache := cache.NewKeyValueCache() dnsLookupCache := cache.NewKeyValueCache()
// branchTimestampCache stores branch timestamps for faster cache checking // clientResponseCache stores responses from the Gitea server
branchTimestampCache := cache.NewKeyValueCache() clientResponseCache := cache.NewKeyValueCache()
// fileResponseCache stores responses from the Gitea server
// TODO: make this an MRU cache with a size limit
fileResponseCache := cache.NewKeyValueCache()
giteaClient, err := gitea.NewClient(giteaRoot, giteaAPIToken, ctx.Bool("enable-symlink-support"), ctx.Bool("enable-lfs-support")) giteaClient, err := gitea.NewClient(giteaRoot, giteaAPIToken, clientResponseCache, ctx.Bool("enable-symlink-support"), ctx.Bool("enable-lfs-support"))
if err != nil { if err != nil {
return fmt.Errorf("could not create new gitea client: %v", err) return fmt.Errorf("could not create new gitea client: %v", err)
} }
// Create handler based on settings // Create handler based on settings
handler := server.Handler(mainDomainSuffix, []byte(rawDomain), httpsHandler := server.Handler(mainDomainSuffix, rawDomain,
giteaClient, giteaClient,
giteaRoot, rawInfoPage, giteaRoot, rawInfoPage,
BlacklistedPaths, allowedCorsDomains, BlacklistedPaths, allowedCorsDomains,
dnsLookupCache, canonicalDomainCache, branchTimestampCache, fileResponseCache) dnsLookupCache, canonicalDomainCache)
fastServer := server.SetupServer(handler) httpHandler := server.SetupHTTPACMEChallengeServer(challengeCache)
httpServer := server.SetupHTTPACMEChallengeServer(challengeCache)
// Setup listener and TLS // Setup listener and TLS
log.Info().Msgf("Listening on https://%s", listeningAddress) log.Info().Msgf("Listening on https://%s", listeningAddress)
@ -138,7 +134,7 @@ func Serve(ctx *cli.Context) error {
if enableHTTPServer { if enableHTTPServer {
go func() { go func() {
log.Info().Msg("Start HTTP server listening on :80") log.Info().Msg("Start HTTP server listening on :80")
err := httpServer.ListenAndServe("[::]:80") err := http.ListenAndServe("[::]:80", httpHandler)
if err != nil { if err != nil {
log.Panic().Err(err).Msg("Couldn't start HTTP fastServer") log.Panic().Err(err).Msg("Couldn't start HTTP fastServer")
} }
@ -147,8 +143,7 @@ func Serve(ctx *cli.Context) error {
// Start the web fastServer // Start the web fastServer
log.Info().Msgf("Start listening on %s", listener.Addr()) log.Info().Msgf("Start listening on %s", listener.Addr())
err = fastServer.Serve(listener) if err := http.Serve(listener, httpsHandler); err != nil {
if err != nil {
log.Panic().Err(err).Msg("Couldn't start fastServer") log.Panic().Err(err).Msg("Couldn't start fastServer")
} }

17
go.mod
View file

@ -1,8 +1,9 @@
module codeberg.org/codeberg/pages module codeberg.org/codeberg/pages
go 1.18 go 1.19
require ( require (
code.gitea.io/sdk/gitea v0.15.1-0.20220729105105-cc14c63cccfa
github.com/OrlovEvgeny/go-mcache v0.0.0-20200121124330-1a8195b34f3a github.com/OrlovEvgeny/go-mcache v0.0.0-20200121124330-1a8195b34f3a
github.com/akrylysov/pogreb v0.10.1 github.com/akrylysov/pogreb v0.10.1
github.com/go-acme/lego/v4 v4.5.3 github.com/go-acme/lego/v4 v4.5.3
@ -11,8 +12,6 @@ require (
github.com/rs/zerolog v1.27.0 github.com/rs/zerolog v1.27.0
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
github.com/urfave/cli/v2 v2.3.0 github.com/urfave/cli/v2 v2.3.0
github.com/valyala/fasthttp v1.31.0
github.com/valyala/fastjson v1.6.3
) )
require ( require (
@ -31,7 +30,6 @@ require (
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.1 // indirect github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.1 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1183 // indirect github.com/aliyun/alibaba-cloud-sdk-go v1.61.1183 // indirect
github.com/andybalholm/brotli v1.0.2 // indirect
github.com/aws/aws-sdk-go v1.39.0 // indirect github.com/aws/aws-sdk-go v1.39.0 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cenkalti/backoff/v4 v4.1.1 // indirect github.com/cenkalti/backoff/v4 v4.1.1 // indirect
@ -39,6 +37,7 @@ require (
github.com/cpu/goacmedns v0.1.1 // indirect github.com/cpu/goacmedns v0.1.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/deepmap/oapi-codegen v1.6.1 // indirect github.com/deepmap/oapi-codegen v1.6.1 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/dnsimple/dnsimple-go v0.70.1 // indirect github.com/dnsimple/dnsimple-go v0.70.1 // indirect
@ -46,6 +45,7 @@ require (
github.com/fatih/structs v1.1.0 // indirect github.com/fatih/structs v1.1.0 // indirect
github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect
github.com/go-errors/errors v1.0.1 // indirect github.com/go-errors/errors v1.0.1 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 // indirect github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 // indirect
github.com/gofrs/uuid v3.2.0+incompatible // indirect github.com/gofrs/uuid v3.2.0+incompatible // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
@ -57,13 +57,13 @@ require (
github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae // indirect github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae // indirect
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
github.com/jarcoal/httpmock v1.0.6 // indirect github.com/jarcoal/httpmock v1.0.6 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.7 // indirect github.com/json-iterator/go v1.1.7 // indirect
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
github.com/klauspost/compress v1.13.4 // indirect
github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b // indirect github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
@ -104,15 +104,14 @@ require (
github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cast v1.3.1 // indirect
github.com/stretchr/objx v0.3.0 // indirect github.com/stretchr/objx v0.3.0 // indirect
github.com/transip/gotransip/v6 v6.6.1 // indirect github.com/transip/gotransip/v6 v6.6.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/vinyldns/go-vinyldns v0.0.0-20200917153823-148a5f6b8f14 // indirect github.com/vinyldns/go-vinyldns v0.0.0-20200917153823-148a5f6b8f14 // indirect
github.com/vultr/govultr/v2 v2.7.1 // indirect github.com/vultr/govultr/v2 v2.7.1 // indirect
go.opencensus.io v0.22.3 // indirect go.opencensus.io v0.22.3 // indirect
go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277 // indirect go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277 // indirect
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
golang.org/x/text v0.3.6 // indirect golang.org/x/text v0.3.6 // indirect
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 // indirect golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 // indirect
google.golang.org/api v0.20.0 // indirect google.golang.org/api v0.20.0 // indirect

33
go.sum
View file

@ -22,6 +22,8 @@ cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIA
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
code.gitea.io/sdk/gitea v0.15.1-0.20220729105105-cc14c63cccfa h1:OVwgYrY6vr6gWZvgnmevFhtL0GVA4HKaFOhD+joPoNk=
code.gitea.io/sdk/gitea v0.15.1-0.20220729105105-cc14c63cccfa/go.mod h1:aRmrQC3CAHdJAU1LQt0C9zqzqI8tUB/5oQtNE746aYE=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/azure-sdk-for-go v32.4.0+incompatible h1:1JP8SKfroEakYiQU2ZyPDosh8w2Tg9UopKt88VyQPt4= github.com/Azure/azure-sdk-for-go v32.4.0+incompatible h1:1JP8SKfroEakYiQU2ZyPDosh8w2Tg9UopKt88VyQPt4=
github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
@ -66,8 +68,6 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1183 h1:dkj8/dxOQ4L1XpwCzRLqukvUBbxuNdz3FeyvHFnRjmo= github.com/aliyun/alibaba-cloud-sdk-go v1.61.1183 h1:dkj8/dxOQ4L1XpwCzRLqukvUBbxuNdz3FeyvHFnRjmo=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1183/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA= github.com/aliyun/alibaba-cloud-sdk-go v1.61.1183/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA=
github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E=
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@ -106,6 +106,8 @@ github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
github.com/deepmap/oapi-codegen v1.6.1 h1:2BvsmRb6pogGNtr8Ann+esAbSKFXx2CZN18VpAMecnw= github.com/deepmap/oapi-codegen v1.6.1 h1:2BvsmRb6pogGNtr8Ann+esAbSKFXx2CZN18VpAMecnw=
github.com/deepmap/oapi-codegen v1.6.1/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= github.com/deepmap/oapi-codegen v1.6.1/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
@ -136,6 +138,8 @@ github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJ
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -182,7 +186,6 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@ -243,6 +246,9 @@ github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdv
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@ -282,8 +288,6 @@ github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcM
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.4 h1:0zhec2I8zGnjWcKyLl6i3gPqKANCCn5e9xmviEEeX6s=
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b h1:DzHy0GlWeF0KAglaTMY7Q+khIFoG8toHP+wLFBVBQJc= github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b h1:DzHy0GlWeF0KAglaTMY7Q+khIFoG8toHP+wLFBVBQJc=
github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ= github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -494,15 +498,9 @@ github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.31.0 h1:lrauRLII19afgCs2fnWRJ4M5IkV0lo2FqA61uGkNBfE=
github.com/valyala/fasthttp v1.31.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc=
github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/vinyldns/go-vinyldns v0.0.0-20200917153823-148a5f6b8f14 h1:TFXGGMHmml4rs29PdPisC/aaCzOxUu1Vsh9on/IpUfE= github.com/vinyldns/go-vinyldns v0.0.0-20200917153823-148a5f6b8f14 h1:TFXGGMHmml4rs29PdPisC/aaCzOxUu1Vsh9on/IpUfE=
github.com/vinyldns/go-vinyldns v0.0.0-20200917153823-148a5f6b8f14/go.mod h1:RWc47jtnVuQv6+lY3c768WtXCas/Xi+U5UFc5xULmYg= github.com/vinyldns/go-vinyldns v0.0.0-20200917153823-148a5f6b8f14/go.mod h1:RWc47jtnVuQv6+lY3c768WtXCas/Xi+U5UFc5xULmYg=
github.com/vultr/govultr/v2 v2.7.1 h1:uF9ERet++Gb+7Cqs3p1P6b6yebeaZqVd7t5P2uZCaJU= github.com/vultr/govultr/v2 v2.7.1 h1:uF9ERet++Gb+7Cqs3p1P6b6yebeaZqVd7t5P2uZCaJU=
@ -539,8 +537,10 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -604,8 +604,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -667,12 +667,13 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/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/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 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= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View file

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<title>%status</title> <title>%status%</title>
<link rel="stylesheet" href="https://design.codeberg.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/inter/Inter%20Web/inter.css" rel="stylesheet" />
@ -23,11 +23,11 @@
<body> <body>
<i class="fa fa-search text-primary" style="font-size: 96px;"></i> <i class="fa fa-search text-primary" style="font-size: 96px;"></i>
<h1 class="mb-0 text-primary"> <h1 class="mb-0 text-primary">
Page not found! Page not found!
</h1> </h1>
<h5 class="text-center" style="max-width: 25em;"> <h5 class="text-center" style="max-width: 25em;">
Sorry, but this page couldn't be found or is inaccessible (%status).<br/> Sorry, but this page couldn't be found or is inaccessible (%status%).<br/>
We hope this isn't a problem on our end ;) - Make sure to check the <a href="https://docs.codeberg.org/codeberg-pages/troubleshooting/" target="_blank">troubleshooting section in the Docs</a>! We hope this isn't a problem on our end ;) - Make sure to check the <a href="https://docs.codeberg.org/codeberg-pages/troubleshooting/" target="_blank">troubleshooting section in the Docs</a>!
</h5> </h5>
<small class="text-muted"> <small class="text-muted">
<img src="https://design.codeberg.org/logo-kit/icon.svg" class="align-top"> <img src="https://design.codeberg.org/logo-kit/icon.svg" class="align-top">

View file

@ -1,24 +1,45 @@
package html package html
import ( import (
"bytes" "net/http"
"strconv" "strconv"
"strings"
"github.com/valyala/fasthttp" "codeberg.org/codeberg/pages/server/context"
) )
// ReturnErrorPage sets the response status code and writes NotFoundPage to the response body, with "%status" replaced // ReturnErrorPage sets the response status code and writes NotFoundPage to the response body,
// with the provided status code. // with "%status%" and %message% replaced with the provided statusCode and msg
func ReturnErrorPage(ctx *fasthttp.RequestCtx, code int) { func ReturnErrorPage(ctx *context.Context, msg string, statusCode int) {
ctx.Response.SetStatusCode(code) ctx.RespWriter.Header().Set("Content-Type", "text/html; charset=utf-8")
ctx.Response.Header.SetContentType("text/html; charset=utf-8") ctx.RespWriter.WriteHeader(statusCode)
message := fasthttp.StatusMessage(code)
if code == fasthttp.StatusMisdirectedRequest { if msg == "" {
message += " - domain not specified in <code>.domains</code> file" msg = errorBody(statusCode)
} else {
// TODO: use template engine
msg = strings.ReplaceAll(strings.ReplaceAll(ErrorPage, "%message%", msg), "%status%", http.StatusText(statusCode))
} }
if code == fasthttp.StatusFailedDependency {
_, _ = ctx.RespWriter.Write([]byte(msg))
}
func errorMessage(statusCode int) string {
message := http.StatusText(statusCode)
switch statusCode {
case http.StatusMisdirectedRequest:
message += " - domain not specified in <code>.domains</code> file"
case http.StatusFailedDependency:
message += " - target repo/branch doesn't exist or is private" message += " - target repo/branch doesn't exist or is private"
} }
// TODO: use template engine?
ctx.Response.SetBody(bytes.ReplaceAll(NotFoundPage, []byte("%status"), []byte(strconv.Itoa(code)+" "+message))) return message
}
// TODO: use template engine
func errorBody(statusCode int) string {
return strings.ReplaceAll(NotFoundPage,
"%status%",
strconv.Itoa(statusCode)+" "+errorMessage(statusCode))
} }

38
html/error.html Normal file
View file

@ -0,0 +1,38 @@
<!doctype html>
<html class="codeberg-design">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>%status%</title>
<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" />
<style>
body {
margin: 0; padding: 1rem; box-sizing: border-box;
width: 100%; min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>
</head>
<body>
<i class="fa fa-search text-primary" style="font-size: 96px;"></i>
<h1 class="mb-0 text-primary">
%status%!
</h1>
<h5 class="text-center" style="max-width: 25em;">
Sorry, but this page couldn't be served.<br/>
We got an <b>"%message%"</b><br/>
We hope this isn't a problem on our end ;) - Make sure to check the <a href="https://docs.codeberg.org/codeberg-pages/troubleshooting/" target="_blank">troubleshooting section in the Docs</a>!
</h5>
<small class="text-muted">
<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>
</html>

View file

@ -3,4 +3,7 @@ package html
import _ "embed" import _ "embed"
//go:embed 404.html //go:embed 404.html
var NotFoundPage []byte var NotFoundPage string
//go:embed error.html
var ErrorPage string

View file

@ -25,7 +25,7 @@ func TestGetRedirect(t *testing.T) {
t.FailNow() t.FailNow()
} }
assert.EqualValues(t, "https://www.cabr2.de/", resp.Header.Get("Location")) assert.EqualValues(t, "https://www.cabr2.de/", resp.Header.Get("Location"))
assert.EqualValues(t, 0, getSize(resp.Body)) assert.EqualValues(t, `<a href="https://www.cabr2.de/">Temporary Redirect</a>.`, strings.TrimSpace(string(getBytes(resp.Body))))
} }
func TestGetContent(t *testing.T) { func TestGetContent(t *testing.T) {
@ -44,12 +44,13 @@ func TestGetContent(t *testing.T) {
// specify branch // specify branch
resp, err = getTestHTTPSClient().Get("https://momar.localhost.mock.directory:4430/pag/@master/") resp, err = getTestHTTPSClient().Get("https://momar.localhost.mock.directory:4430/pag/@master/")
assert.NoError(t, err) assert.NoError(t, err)
if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) { if !assert.NotNil(t, resp) {
t.FailNow() t.FailNow()
} }
assert.EqualValues(t, http.StatusOK, resp.StatusCode)
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type")) assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
assert.True(t, getSize(resp.Body) > 1000) assert.True(t, getSize(resp.Body) > 1000)
assert.Len(t, resp.Header.Get("ETag"), 42) assert.Len(t, resp.Header.Get("ETag"), 44)
// access branch name contains '/' // 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://blumia.localhost.mock.directory:4430/pages-server-integration-tests/@docs~main/")
@ -59,7 +60,7 @@ func TestGetContent(t *testing.T) {
} }
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type")) assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
assert.True(t, getSize(resp.Body) > 100) assert.True(t, getSize(resp.Body) > 100)
assert.Len(t, resp.Header.Get("ETag"), 42) assert.Len(t, resp.Header.Get("ETag"), 44)
// TODO: test get of non cachable content (content size > fileCacheSizeLimit) // TODO: test get of non cachable content (content size > fileCacheSizeLimit)
} }
@ -68,9 +69,10 @@ func TestCustomDomain(t *testing.T) {
log.Println("=== TestCustomDomain ===") log.Println("=== TestCustomDomain ===")
resp, err := getTestHTTPSClient().Get("https://mock-pages.codeberg-test.org:4430/README.md") resp, err := getTestHTTPSClient().Get("https://mock-pages.codeberg-test.org:4430/README.md")
assert.NoError(t, err) assert.NoError(t, err)
if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) { if !assert.NotNil(t, resp) {
t.FailNow() t.FailNow()
} }
assert.EqualValues(t, http.StatusOK, resp.StatusCode)
assert.EqualValues(t, "text/markdown; charset=utf-8", resp.Header.Get("Content-Type")) assert.EqualValues(t, "text/markdown; charset=utf-8", resp.Header.Get("Content-Type"))
assert.EqualValues(t, "106", resp.Header.Get("Content-Length")) assert.EqualValues(t, "106", resp.Header.Get("Content-Length"))
assert.EqualValues(t, 106, getSize(resp.Body)) assert.EqualValues(t, 106, getSize(resp.Body))
@ -81,9 +83,10 @@ func TestGetNotFound(t *testing.T) {
// test custom not found pages // test custom not found pages
resp, err := getTestHTTPSClient().Get("https://crystal.localhost.mock.directory:4430/pages-404-demo/blah") resp, err := getTestHTTPSClient().Get("https://crystal.localhost.mock.directory:4430/pages-404-demo/blah")
assert.NoError(t, err) assert.NoError(t, err)
if !assert.EqualValues(t, http.StatusNotFound, resp.StatusCode) { if !assert.NotNil(t, resp) {
t.FailNow() t.FailNow()
} }
assert.EqualValues(t, http.StatusNotFound, resp.StatusCode)
assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type")) assert.EqualValues(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
assert.EqualValues(t, "37", resp.Header.Get("Content-Length")) assert.EqualValues(t, "37", resp.Header.Get("Content-Length"))
assert.EqualValues(t, 37, getSize(resp.Body)) assert.EqualValues(t, 37, getSize(resp.Body))
@ -94,9 +97,10 @@ func TestFollowSymlink(t *testing.T) {
resp, err := getTestHTTPSClient().Get("https://6543.localhost.mock.directory:4430/tests_for_pages-server/@main/link") resp, err := getTestHTTPSClient().Get("https://6543.localhost.mock.directory:4430/tests_for_pages-server/@main/link")
assert.NoError(t, err) assert.NoError(t, err)
if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) { if !assert.NotNil(t, resp) {
t.FailNow() t.FailNow()
} }
assert.EqualValues(t, http.StatusOK, resp.StatusCode)
assert.EqualValues(t, "application/octet-stream", resp.Header.Get("Content-Type")) assert.EqualValues(t, "application/octet-stream", resp.Header.Get("Content-Type"))
assert.EqualValues(t, "4", resp.Header.Get("Content-Length")) assert.EqualValues(t, "4", resp.Header.Get("Content-Length"))
body := getBytes(resp.Body) body := getBytes(resp.Body)
@ -109,14 +113,27 @@ func TestLFSSupport(t *testing.T) {
resp, err := getTestHTTPSClient().Get("https://6543.localhost.mock.directory:4430/tests_for_pages-server/@main/lfs.txt") resp, err := getTestHTTPSClient().Get("https://6543.localhost.mock.directory:4430/tests_for_pages-server/@main/lfs.txt")
assert.NoError(t, err) assert.NoError(t, err)
if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) { if !assert.NotNil(t, resp) {
t.FailNow() t.FailNow()
} }
assert.EqualValues(t, http.StatusOK, resp.StatusCode)
body := strings.TrimSpace(string(getBytes(resp.Body))) body := strings.TrimSpace(string(getBytes(resp.Body)))
assert.EqualValues(t, 12, len(body)) assert.EqualValues(t, 12, len(body))
assert.EqualValues(t, "actual value", body) assert.EqualValues(t, "actual value", body)
} }
func TestGetOptions(t *testing.T) {
log.Println("=== TestGetOptions ===")
req, _ := http.NewRequest(http.MethodOptions, "https://mock-pages.codeberg-test.org:4430/README.md", nil)
resp, err := getTestHTTPSClient().Do(req)
assert.NoError(t, err)
if !assert.NotNil(t, resp) {
t.FailNow()
}
assert.EqualValues(t, http.StatusNoContent, resp.StatusCode)
assert.EqualValues(t, "GET, HEAD, OPTIONS", resp.Header.Get("Allow"))
}
func getTestHTTPSClient() *http.Client { func getTestHTTPSClient() *http.Client {
cookieJar, _ := cookiejar.New(nil) cookieJar, _ := cookiejar.New(nil)
return &http.Client{ return &http.Client{

View file

@ -36,7 +36,7 @@ import (
) )
// TLSConfig returns the configuration for generating, serving and cleaning up Let's Encrypt certificates. // TLSConfig returns the configuration for generating, serving and cleaning up Let's Encrypt certificates.
func TLSConfig(mainDomainSuffix []byte, func TLSConfig(mainDomainSuffix string,
giteaClient *gitea.Client, giteaClient *gitea.Client,
dnsProvider string, dnsProvider string,
acmeUseRateLimits bool, acmeUseRateLimits bool,
@ -47,7 +47,6 @@ func TLSConfig(mainDomainSuffix []byte,
// check DNS name & get certificate from Let's Encrypt // check DNS name & get certificate from Let's Encrypt
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
sni := strings.ToLower(strings.TrimSpace(info.ServerName)) sni := strings.ToLower(strings.TrimSpace(info.ServerName))
sniBytes := []byte(sni)
if len(sni) < 1 { if len(sni) < 1 {
return nil, errors.New("missing sni") return nil, errors.New("missing sni")
} }
@ -69,23 +68,20 @@ func TLSConfig(mainDomainSuffix []byte,
} }
targetOwner := "" targetOwner := ""
if bytes.HasSuffix(sniBytes, mainDomainSuffix) || bytes.Equal(sniBytes, mainDomainSuffix[1:]) { if strings.HasSuffix(sni, mainDomainSuffix) || strings.EqualFold(sni, mainDomainSuffix[1:]) {
// deliver default certificate for the main domain (*.codeberg.page) // deliver default certificate for the main domain (*.codeberg.page)
sniBytes = mainDomainSuffix sni = mainDomainSuffix
sni = string(sniBytes)
} else { } else {
var targetRepo, targetBranch string var targetRepo, targetBranch string
targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(sni, string(mainDomainSuffix), dnsLookupCache) targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(sni, mainDomainSuffix, dnsLookupCache)
if targetOwner == "" { if targetOwner == "" {
// DNS not set up, return main certificate to redirect to the docs // DNS not set up, return main certificate to redirect to the docs
sniBytes = mainDomainSuffix sni = mainDomainSuffix
sni = string(sniBytes)
} else { } else {
_, _ = targetRepo, targetBranch _, _ = targetRepo, targetBranch
_, valid := upstream.CheckCanonicalDomain(giteaClient, targetOwner, targetRepo, targetBranch, sni, string(mainDomainSuffix), canonicalDomainCache) _, valid := upstream.CheckCanonicalDomain(giteaClient, targetOwner, targetRepo, targetBranch, sni, mainDomainSuffix, canonicalDomainCache)
if !valid { if !valid {
sniBytes = mainDomainSuffix sni = mainDomainSuffix
sni = string(sniBytes)
} }
} }
} }
@ -98,9 +94,9 @@ func TLSConfig(mainDomainSuffix []byte,
var tlsCertificate tls.Certificate var tlsCertificate tls.Certificate
var err error var err error
var ok bool var ok bool
if tlsCertificate, ok = retrieveCertFromDB(sniBytes, mainDomainSuffix, dnsProvider, acmeUseRateLimits, certDB); !ok { if tlsCertificate, ok = retrieveCertFromDB(sni, mainDomainSuffix, dnsProvider, acmeUseRateLimits, certDB); !ok {
// request a new certificate // request a new certificate
if bytes.Equal(sniBytes, mainDomainSuffix) { if strings.EqualFold(sni, mainDomainSuffix) {
return nil, errors.New("won't request certificate for main domain, something really bad has happened") return nil, errors.New("won't request certificate for main domain, something really bad has happened")
} }
@ -192,7 +188,7 @@ func (a AcmeHTTPChallengeProvider) CleanUp(domain, token, _ string) error {
return nil return nil
} }
func retrieveCertFromDB(sni, mainDomainSuffix []byte, dnsProvider string, acmeUseRateLimits bool, certDB database.CertDB) (tls.Certificate, bool) { func retrieveCertFromDB(sni, mainDomainSuffix, dnsProvider string, acmeUseRateLimits bool, certDB database.CertDB) (tls.Certificate, bool) {
// parse certificate from database // parse certificate from database
res, err := certDB.Get(string(sni)) res, err := certDB.Get(string(sni))
if err != nil { if err != nil {
@ -208,7 +204,7 @@ func retrieveCertFromDB(sni, mainDomainSuffix []byte, dnsProvider string, acmeUs
} }
// TODO: document & put into own function // TODO: document & put into own function
if !bytes.Equal(sni, mainDomainSuffix) { if !strings.EqualFold(sni, mainDomainSuffix) {
tlsCertificate.Leaf, err = x509.ParseCertificate(tlsCertificate.Certificate[0]) tlsCertificate.Leaf, err = x509.ParseCertificate(tlsCertificate.Certificate[0])
if err != nil { if err != nil {
panic(err) panic(err)
@ -239,7 +235,7 @@ func retrieveCertFromDB(sni, mainDomainSuffix []byte, dnsProvider string, acmeUs
var obtainLocks = sync.Map{} var obtainLocks = sync.Map{}
func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Resource, user, dnsProvider string, mainDomainSuffix []byte, acmeUseRateLimits bool, keyDatabase database.CertDB) (tls.Certificate, error) { func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Resource, user, dnsProvider, mainDomainSuffix string, acmeUseRateLimits bool, keyDatabase database.CertDB) (tls.Certificate, error) {
name := strings.TrimPrefix(domains[0], "*") name := strings.TrimPrefix(domains[0], "*")
if dnsProvider == "" && len(domains[0]) > 0 && domains[0][0] == '*' { if dnsProvider == "" && len(domains[0]) > 0 && domains[0][0] == '*' {
domains = domains[1:] domains = domains[1:]
@ -252,7 +248,7 @@ func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Re
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
_, working = obtainLocks.Load(name) _, working = obtainLocks.Load(name)
} }
cert, ok := retrieveCertFromDB([]byte(name), mainDomainSuffix, dnsProvider, acmeUseRateLimits, keyDatabase) cert, ok := retrieveCertFromDB(name, mainDomainSuffix, dnsProvider, acmeUseRateLimits, keyDatabase)
if !ok { if !ok {
return tls.Certificate{}, errors.New("certificate failed in synchronous request") return tls.Certificate{}, errors.New("certificate failed in synchronous request")
} }
@ -405,7 +401,7 @@ func SetupAcmeConfig(acmeAPI, acmeMail, acmeEabHmac, acmeEabKID string, acmeAcce
return myAcmeConfig, nil return myAcmeConfig, nil
} }
func SetupCertificates(mainDomainSuffix []byte, dnsProvider string, acmeConfig *lego.Config, acmeUseRateLimits, enableHTTPServer bool, challengeCache cache.SetGetKey, certDB database.CertDB) error { 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 // 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(string(mainDomainSuffix))
if err != nil { if err != nil {
@ -460,7 +456,7 @@ func SetupCertificates(mainDomainSuffix []byte, dnsProvider string, acmeConfig *
return nil return nil
} }
func MaintainCertDB(ctx context.Context, interval time.Duration, mainDomainSuffix []byte, dnsProvider string, acmeUseRateLimits bool, certDB database.CertDB) { func MaintainCertDB(ctx context.Context, interval time.Duration, mainDomainSuffix, dnsProvider string, acmeUseRateLimits bool, certDB database.CertDB) {
for { for {
// clean up expired certs // clean up expired certs
now := time.Now() now := time.Now()
@ -468,7 +464,7 @@ func MaintainCertDB(ctx context.Context, interval time.Duration, mainDomainSuffi
keyDatabaseIterator := certDB.Items() keyDatabaseIterator := certDB.Items()
key, resBytes, err := keyDatabaseIterator.Next() key, resBytes, err := keyDatabaseIterator.Next()
for err == nil { for err == nil {
if !bytes.Equal(key, mainDomainSuffix) { if !strings.EqualFold(string(key), mainDomainSuffix) {
resGob := bytes.NewBuffer(resBytes) resGob := bytes.NewBuffer(resBytes)
resDec := gob.NewDecoder(resGob) resDec := gob.NewDecoder(resGob)
res := &certificate.Resource{} res := &certificate.Resource{}

62
server/context/context.go Normal file
View file

@ -0,0 +1,62 @@
package context
import (
stdContext "context"
"net/http"
)
type Context struct {
RespWriter http.ResponseWriter
Req *http.Request
StatusCode int
}
func New(w http.ResponseWriter, r *http.Request) *Context {
return &Context{
RespWriter: w,
Req: r,
StatusCode: http.StatusOK,
}
}
func (c *Context) Context() stdContext.Context {
if c.Req != nil {
return c.Req.Context()
}
return stdContext.Background()
}
func (c *Context) Response() *http.Response {
if c.Req != nil && c.Req.Response != nil {
return c.Req.Response
}
return nil
}
func (c *Context) String(raw string, status ...int) {
code := http.StatusOK
if len(status) != 0 {
code = status[0]
}
c.RespWriter.WriteHeader(code)
_, _ = c.RespWriter.Write([]byte(raw))
}
func (c *Context) IsMethod(m string) bool {
return c.Req.Method == m
}
func (c *Context) Redirect(uri string, statusCode int) {
http.Redirect(c.RespWriter, c.Req, uri, statusCode)
}
// Path returns requested path.
//
// The returned bytes are valid until your request handler returns.
func (c *Context) Path() string {
return c.Req.URL.Path
}
func (c *Context) Host() string {
return c.Req.URL.Host
}

View file

@ -28,7 +28,7 @@ func (p tmpDB) Put(name string, cert *certificate.Resource) error {
func (p tmpDB) Get(name string) (*certificate.Resource, error) { func (p tmpDB) Get(name string) (*certificate.Resource, error) {
cert, has := p.intern.Get(name) cert, has := p.intern.Get(name)
if !has { if !has {
return nil, fmt.Errorf("cert for '%s' not found", name) return nil, fmt.Errorf("cert for %q not found", name)
} }
return cert.(*certificate.Resource), nil return cert.(*certificate.Resource), nil
} }

View file

@ -1,6 +0,0 @@
package dns
import "time"
// lookupCacheTimeout specifies the timeout for the DNS lookup cache.
var lookupCacheTimeout = 15 * time.Minute

View file

@ -3,10 +3,14 @@ package dns
import ( import (
"net" "net"
"strings" "strings"
"time"
"codeberg.org/codeberg/pages/server/cache" "codeberg.org/codeberg/pages/server/cache"
) )
// lookupCacheTimeout specifies the timeout for the DNS lookup cache.
var lookupCacheTimeout = 15 * time.Minute
// GetTargetFromDNS searches for CNAME or TXT entries on the request domain ending with MainDomainSuffix. // GetTargetFromDNS searches for CNAME or TXT entries on the request domain ending with MainDomainSuffix.
// If everything is fine, it returns the target data. // 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 string, dnsLookupCache cache.SetGetKey) (targetOwner, targetRepo, targetBranch string) {

View file

@ -1,12 +1,116 @@
package gitea package gitea
import (
"bytes"
"fmt"
"io"
"net/http"
"time"
"github.com/rs/zerolog/log"
"codeberg.org/codeberg/pages/server/cache"
)
const (
// defaultBranchCacheTimeout specifies the timeout for the default branch cache. It can be quite long.
defaultBranchCacheTimeout = 15 * time.Minute
// branchExistenceCacheTimeout specifies the timeout for the branch timestamp & existence cache. It should be shorter
// than fileCacheTimeout, as that gets invalidated if the branch timestamp has changed. That way, repo changes will be
// picked up faster, while still allowing the content to be cached longer if nothing changes.
branchExistenceCacheTimeout = 5 * time.Minute
// fileCacheTimeout specifies the timeout for the file content cache - you might want to make this quite long, depending
// on your available memory.
// TODO: move as option into cache interface
fileCacheTimeout = 5 * time.Minute
// fileCacheSizeLimit limits the maximum file size that will be cached, and is set to 1 MB by default.
fileCacheSizeLimit = int64(1000 * 1000)
)
type FileResponse struct { type FileResponse struct {
Exists bool Exists bool
ETag []byte IsSymlink bool
MimeType string ETag string
Body []byte MimeType string
Body []byte
} }
func (f FileResponse) IsEmpty() bool { func (f FileResponse) IsEmpty() bool {
return len(f.Body) != 0 return len(f.Body) != 0
} }
func (f FileResponse) createHttpResponse(cacheKey string) (http.Header, int) {
header := make(http.Header)
var statusCode int
if f.Exists {
statusCode = http.StatusOK
} else {
statusCode = http.StatusNotFound
}
if f.IsSymlink {
header.Set(giteaObjectTypeHeader, objTypeSymlink)
}
header.Set(ETagHeader, f.ETag)
header.Set(ContentTypeHeader, f.MimeType)
header.Set(ContentLengthHeader, fmt.Sprintf("%d", len(f.Body)))
header.Set(PagesCacheIndicatorHeader, "true")
log.Trace().Msgf("fileCache for %q used", cacheKey)
return header, statusCode
}
type BranchTimestamp struct {
Branch string
Timestamp time.Time
notFound bool
}
type writeCacheReader struct {
originalReader io.ReadCloser
buffer *bytes.Buffer
rileResponse *FileResponse
cacheKey string
cache cache.SetGetKey
hasError bool
}
func (t *writeCacheReader) Read(p []byte) (n int, err error) {
n, err = t.originalReader.Read(p)
if err != nil {
log.Trace().Err(err).Msgf("[cache] original reader for %q has returned an error", t.cacheKey)
t.hasError = true
} else if n > 0 {
_, _ = t.buffer.Write(p[:n])
}
return
}
func (t *writeCacheReader) Close() error {
if !t.hasError {
fc := *t.rileResponse
fc.Body = t.buffer.Bytes()
_ = t.cache.Set(t.cacheKey, fc, fileCacheTimeout)
}
log.Trace().Msgf("cacheReader for %q saved=%t closed", t.cacheKey, !t.hasError)
return t.originalReader.Close()
}
func (f FileResponse) CreateCacheReader(r io.ReadCloser, cache cache.SetGetKey, cacheKey string) io.ReadCloser {
if r == nil || cache == nil || cacheKey == "" {
log.Error().Msg("could not create CacheReader")
return nil
}
return &writeCacheReader{
originalReader: r,
buffer: bytes.NewBuffer(make([]byte, 0)),
rileResponse: &f,
cache: cache,
cacheKey: cacheKey,
}
}

View file

@ -1,142 +1,276 @@
package gitea package gitea
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"io"
"mime"
"net/http"
"net/url" "net/url"
"path"
"strconv"
"strings" "strings"
"time" "time"
"code.gitea.io/sdk/gitea"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/valyala/fasthttp"
"github.com/valyala/fastjson"
)
const ( "codeberg.org/codeberg/pages/server/cache"
giteaAPIRepos = "/api/v1/repos/"
giteaObjectTypeHeader = "X-Gitea-Object-Type"
) )
var ErrorNotFound = errors.New("not found") var ErrorNotFound = errors.New("not found")
const (
// cache key prefixe
branchTimestampCacheKeyPrefix = "branchTime"
defaultBranchCacheKeyPrefix = "defaultBranch"
rawContentCacheKeyPrefix = "rawContent"
// pages server
PagesCacheIndicatorHeader = "X-Pages-Cache"
symlinkReadLimit = 10000
// gitea
giteaObjectTypeHeader = "X-Gitea-Object-Type"
objTypeSymlink = "symlink"
// std
ETagHeader = "ETag"
ContentTypeHeader = "Content-Type"
ContentLengthHeader = "Content-Length"
)
type Client struct { type Client struct {
giteaRoot string sdkClient *gitea.Client
giteaAPIToken string responseCache cache.SetGetKey
fastClient *fasthttp.Client
infoTimeout time.Duration
contentTimeout time.Duration
followSymlinks bool followSymlinks bool
supportLFS bool supportLFS bool
forbiddenMimeTypes map[string]bool
defaultMimeType string
} }
// TODO: once golang v1.19 is min requirement, we can switch to 'JoinPath()' of 'net/url' package func NewClient(giteaRoot, giteaAPIToken string, respCache cache.SetGetKey, followSymlinks, supportLFS bool) (*Client, error) {
func joinURL(baseURL string, paths ...string) string {
p := make([]string, 0, len(paths))
for i := range paths {
path := strings.TrimSpace(paths[i])
path = strings.Trim(path, "/")
if len(path) != 0 {
p = append(p, path)
}
}
return baseURL + "/" + strings.Join(p, "/")
}
func NewClient(giteaRoot, giteaAPIToken string, followSymlinks, supportLFS bool) (*Client, error) {
rootURL, err := url.Parse(giteaRoot) rootURL, err := url.Parse(giteaRoot)
if err != nil {
return nil, err
}
giteaRoot = strings.Trim(rootURL.String(), "/") giteaRoot = strings.Trim(rootURL.String(), "/")
stdClient := http.Client{Timeout: 10 * time.Second}
// TODO: pass down
var (
forbiddenMimeTypes map[string]bool
defaultMimeType string
)
if forbiddenMimeTypes == nil {
forbiddenMimeTypes = make(map[string]bool)
}
if defaultMimeType == "" {
defaultMimeType = "application/octet-stream"
}
sdk, err := gitea.NewClient(giteaRoot, gitea.SetHTTPClient(&stdClient), gitea.SetToken(giteaAPIToken))
return &Client{ return &Client{
giteaRoot: giteaRoot, sdkClient: sdk,
giteaAPIToken: giteaAPIToken, responseCache: respCache,
infoTimeout: 5 * time.Second,
contentTimeout: 10 * time.Second,
fastClient: getFastHTTPClient(),
followSymlinks: followSymlinks, followSymlinks: followSymlinks,
supportLFS: supportLFS, supportLFS: supportLFS,
forbiddenMimeTypes: forbiddenMimeTypes,
defaultMimeType: defaultMimeType,
}, err }, err
} }
func (client *Client) GiteaRawContent(targetOwner, targetRepo, ref, resource string) ([]byte, error) { func (client *Client) GiteaRawContent(targetOwner, targetRepo, ref, resource string) ([]byte, error) {
resp, err := client.ServeRawContent(targetOwner, targetRepo, ref, resource) reader, _, _, err := client.ServeRawContent(targetOwner, targetRepo, ref, resource)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return resp.Body(), nil defer reader.Close()
return io.ReadAll(reader)
} }
func (client *Client) ServeRawContent(targetOwner, targetRepo, ref, resource string) (*fasthttp.Response, error) { func (client *Client) ServeRawContent(targetOwner, targetRepo, ref, resource string) (io.ReadCloser, http.Header, int, error) {
var apiURL string cacheKey := fmt.Sprintf("%s/%s/%s|%s|%s", rawContentCacheKeyPrefix, targetOwner, targetRepo, ref, resource)
if client.supportLFS { log := log.With().Str("cache_key", cacheKey).Logger()
apiURL = joinURL(client.giteaRoot, giteaAPIRepos, targetOwner, targetRepo, "media", resource+"?ref="+url.QueryEscape(ref))
} else {
apiURL = joinURL(client.giteaRoot, giteaAPIRepos, targetOwner, targetRepo, "raw", resource+"?ref="+url.QueryEscape(ref))
}
resp, err := client.do(client.contentTimeout, apiURL)
if err != nil {
return nil, err
}
if err != nil { // handle if cache entry exist
return nil, err if cache, ok := client.responseCache.Get(cacheKey); ok {
} cache := cache.(FileResponse)
cachedHeader, cachedStatusCode := cache.createHttpResponse(cacheKey)
switch resp.StatusCode() { // TODO: check against some timestamp missmatch?!?
case fasthttp.StatusOK: if cache.Exists {
objType := string(resp.Header.Peek(giteaObjectTypeHeader)) if cache.IsSymlink {
log.Trace().Msgf("server raw content object: %s", objType) linkDest := string(cache.Body)
if client.followSymlinks && objType == "symlink" { log.Debug().Msgf("[cache] follow symlink from %q to %q", resource, linkDest)
// TODO: limit to 1000 chars if we switched to std return client.ServeRawContent(targetOwner, targetRepo, ref, linkDest)
linkDest := strings.TrimSpace(string(resp.Body())) } else {
log.Debug().Msgf("follow symlink from '%s' to '%s'", resource, linkDest) log.Debug().Msg("[cache] return bytes")
return client.ServeRawContent(targetOwner, targetRepo, ref, linkDest) return io.NopCloser(bytes.NewReader(cache.Body)), cachedHeader, cachedStatusCode, nil
}
} else {
return nil, cachedHeader, cachedStatusCode, ErrorNotFound
} }
return resp, nil
case fasthttp.StatusNotFound:
return nil, ErrorNotFound
default:
return nil, fmt.Errorf("unexpected status code '%d'", resp.StatusCode())
} }
// not in cache, open reader via gitea api
reader, resp, err := client.sdkClient.GetFileReader(targetOwner, targetRepo, ref, resource, client.supportLFS)
if resp != nil {
switch resp.StatusCode {
case http.StatusOK:
// first handle symlinks
{
objType := resp.Header.Get(giteaObjectTypeHeader)
log.Trace().Msgf("server raw content object %q", objType)
if client.followSymlinks && objType == objTypeSymlink {
defer reader.Close()
// read limited chars for symlink
linkDestBytes, err := io.ReadAll(io.LimitReader(reader, symlinkReadLimit))
if err != nil {
return nil, nil, http.StatusInternalServerError, err
}
linkDest := strings.TrimSpace(string(linkDestBytes))
// we store symlink not content to reduce duplicates in cache
if err := client.responseCache.Set(cacheKey, FileResponse{
Exists: true,
IsSymlink: true,
Body: []byte(linkDest),
ETag: resp.Header.Get(ETagHeader),
}, fileCacheTimeout); err != nil {
log.Error().Err(err).Msg("[cache] error on cache write")
}
log.Debug().Msgf("follow symlink from %q to %q", resource, linkDest)
return client.ServeRawContent(targetOwner, targetRepo, ref, linkDest)
}
}
// now we are sure it's content so set the MIME type
mimeType := client.getMimeTypeByExtension(resource)
resp.Response.Header.Set(ContentTypeHeader, mimeType)
if !shouldRespBeSavedToCache(resp.Response) {
return reader, resp.Response.Header, resp.StatusCode, err
}
// now we write to cache and respond at the sime time
fileResp := FileResponse{
Exists: true,
ETag: resp.Header.Get(ETagHeader),
MimeType: mimeType,
}
return fileResp.CreateCacheReader(reader, client.responseCache, cacheKey), resp.Response.Header, resp.StatusCode, nil
case http.StatusNotFound:
if err := client.responseCache.Set(cacheKey, FileResponse{
Exists: false,
ETag: resp.Header.Get(ETagHeader),
}, fileCacheTimeout); err != nil {
log.Error().Err(err).Msg("[cache] error on cache write")
}
return nil, resp.Response.Header, http.StatusNotFound, ErrorNotFound
default:
return nil, resp.Response.Header, resp.StatusCode, fmt.Errorf("unexpected status code '%d'", resp.StatusCode)
}
}
return nil, nil, http.StatusInternalServerError, err
} }
func (client *Client) GiteaGetRepoBranchTimestamp(repoOwner, repoName, branchName string) (time.Time, error) { func (client *Client) GiteaGetRepoBranchTimestamp(repoOwner, repoName, branchName string) (*BranchTimestamp, error) {
url := joinURL(client.giteaRoot, giteaAPIRepos, repoOwner, repoName, "branches", branchName) cacheKey := fmt.Sprintf("%s/%s/%s/%s", branchTimestampCacheKeyPrefix, repoOwner, repoName, branchName)
res, err := client.do(client.infoTimeout, url)
if stamp, ok := client.responseCache.Get(cacheKey); ok && stamp != nil {
branchTimeStamp := stamp.(*BranchTimestamp)
if branchTimeStamp.notFound {
log.Trace().Msgf("[cache] use branch %q not found", branchName)
return &BranchTimestamp{}, ErrorNotFound
}
log.Trace().Msgf("[cache] use branch %q exist", branchName)
return branchTimeStamp, nil
}
branch, resp, err := client.sdkClient.GetRepoBranch(repoOwner, repoName, branchName)
if err != nil { if err != nil {
return time.Time{}, err if resp != nil && resp.StatusCode == http.StatusNotFound {
log.Trace().Msgf("[cache] set cache branch %q not found", branchName)
if err := client.responseCache.Set(cacheKey, &BranchTimestamp{Branch: branchName, notFound: true}, branchExistenceCacheTimeout); err != nil {
log.Error().Err(err).Msg("[cache] error on cache write")
}
return &BranchTimestamp{}, ErrorNotFound
}
return &BranchTimestamp{}, err
} }
if res.StatusCode() != fasthttp.StatusOK { if resp.StatusCode != http.StatusOK {
return time.Time{}, fmt.Errorf("unexpected status code '%d'", res.StatusCode()) return &BranchTimestamp{}, fmt.Errorf("unexpected status code '%d'", resp.StatusCode)
} }
return time.Parse(time.RFC3339, fastjson.GetString(res.Body(), "commit", "timestamp"))
stamp := &BranchTimestamp{
Branch: branch.Name,
Timestamp: branch.Commit.Timestamp,
}
log.Trace().Msgf("set cache branch [%s] exist", branchName)
if err := client.responseCache.Set(cacheKey, stamp, branchExistenceCacheTimeout); err != nil {
log.Error().Err(err).Msg("[cache] error on cache write")
}
return stamp, nil
} }
func (client *Client) GiteaGetRepoDefaultBranch(repoOwner, repoName string) (string, error) { func (client *Client) GiteaGetRepoDefaultBranch(repoOwner, repoName string) (string, error) {
url := joinURL(client.giteaRoot, giteaAPIRepos, repoOwner, repoName) cacheKey := fmt.Sprintf("%s/%s/%s", defaultBranchCacheKeyPrefix, repoOwner, repoName)
res, err := client.do(client.infoTimeout, url)
if branch, ok := client.responseCache.Get(cacheKey); ok && branch != nil {
return branch.(string), nil
}
repo, resp, err := client.sdkClient.GetRepo(repoOwner, repoName)
if err != nil { if err != nil {
return "", err return "", err
} }
if res.StatusCode() != fasthttp.StatusOK { if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("unexpected status code '%d'", res.StatusCode()) return "", fmt.Errorf("unexpected status code '%d'", resp.StatusCode)
} }
return fastjson.GetString(res.Body(), "default_branch"), nil
branch := repo.DefaultBranch
if err := client.responseCache.Set(cacheKey, branch, defaultBranchCacheTimeout); err != nil {
log.Error().Err(err).Msg("[cache] error on cache write")
}
return branch, nil
} }
func (client *Client) do(timeout time.Duration, url string) (*fasthttp.Response, error) { func (client *Client) getMimeTypeByExtension(resource string) string {
req := fasthttp.AcquireRequest() mimeType := mime.TypeByExtension(path.Ext(resource))
mimeTypeSplit := strings.SplitN(mimeType, ";", 2)
req.SetRequestURI(url) if client.forbiddenMimeTypes[mimeTypeSplit[0]] || mimeType == "" {
req.Header.Set(fasthttp.HeaderAuthorization, "token "+client.giteaAPIToken) mimeType = client.defaultMimeType
res := fasthttp.AcquireResponse() }
log.Trace().Msgf("probe mime of %q is %q", resource, mimeType)
err := client.fastClient.DoTimeout(req, res, timeout) return mimeType
}
return res, err
func shouldRespBeSavedToCache(resp *http.Response) bool {
if resp == nil {
return false
}
contentLengthRaw := resp.Header.Get(ContentLengthHeader)
if contentLengthRaw == "" {
return false
}
contentLeng, err := strconv.ParseInt(contentLengthRaw, 10, 64)
if err != nil {
log.Error().Err(err).Msg("could not parse content length")
}
// if content to big or could not be determined we not cache it
return contentLeng > 0 && contentLeng < fileCacheSizeLimit
} }

View file

@ -1,23 +0,0 @@
package gitea
import (
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
func TestJoinURL(t *testing.T) {
baseURL := ""
assert.EqualValues(t, "/", joinURL(baseURL))
assert.EqualValues(t, "/", joinURL(baseURL, "", ""))
baseURL = "http://wwow.url.com"
assert.EqualValues(t, "http://wwow.url.com/a/b/c/d", joinURL(baseURL, "a", "b/c/", "d"))
baseURL = "http://wow.url.com/subpath/2"
assert.EqualValues(t, "http://wow.url.com/subpath/2/content.pdf", joinURL(baseURL, "/content.pdf"))
assert.EqualValues(t, "http://wow.url.com/subpath/2/wonderful.jpg", joinURL(baseURL, "wonderful.jpg"))
assert.EqualValues(t, "http://wow.url.com/subpath/2/raw/wonderful.jpg?ref=main", joinURL(baseURL, "raw", "wonderful.jpg"+"?ref="+url.QueryEscape("main")))
assert.EqualValues(t, "http://wow.url.com/subpath/2/raw/wonderful.jpg%3Fref=main", joinURL(baseURL, "raw", "wonderful.jpg%3Fref=main"))
}

View file

@ -1,15 +0,0 @@
package gitea
import (
"time"
"github.com/valyala/fasthttp"
)
func getFastHTTPClient() *fasthttp.Client {
return &fasthttp.Client{
MaxConnDuration: 60 * time.Second,
MaxConnWaitTimeout: 1000 * time.Millisecond,
MaxConnsPerHost: 128 * 16, // TODO: adjust bottlenecks for best performance with Gitea!
}
}

View file

@ -1,15 +1,17 @@
package server package server
import ( import (
"bytes" "fmt"
"net/http"
"path"
"strings" "strings"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/valyala/fasthttp"
"codeberg.org/codeberg/pages/html" "codeberg.org/codeberg/pages/html"
"codeberg.org/codeberg/pages/server/cache" "codeberg.org/codeberg/pages/server/cache"
"codeberg.org/codeberg/pages/server/context"
"codeberg.org/codeberg/pages/server/dns" "codeberg.org/codeberg/pages/server/dns"
"codeberg.org/codeberg/pages/server/gitea" "codeberg.org/codeberg/pages/server/gitea"
"codeberg.org/codeberg/pages/server/upstream" "codeberg.org/codeberg/pages/server/upstream"
@ -17,42 +19,48 @@ import (
"codeberg.org/codeberg/pages/server/version" "codeberg.org/codeberg/pages/server/version"
) )
const (
headerAccessControlAllowOrigin = "Access-Control-Allow-Origin"
headerAccessControlAllowMethods = "Access-Control-Allow-Methods"
)
// Handler handles a single HTTP request to the web server. // Handler handles a single HTTP request to the web server.
func Handler(mainDomainSuffix, rawDomain []byte, func Handler(mainDomainSuffix, rawDomain string,
giteaClient *gitea.Client, giteaClient *gitea.Client,
giteaRoot, rawInfoPage string, giteaRoot, rawInfoPage string,
blacklistedPaths, allowedCorsDomains [][]byte, blacklistedPaths, allowedCorsDomains []string,
dnsLookupCache, canonicalDomainCache, branchTimestampCache, fileResponseCache cache.SetGetKey, dnsLookupCache, canonicalDomainCache cache.SetGetKey,
) func(ctx *fasthttp.RequestCtx) { ) http.HandlerFunc {
return func(ctx *fasthttp.RequestCtx) { return func(w http.ResponseWriter, req *http.Request) {
log := log.With().Strs("Handler", []string{string(ctx.Request.Host()), string(ctx.Request.Header.RequestURI())}).Logger() log := log.With().Strs("Handler", []string{string(req.Host), req.RequestURI}).Logger()
ctx := context.New(w, req)
ctx.Response.Header.Set("Server", "CodebergPages/"+version.Version) ctx.RespWriter.Header().Set("Server", "CodebergPages/"+version.Version)
// Force new default from specification (since November 2020) - see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#strict-origin-when-cross-origin // Force new default from specification (since November 2020) - see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#strict-origin-when-cross-origin
ctx.Response.Header.Set("Referrer-Policy", "strict-origin-when-cross-origin") ctx.RespWriter.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
// Enable browser caching for up to 10 minutes // Enable browser caching for up to 10 minutes
ctx.Response.Header.Set("Cache-Control", "public, max-age=600") ctx.RespWriter.Header().Set("Cache-Control", "public, max-age=600")
trimmedHost := utils.TrimHostPort(ctx.Request.Host()) trimmedHost := utils.TrimHostPort(req.Host)
// Add HSTS for RawDomain and MainDomainSuffix // Add HSTS for RawDomain and MainDomainSuffix
if hsts := GetHSTSHeader(trimmedHost, mainDomainSuffix, rawDomain); hsts != "" { if hsts := getHSTSHeader(trimmedHost, mainDomainSuffix, rawDomain); hsts != "" {
ctx.Response.Header.Set("Strict-Transport-Security", hsts) ctx.RespWriter.Header().Set("Strict-Transport-Security", hsts)
} }
// Block all methods not required for static pages // Block all methods not required for static pages
if !ctx.IsGet() && !ctx.IsHead() && !ctx.IsOptions() { if !ctx.IsMethod(http.MethodGet) && !ctx.IsMethod(http.MethodHead) && !ctx.IsMethod(http.MethodOptions) {
ctx.Response.Header.Set("Allow", "GET, HEAD, OPTIONS") ctx.RespWriter.Header().Set("Allow", http.MethodGet+", "+http.MethodHead+", "+http.MethodOptions) // duplic 1
ctx.Error("Method not allowed", fasthttp.StatusMethodNotAllowed) ctx.String("Method not allowed", http.StatusMethodNotAllowed)
return return
} }
// Block blacklisted paths (like ACME challenges) // Block blacklisted paths (like ACME challenges)
for _, blacklistedPath := range blacklistedPaths { for _, blacklistedPath := range blacklistedPaths {
if bytes.HasPrefix(ctx.Path(), blacklistedPath) { if strings.HasPrefix(ctx.Path(), blacklistedPath) {
html.ReturnErrorPage(ctx, fasthttp.StatusForbidden) html.ReturnErrorPage(ctx, "requested blacklisted path", http.StatusForbidden)
return return
} }
} }
@ -60,18 +68,19 @@ func Handler(mainDomainSuffix, rawDomain []byte,
// Allow CORS for specified domains // Allow CORS for specified domains
allowCors := false allowCors := false
for _, allowedCorsDomain := range allowedCorsDomains { for _, allowedCorsDomain := range allowedCorsDomains {
if bytes.Equal(trimmedHost, allowedCorsDomain) { if strings.EqualFold(trimmedHost, allowedCorsDomain) {
allowCors = true allowCors = true
break break
} }
} }
if allowCors { if allowCors {
ctx.Response.Header.Set("Access-Control-Allow-Origin", "*") ctx.RespWriter.Header().Set(headerAccessControlAllowOrigin, "*")
ctx.Response.Header.Set("Access-Control-Allow-Methods", "GET, HEAD") ctx.RespWriter.Header().Set(headerAccessControlAllowMethods, http.MethodGet+", "+http.MethodHead)
} }
ctx.Response.Header.Set("Allow", "GET, HEAD, OPTIONS")
if ctx.IsOptions() { ctx.RespWriter.Header().Set("Allow", http.MethodGet+", "+http.MethodHead+", "+http.MethodOptions) // duplic 1
ctx.Response.Header.SetStatusCode(fasthttp.StatusNoContent) if ctx.IsMethod(http.MethodOptions) {
ctx.RespWriter.WriteHeader(http.StatusNoContent)
return return
} }
@ -83,9 +92,10 @@ func Handler(mainDomainSuffix, rawDomain []byte,
// tryBranch checks if a branch exists and populates the target variables. If canonicalLink is non-empty, it will // tryBranch checks if a branch exists and populates the target variables. If canonicalLink is non-empty, it will
// also disallow search indexing and add a Link header to the canonical URL. // also disallow search indexing and add a Link header to the canonical URL.
tryBranch := func(log zerolog.Logger, repo, branch string, path []string, canonicalLink string) bool { // TODO: move into external func to not alert vars indirectly
tryBranch := func(log zerolog.Logger, repo, branch string, _path []string, canonicalLink string) bool {
if repo == "" { if repo == "" {
log.Warn().Msg("tryBranch: repo is empty") log.Debug().Msg("tryBranch: repo is empty")
return false return false
} }
@ -94,23 +104,23 @@ func Handler(mainDomainSuffix, rawDomain []byte,
branch = strings.ReplaceAll(branch, "~", "/") branch = strings.ReplaceAll(branch, "~", "/")
// Check if the branch exists, otherwise treat it as a file path // Check if the branch exists, otherwise treat it as a file path
branchTimestampResult := upstream.GetBranchTimestamp(giteaClient, targetOwner, repo, branch, branchTimestampCache) branchTimestampResult := upstream.GetBranchTimestamp(giteaClient, targetOwner, repo, branch)
if branchTimestampResult == nil { if branchTimestampResult == nil {
log.Warn().Msg("tryBranch: branch doesn't exist") log.Debug().Msg("tryBranch: branch doesn't exist")
return false return false
} }
// Branch exists, use it // Branch exists, use it
targetRepo = repo targetRepo = repo
targetPath = strings.Trim(strings.Join(path, "/"), "/") targetPath = path.Join(_path...)
targetBranch = branchTimestampResult.Branch targetBranch = branchTimestampResult.Branch
targetOptions.BranchTimestamp = branchTimestampResult.Timestamp targetOptions.BranchTimestamp = branchTimestampResult.Timestamp
if canonicalLink != "" { if canonicalLink != "" {
// Hide from search machines & add canonical link // Hide from search machines & add canonical link
ctx.Response.Header.Set("X-Robots-Tag", "noarchive, noindex") ctx.RespWriter.Header().Set("X-Robots-Tag", "noarchive, noindex")
ctx.Response.Header.Set("Link", ctx.RespWriter.Header().Set("Link",
strings.NewReplacer("%b", targetBranch, "%p", targetPath).Replace(canonicalLink)+ strings.NewReplacer("%b", targetBranch, "%p", targetPath).Replace(canonicalLink)+
"; rel=\"canonical\"", "; rel=\"canonical\"",
) )
@ -120,22 +130,18 @@ func Handler(mainDomainSuffix, rawDomain []byte,
return true return true
} }
log.Debug().Msg("Preparing") log.Debug().Msg("preparations")
if rawDomain != nil && bytes.Equal(trimmedHost, rawDomain) { if rawDomain != "" && strings.EqualFold(trimmedHost, rawDomain) {
// Serve raw content from RawDomain // Serve raw content from RawDomain
log.Debug().Msg("Serving raw domain") log.Debug().Msg("raw domain")
targetOptions.TryIndexPages = false targetOptions.TryIndexPages = false
if targetOptions.ForbiddenMimeTypes == nil { targetOptions.ServeRaw = true
targetOptions.ForbiddenMimeTypes = make(map[string]bool)
}
targetOptions.ForbiddenMimeTypes["text/html"] = true
targetOptions.DefaultMimeType = "text/plain; charset=utf-8"
pathElements := strings.Split(string(bytes.Trim(ctx.Request.URI().Path(), "/")), "/") pathElements := strings.Split(strings.Trim(ctx.Path(), "/"), "/")
if len(pathElements) < 2 { if len(pathElements) < 2 {
// https://{RawDomain}/{owner}/{repo}[/@{branch}]/{path} is required // https://{RawDomain}/{owner}/{repo}[/@{branch}]/{path} is required
ctx.Redirect(rawInfoPage, fasthttp.StatusTemporaryRedirect) ctx.Redirect(rawInfoPage, http.StatusTemporaryRedirect)
return return
} }
targetOwner = pathElements[0] targetOwner = pathElements[0]
@ -143,45 +149,45 @@ func Handler(mainDomainSuffix, rawDomain []byte,
// raw.codeberg.org/example/myrepo/@main/index.html // raw.codeberg.org/example/myrepo/@main/index.html
if len(pathElements) > 2 && strings.HasPrefix(pathElements[2], "@") { if len(pathElements) > 2 && strings.HasPrefix(pathElements[2], "@") {
log.Debug().Msg("Preparing raw domain, now trying with specified branch") log.Debug().Msg("raw domain preparations, now trying with specified branch")
if tryBranch(log, if tryBranch(log,
targetRepo, pathElements[2][1:], pathElements[3:], targetRepo, pathElements[2][1:], pathElements[3:],
giteaRoot+"/"+targetOwner+"/"+targetRepo+"/src/branch/%b/%p", giteaRoot+"/"+targetOwner+"/"+targetRepo+"/src/branch/%b/%p",
) { ) {
log.Info().Msg("tryBranch, now trying upstream 1") log.Debug().Msg("tryBranch, now trying upstream 1")
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost,
targetOptions, targetOwner, targetRepo, targetBranch, targetPath, targetOptions, targetOwner, targetRepo, targetBranch, targetPath,
canonicalDomainCache, branchTimestampCache, fileResponseCache) canonicalDomainCache)
return return
} }
log.Warn().Msg("Path missed a branch") log.Debug().Msg("missing branch info")
html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency) html.ReturnErrorPage(ctx, "missing branch info", http.StatusFailedDependency)
return return
} }
log.Debug().Msg("Preparing raw domain, now trying with default branch") log.Debug().Msg("raw domain preparations, now trying with default branch")
tryBranch(log, tryBranch(log,
targetRepo, "", pathElements[2:], targetRepo, "", pathElements[2:],
giteaRoot+"/"+targetOwner+"/"+targetRepo+"/src/branch/%b/%p", giteaRoot+"/"+targetOwner+"/"+targetRepo+"/src/branch/%b/%p",
) )
log.Info().Msg("tryBranch, now trying upstream 2") log.Debug().Msg("tryBranch, now trying upstream 2")
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost,
targetOptions, targetOwner, targetRepo, targetBranch, targetPath, targetOptions, targetOwner, targetRepo, targetBranch, targetPath,
canonicalDomainCache, branchTimestampCache, fileResponseCache) canonicalDomainCache)
return return
} else if bytes.HasSuffix(trimmedHost, mainDomainSuffix) { } else if strings.HasSuffix(trimmedHost, mainDomainSuffix) {
// Serve pages from subdomains of MainDomainSuffix // Serve pages from subdomains of MainDomainSuffix
log.Info().Msg("Serve pages from main domain suffix") log.Debug().Msg("main domain suffix")
pathElements := strings.Split(string(bytes.Trim(ctx.Request.URI().Path(), "/")), "/") pathElements := strings.Split(strings.Trim(ctx.Path(), "/"), "/")
targetOwner = string(bytes.TrimSuffix(trimmedHost, mainDomainSuffix)) targetOwner = strings.TrimSuffix(trimmedHost, mainDomainSuffix)
targetRepo = pathElements[0] targetRepo = pathElements[0]
targetPath = strings.Trim(strings.Join(pathElements[1:], "/"), "/") targetPath = strings.Trim(strings.Join(pathElements[1:], "/"), "/")
if targetOwner == "www" { if targetOwner == "www" {
// www.codeberg.page redirects to codeberg.page // TODO: rm hardcoded - use cname? // www.codeberg.page redirects to codeberg.page // TODO: rm hardcoded - use cname?
ctx.Redirect("https://"+string(mainDomainSuffix[1:])+string(ctx.Path()), fasthttp.StatusPermanentRedirect) ctx.Redirect("https://"+string(mainDomainSuffix[1:])+string(ctx.Path()), http.StatusPermanentRedirect)
return return
} }
@ -190,22 +196,24 @@ func Handler(mainDomainSuffix, rawDomain []byte,
if len(pathElements) > 1 && strings.HasPrefix(pathElements[1], "@") { if len(pathElements) > 1 && strings.HasPrefix(pathElements[1], "@") {
if targetRepo == "pages" { if targetRepo == "pages" {
// example.codeberg.org/pages/@... redirects to example.codeberg.org/@... // example.codeberg.org/pages/@... redirects to example.codeberg.org/@...
ctx.Redirect("/"+strings.Join(pathElements[1:], "/"), fasthttp.StatusTemporaryRedirect) ctx.Redirect("/"+strings.Join(pathElements[1:], "/"), http.StatusTemporaryRedirect)
return return
} }
log.Debug().Msg("Preparing main domain, now trying with specified repo & branch") log.Debug().Msg("main domain preparations, now trying with specified repo & branch")
branch := pathElements[1][1:]
if tryBranch(log, if tryBranch(log,
pathElements[0], pathElements[1][1:], pathElements[2:], pathElements[0], branch, pathElements[2:],
"/"+pathElements[0]+"/%p", "/"+pathElements[0]+"/%p",
) { ) {
log.Info().Msg("tryBranch, now trying upstream 3") log.Debug().Msg("tryBranch, now trying upstream 3")
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost,
targetOptions, targetOwner, targetRepo, targetBranch, targetPath, targetOptions, targetOwner, targetRepo, targetBranch, targetPath,
canonicalDomainCache, branchTimestampCache, fileResponseCache) canonicalDomainCache)
} else { } else {
log.Warn().Msg("tryBranch: upstream 3 failed") html.ReturnErrorPage(ctx,
html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency) fmt.Sprintf("explizite set branch %q do not exist at '%s/%s'", branch, targetOwner, targetRepo),
http.StatusFailedDependency)
} }
return return
} }
@ -213,16 +221,18 @@ func Handler(mainDomainSuffix, rawDomain []byte,
// Check if the first directory is a branch for the "pages" repo // Check if the first directory is a branch for the "pages" repo
// example.codeberg.page/@main/index.html // example.codeberg.page/@main/index.html
if strings.HasPrefix(pathElements[0], "@") { if strings.HasPrefix(pathElements[0], "@") {
log.Debug().Msg("Preparing main domain, now trying with specified branch") log.Debug().Msg("main domain preparations, now trying with specified branch")
branch := pathElements[0][1:]
if tryBranch(log, if tryBranch(log,
"pages", pathElements[0][1:], pathElements[1:], "/%p") { "pages", branch, pathElements[1:], "/%p") {
log.Info().Msg("tryBranch, now trying upstream 4") log.Debug().Msg("tryBranch, now trying upstream 4")
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost,
targetOptions, targetOwner, targetRepo, targetBranch, targetPath, targetOptions, targetOwner, "pages", targetBranch, targetPath,
canonicalDomainCache, branchTimestampCache, fileResponseCache) canonicalDomainCache)
} else { } else {
log.Warn().Msg("tryBranch: upstream 4 failed") html.ReturnErrorPage(ctx,
html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency) fmt.Sprintf("explizite set branch %q do not exist at '%s/%s'", branch, targetOwner, "pages"),
http.StatusFailedDependency)
} }
return return
} }
@ -233,10 +243,10 @@ func Handler(mainDomainSuffix, rawDomain []byte,
log.Debug().Msg("main domain preparations, now trying with specified repo") log.Debug().Msg("main domain preparations, now trying with specified repo")
if pathElements[0] != "pages" && tryBranch(log, if pathElements[0] != "pages" && tryBranch(log,
pathElements[0], "pages", pathElements[1:], "") { pathElements[0], "pages", pathElements[1:], "") {
log.Info().Msg("tryBranch, now trying upstream 5") log.Debug().Msg("tryBranch, now trying upstream 5")
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost,
targetOptions, targetOwner, targetRepo, targetBranch, targetPath, targetOptions, targetOwner, targetRepo, targetBranch, targetPath,
canonicalDomainCache, branchTimestampCache, fileResponseCache) canonicalDomainCache)
return return
} }
@ -245,28 +255,31 @@ func Handler(mainDomainSuffix, rawDomain []byte,
log.Debug().Msg("main domain preparations, now trying with default repo/branch") log.Debug().Msg("main domain preparations, now trying with default repo/branch")
if tryBranch(log, if tryBranch(log,
"pages", "", pathElements, "") { "pages", "", pathElements, "") {
log.Info().Msg("tryBranch, now trying upstream 6") log.Debug().Msg("tryBranch, now trying upstream 6")
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost,
targetOptions, targetOwner, targetRepo, targetBranch, targetPath, targetOptions, targetOwner, targetRepo, targetBranch, targetPath,
canonicalDomainCache, branchTimestampCache, fileResponseCache) canonicalDomainCache)
return return
} }
// Couldn't find a valid repo/branch // Couldn't find a valid repo/branch
html.ReturnErrorPage(ctx,
html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency) fmt.Sprintf("couldn't find a valid repo[%s]/branch[%s]", targetRepo, targetBranch),
http.StatusFailedDependency)
return return
} else { } else {
trimmedHostStr := string(trimmedHost) trimmedHostStr := string(trimmedHost)
// Serve pages from external domains // Serve pages from custom domains
targetOwner, targetRepo, targetBranch = dns.GetTargetFromDNS(trimmedHostStr, string(mainDomainSuffix), dnsLookupCache) targetOwner, targetRepo, targetBranch = dns.GetTargetFromDNS(trimmedHostStr, string(mainDomainSuffix), dnsLookupCache)
if targetOwner == "" { if targetOwner == "" {
html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency) html.ReturnErrorPage(ctx,
"could not obtain repo owner from custom domain",
http.StatusFailedDependency)
return return
} }
pathElements := strings.Split(string(bytes.Trim(ctx.Request.URI().Path(), "/")), "/") pathElements := strings.Split(strings.Trim(ctx.Path(), "/"), "/")
canonicalLink := "" canonicalLink := ""
if strings.HasPrefix(pathElements[0], "@") { if strings.HasPrefix(pathElements[0], "@") {
targetBranch = pathElements[0][1:] targetBranch = pathElements[0][1:]
@ -275,36 +288,33 @@ func Handler(mainDomainSuffix, rawDomain []byte,
} }
// Try to use the given repo on the given branch or the default branch // Try to use the given repo on the given branch or the default branch
log.Debug().Msg("Preparing custom domain, now trying with details from DNS") log.Debug().Msg("custom domain preparations, now trying with details from DNS")
if tryBranch(log, if tryBranch(log,
targetRepo, targetBranch, pathElements, canonicalLink) { targetRepo, targetBranch, pathElements, canonicalLink) {
canonicalDomain, valid := upstream.CheckCanonicalDomain(giteaClient, targetOwner, targetRepo, targetBranch, trimmedHostStr, string(mainDomainSuffix), canonicalDomainCache) canonicalDomain, valid := upstream.CheckCanonicalDomain(giteaClient, targetOwner, targetRepo, targetBranch, trimmedHostStr, string(mainDomainSuffix), canonicalDomainCache)
if !valid { if !valid {
log.Warn().Msg("Custom domains, domain from DNS isn't valid/canonical") html.ReturnErrorPage(ctx, "domain not specified in <code>.domains</code> file", http.StatusMisdirectedRequest)
html.ReturnErrorPage(ctx, fasthttp.StatusMisdirectedRequest)
return return
} else if canonicalDomain != trimmedHostStr { } else if canonicalDomain != trimmedHostStr {
// only redirect if the target is also a codeberg page! // only redirect if the target is also a codeberg page!
targetOwner, _, _ = dns.GetTargetFromDNS(strings.SplitN(canonicalDomain, "/", 2)[0], string(mainDomainSuffix), dnsLookupCache) targetOwner, _, _ = dns.GetTargetFromDNS(strings.SplitN(canonicalDomain, "/", 2)[0], string(mainDomainSuffix), dnsLookupCache)
if targetOwner != "" { if targetOwner != "" {
ctx.Redirect("https://"+canonicalDomain+string(ctx.RequestURI()), fasthttp.StatusTemporaryRedirect) ctx.Redirect("https://"+canonicalDomain+string(ctx.Path()), http.StatusTemporaryRedirect)
return return
} }
log.Warn().Msg("Custom domains, targetOwner from DNS is empty") html.ReturnErrorPage(ctx, "target is no codeberg page", http.StatusFailedDependency)
html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency)
return return
} }
log.Info().Msg("tryBranch, now trying upstream 7") log.Debug().Msg("tryBranch, now trying upstream 7")
tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost, tryUpstream(ctx, giteaClient, mainDomainSuffix, trimmedHost,
targetOptions, targetOwner, targetRepo, targetBranch, targetPath, targetOptions, targetOwner, targetRepo, targetBranch, targetPath,
canonicalDomainCache, branchTimestampCache, fileResponseCache) canonicalDomainCache)
return return
} }
log.Warn().Msg("Couldn't handle request, none of the options succeed") html.ReturnErrorPage(ctx, "could not find target for custom domain", http.StatusFailedDependency)
html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency)
return return
} }
} }

View file

@ -1,44 +1,42 @@
package server package server
import ( import (
"fmt" "net/http/httptest"
"testing" "testing"
"time" "time"
"github.com/valyala/fasthttp"
"codeberg.org/codeberg/pages/server/cache" "codeberg.org/codeberg/pages/server/cache"
"codeberg.org/codeberg/pages/server/gitea" "codeberg.org/codeberg/pages/server/gitea"
"github.com/rs/zerolog/log"
) )
func TestHandlerPerformance(t *testing.T) { func TestHandlerPerformance(t *testing.T) {
giteaRoot := "https://codeberg.org" giteaRoot := "https://codeberg.org"
giteaClient, _ := gitea.NewClient(giteaRoot, "", false, false) giteaClient, _ := gitea.NewClient(giteaRoot, "", cache.NewKeyValueCache(), false, false)
testHandler := Handler( testHandler := Handler(
[]byte("codeberg.page"), []byte("raw.codeberg.org"), "codeberg.page", "raw.codeberg.org",
giteaClient, giteaClient,
giteaRoot, "https://docs.codeberg.org/pages/raw-content/", giteaRoot, "https://docs.codeberg.org/pages/raw-content/",
[][]byte{[]byte("/.well-known/acme-challenge/")}, []string{"/.well-known/acme-challenge/"},
[][]byte{[]byte("raw.codeberg.org"), []byte("fonts.codeberg.org"), []byte("design.codeberg.org")}, []string{"raw.codeberg.org", "fonts.codeberg.org", "design.codeberg.org"},
cache.NewKeyValueCache(),
cache.NewKeyValueCache(),
cache.NewKeyValueCache(), cache.NewKeyValueCache(),
cache.NewKeyValueCache(), cache.NewKeyValueCache(),
) )
testCase := func(uri string, status int) { testCase := func(uri string, status int) {
ctx := &fasthttp.RequestCtx{ req := httptest.NewRequest("GET", uri, nil)
Request: *fasthttp.AcquireRequest(), w := httptest.NewRecorder()
Response: *fasthttp.AcquireResponse(),
} log.Printf("Start: %v\n", time.Now())
ctx.Request.SetRequestURI(uri)
fmt.Printf("Start: %v\n", time.Now())
start := time.Now() start := time.Now()
testHandler(ctx) testHandler(w, req)
end := time.Now() end := time.Now()
fmt.Printf("Done: %v\n", time.Now()) log.Printf("Done: %v\n", time.Now())
if ctx.Response.StatusCode() != status {
t.Errorf("request failed with status code %d", ctx.Response.StatusCode()) resp := w.Result()
if resp.StatusCode != status {
t.Errorf("request failed with status code %d", resp.StatusCode)
} else { } else {
t.Logf("request took %d milliseconds", end.Sub(start).Milliseconds()) t.Logf("request took %d milliseconds", end.Sub(start).Milliseconds())
} }

View file

@ -1,13 +1,13 @@
package server package server
import ( import (
"bytes" "strings"
) )
// GetHSTSHeader returns a HSTS header with includeSubdomains & preload for MainDomainSuffix and RawDomain, or an empty // getHSTSHeader returns a HSTS header with includeSubdomains & preload for MainDomainSuffix and RawDomain, or an empty
// string for custom domains. // string for custom domains.
func GetHSTSHeader(host, mainDomainSuffix, rawDomain []byte) string { func getHSTSHeader(host, mainDomainSuffix, rawDomain string) string {
if bytes.HasSuffix(host, mainDomainSuffix) || bytes.Equal(host, rawDomain) { if strings.HasSuffix(host, mainDomainSuffix) || strings.EqualFold(host, rawDomain) {
return "max-age=63072000; includeSubdomains; preload" return "max-age=63072000; includeSubdomains; preload"
} else { } else {
return "" return ""

View file

@ -1,53 +1,27 @@
package server package server
import ( import (
"bytes"
"fmt"
"net/http" "net/http"
"time" "strings"
"github.com/rs/zerolog/log"
"github.com/valyala/fasthttp"
"codeberg.org/codeberg/pages/server/cache" "codeberg.org/codeberg/pages/server/cache"
"codeberg.org/codeberg/pages/server/context"
"codeberg.org/codeberg/pages/server/utils" "codeberg.org/codeberg/pages/server/utils"
) )
type fasthttpLogger struct{} func SetupHTTPACMEChallengeServer(challengeCache cache.SetGetKey) http.HandlerFunc {
challengePath := "/.well-known/acme-challenge/"
func (fasthttpLogger) Printf(format string, args ...interface{}) { return func(w http.ResponseWriter, req *http.Request) {
log.Printf("FastHTTP: %s", fmt.Sprintf(format, args...)) 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)))
func SetupServer(handler fasthttp.RequestHandler) *fasthttp.Server { if !ok || challenge == nil {
// Enable compression by wrapping the handler with the compression function provided by FastHTTP ctx.String("no challenge for this token", http.StatusNotFound)
compressedHandler := fasthttp.CompressHandlerBrotliLevel(handler, fasthttp.CompressBrotliBestSpeed, fasthttp.CompressBestSpeed)
return &fasthttp.Server{
Handler: compressedHandler,
DisablePreParseMultipartForm: true,
NoDefaultServerHeader: true,
NoDefaultDate: true,
ReadTimeout: 30 * time.Second, // needs to be this high for ACME certificates with ZeroSSL & HTTP-01 challenge
Logger: fasthttpLogger{},
}
}
func SetupHTTPACMEChallengeServer(challengeCache cache.SetGetKey) *fasthttp.Server {
challengePath := []byte("/.well-known/acme-challenge/")
return &fasthttp.Server{
Handler: func(ctx *fasthttp.RequestCtx) {
if bytes.HasPrefix(ctx.Path(), challengePath) {
challenge, ok := challengeCache.Get(string(utils.TrimHostPort(ctx.Host())) + "/" + string(bytes.TrimPrefix(ctx.Path(), challengePath)))
if !ok || challenge == nil {
ctx.SetStatusCode(http.StatusNotFound)
ctx.SetBodyString("no challenge for this token")
}
ctx.SetBodyString(challenge.(string))
} else {
ctx.Redirect("https://"+string(ctx.Host())+string(ctx.RequestURI()), http.StatusMovedPermanently)
} }
}, ctx.String(challenge.(string))
} else {
ctx.Redirect("https://"+string(ctx.Host())+string(ctx.Path()), http.StatusMovedPermanently)
}
} }
} }

View file

@ -1,38 +1,37 @@
package server package server
import ( import (
"bytes" "net/http"
"strings" "strings"
"github.com/valyala/fasthttp"
"codeberg.org/codeberg/pages/html" "codeberg.org/codeberg/pages/html"
"codeberg.org/codeberg/pages/server/cache" "codeberg.org/codeberg/pages/server/cache"
"codeberg.org/codeberg/pages/server/context"
"codeberg.org/codeberg/pages/server/gitea" "codeberg.org/codeberg/pages/server/gitea"
"codeberg.org/codeberg/pages/server/upstream" "codeberg.org/codeberg/pages/server/upstream"
) )
// tryUpstream forwards the target request to the Gitea API, and shows an error page on failure. // tryUpstream forwards the target request to the Gitea API, and shows an error page on failure.
func tryUpstream(ctx *fasthttp.RequestCtx, giteaClient *gitea.Client, func tryUpstream(ctx *context.Context, giteaClient *gitea.Client,
mainDomainSuffix, trimmedHost []byte, mainDomainSuffix, trimmedHost string,
targetOptions *upstream.Options, targetOptions *upstream.Options,
targetOwner, targetRepo, targetBranch, targetPath string, targetOwner, targetRepo, targetBranch, targetPath string,
canonicalDomainCache, branchTimestampCache, fileResponseCache cache.SetGetKey, canonicalDomainCache cache.SetGetKey,
) { ) {
// check if a canonical domain exists on a request on MainDomain // check if a canonical domain exists on a request on MainDomain
if bytes.HasSuffix(trimmedHost, mainDomainSuffix) { if strings.HasSuffix(trimmedHost, mainDomainSuffix) {
canonicalDomain, _ := upstream.CheckCanonicalDomain(giteaClient, targetOwner, targetRepo, targetBranch, "", string(mainDomainSuffix), canonicalDomainCache) canonicalDomain, _ := upstream.CheckCanonicalDomain(giteaClient, targetOwner, targetRepo, targetBranch, "", string(mainDomainSuffix), canonicalDomainCache)
if !strings.HasSuffix(strings.SplitN(canonicalDomain, "/", 2)[0], string(mainDomainSuffix)) { if !strings.HasSuffix(strings.SplitN(canonicalDomain, "/", 2)[0], string(mainDomainSuffix)) {
canonicalPath := string(ctx.RequestURI()) canonicalPath := ctx.Req.RequestURI
if targetRepo != "pages" { if targetRepo != "pages" {
path := strings.SplitN(canonicalPath, "/", 3) path := strings.SplitN(canonicalPath, "/", 3)
if len(path) >= 3 { if len(path) >= 3 {
canonicalPath = "/" + path[2] canonicalPath = "/" + path[2]
} }
} }
ctx.Redirect("https://"+canonicalDomain+canonicalPath, fasthttp.StatusTemporaryRedirect) ctx.Redirect("https://"+canonicalDomain+canonicalPath, http.StatusTemporaryRedirect)
return return
} }
} }
@ -44,7 +43,7 @@ func tryUpstream(ctx *fasthttp.RequestCtx, giteaClient *gitea.Client,
targetOptions.Host = string(trimmedHost) targetOptions.Host = string(trimmedHost)
// Try to request the file from the Gitea API // Try to request the file from the Gitea API
if !targetOptions.Upstream(ctx, giteaClient, branchTimestampCache, fileResponseCache) { if !targetOptions.Upstream(ctx, giteaClient) {
html.ReturnErrorPage(ctx, ctx.Response.StatusCode()) html.ReturnErrorPage(ctx, "", ctx.StatusCode)
} }
} }

View file

@ -1,24 +0,0 @@
package upstream
import "time"
// defaultBranchCacheTimeout specifies the timeout for the default branch cache. It can be quite long.
var defaultBranchCacheTimeout = 15 * time.Minute
// branchExistenceCacheTimeout specifies the timeout for the branch timestamp & existence cache. It should be shorter
// than fileCacheTimeout, as that gets invalidated if the branch timestamp has changed. That way, repo changes will be
// picked up faster, while still allowing the content to be cached longer if nothing changes.
var branchExistenceCacheTimeout = 5 * time.Minute
// fileCacheTimeout specifies the timeout for the file content cache - you might want to make this quite long, depending
// on your available memory.
// TODO: move as option into cache interface
var fileCacheTimeout = 5 * time.Minute
// fileCacheSizeLimit limits the maximum file size that will be cached, and is set to 1 MB by default.
var fileCacheSizeLimit = 1024 * 1024
// canonicalDomainCacheTimeout specifies the timeout for the canonical domain cache.
var canonicalDomainCacheTimeout = 15 * time.Minute
const canonicalDomainConfig = ".domains"

View file

@ -2,11 +2,19 @@ package upstream
import ( import (
"strings" "strings"
"time"
"github.com/rs/zerolog/log"
"codeberg.org/codeberg/pages/server/cache" "codeberg.org/codeberg/pages/server/cache"
"codeberg.org/codeberg/pages/server/gitea" "codeberg.org/codeberg/pages/server/gitea"
) )
// canonicalDomainCacheTimeout specifies the timeout for the canonical domain cache.
var canonicalDomainCacheTimeout = 15 * time.Minute
const canonicalDomainConfig = ".domains"
// CheckCanonicalDomain returns the canonical domain specified in the repo (using the `.domains` file). // CheckCanonicalDomain returns the canonical domain specified in the repo (using the `.domains` file).
func CheckCanonicalDomain(giteaClient *gitea.Client, targetOwner, targetRepo, targetBranch, actualDomain, mainDomainSuffix string, canonicalDomainCache cache.SetGetKey) (string, bool) { func CheckCanonicalDomain(giteaClient *gitea.Client, targetOwner, targetRepo, targetBranch, actualDomain, mainDomainSuffix string, canonicalDomainCache cache.SetGetKey) (string, bool) {
var ( var (
@ -36,6 +44,8 @@ func CheckCanonicalDomain(giteaClient *gitea.Client, targetOwner, targetRepo, ta
valid = true valid = true
} }
} }
} else {
log.Info().Err(err).Msgf("could not read %s of %s/%s", canonicalDomainConfig, targetOwner, targetRepo)
} }
domains = append(domains, targetOwner+mainDomainSuffix) domains = append(domains, targetOwner+mainDomainSuffix)
if domains[len(domains)-1] == actualDomain { if domains[len(domains)-1] == actualDomain {

View file

@ -1,84 +1,36 @@
package upstream package upstream
import ( import (
"mime" "errors"
"path"
"strconv"
"strings"
"time"
"codeberg.org/codeberg/pages/server/cache"
"codeberg.org/codeberg/pages/server/gitea"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
)
type branchTimestamp struct { "codeberg.org/codeberg/pages/server/gitea"
Branch string )
Timestamp time.Time
}
// GetBranchTimestamp finds the default branch (if branch is "") and returns the last modification time of the branch // GetBranchTimestamp finds the default branch (if branch is "") and returns the last modification time of the branch
// (or nil if the branch doesn't exist) // (or nil if the branch doesn't exist)
func GetBranchTimestamp(giteaClient *gitea.Client, owner, repo, branch string, branchTimestampCache cache.SetGetKey) *branchTimestamp { func GetBranchTimestamp(giteaClient *gitea.Client, owner, repo, branch string) *gitea.BranchTimestamp {
log := log.With().Strs("BranchInfo", []string{owner, repo, branch}).Logger() log := log.With().Strs("BranchInfo", []string{owner, repo, branch}).Logger()
if result, ok := branchTimestampCache.Get(owner + "/" + repo + "/" + branch); ok {
if result == nil {
log.Debug().Msg("branchTimestampCache found item, but result is empty")
return nil
}
log.Debug().Msg("branchTimestampCache found item, returning result")
return result.(*branchTimestamp)
}
result := &branchTimestamp{
Branch: branch,
}
if len(branch) == 0 { if len(branch) == 0 {
// Get default branch // Get default branch
defaultBranch, err := giteaClient.GiteaGetRepoDefaultBranch(owner, repo) defaultBranch, err := giteaClient.GiteaGetRepoDefaultBranch(owner, repo)
if err != nil { if err != nil {
log.Err(err).Msg("Could't fetch default branch from repository") log.Err(err).Msg("Could't fetch default branch from repository")
_ = branchTimestampCache.Set(owner+"/"+repo+"/", nil, defaultBranchCacheTimeout)
return nil return nil
} }
log.Debug().Msg("Succesfully fetched default branch from Gitea") log.Debug().Msgf("Succesfully fetched default branch %q from Gitea", defaultBranch)
result.Branch = defaultBranch branch = defaultBranch
} }
timestamp, err := giteaClient.GiteaGetRepoBranchTimestamp(owner, repo, result.Branch) timestamp, err := giteaClient.GiteaGetRepoBranchTimestamp(owner, repo, branch)
if err != nil { if err != nil {
log.Err(err).Msg("Could not get latest commit's timestamp from branch") if !errors.Is(err, gitea.ErrorNotFound) {
log.Error().Err(err).Msg("Could not get latest commit's timestamp from branch")
}
return nil return nil
} }
log.Debug().Msg("Succesfully fetched latest commit's timestamp from branch, adding to cache") log.Debug().Msgf("Succesfully fetched latest commit's timestamp from branch: %#v", timestamp)
result.Timestamp = timestamp return timestamp
_ = branchTimestampCache.Set(owner+"/"+repo+"/"+branch, result, branchExistenceCacheTimeout)
return result
}
func (o *Options) getMimeTypeByExtension() string {
if o.ForbiddenMimeTypes == nil {
o.ForbiddenMimeTypes = make(map[string]bool)
}
mimeType := mime.TypeByExtension(path.Ext(o.TargetPath))
mimeTypeSplit := strings.SplitN(mimeType, ";", 2)
if o.ForbiddenMimeTypes[mimeTypeSplit[0]] || mimeType == "" {
if o.DefaultMimeType != "" {
mimeType = o.DefaultMimeType
} else {
mimeType = "application/octet-stream"
}
}
return mimeType
}
func (o *Options) generateUri() string {
return path.Join(o.TargetOwner, o.TargetRepo, "raw", o.TargetBranch, o.TargetPath)
}
func (o *Options) generateUriClientArgs() (targetOwner, targetRepo, ref, resource string) {
return o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath
}
func (o *Options) timestamp() string {
return strconv.FormatInt(o.BranchTimestamp.Unix(), 10)
} }

View file

@ -1,20 +1,27 @@
package upstream package upstream
import ( import (
"bytes"
"errors" "errors"
"fmt"
"io" "io"
"net/http"
"strings" "strings"
"time" "time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/valyala/fasthttp"
"codeberg.org/codeberg/pages/html" "codeberg.org/codeberg/pages/html"
"codeberg.org/codeberg/pages/server/cache" "codeberg.org/codeberg/pages/server/context"
"codeberg.org/codeberg/pages/server/gitea" "codeberg.org/codeberg/pages/server/gitea"
) )
const (
headerLastModified = "Last-Modified"
headerIfModifiedSince = "If-Modified-Since"
rawMime = "text/plain; charset=utf-8"
)
// upstreamIndexPages lists pages that may be considered as index pages for directories. // upstreamIndexPages lists pages that may be considered as index pages for directories.
var upstreamIndexPages = []string{ var upstreamIndexPages = []string{
"index.html", "index.html",
@ -35,61 +42,61 @@ type Options struct {
// Used for debugging purposes. // Used for debugging purposes.
Host string Host string
DefaultMimeType string TryIndexPages bool
ForbiddenMimeTypes map[string]bool BranchTimestamp time.Time
TryIndexPages bool
BranchTimestamp time.Time
// internal // internal
appendTrailingSlash bool appendTrailingSlash bool
redirectIfExists string redirectIfExists string
ServeRaw bool
} }
// Upstream requests a file from the Gitea API at GiteaRoot and writes it to the request context. // Upstream requests a file from the Gitea API at GiteaRoot and writes it to the request context.
func (o *Options) Upstream(ctx *fasthttp.RequestCtx, giteaClient *gitea.Client, branchTimestampCache, fileResponseCache cache.SetGetKey) (final bool) { func (o *Options) Upstream(ctx *context.Context, giteaClient *gitea.Client) (final bool) {
log := log.With().Strs("upstream", []string{o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath, o.Host}).Logger() log := log.With().Strs("upstream", []string{o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath}).Logger()
if o.TargetOwner == "" || o.TargetRepo == "" {
html.ReturnErrorPage(ctx, "either repo owner or name info is missing", http.StatusBadRequest)
return true
}
// Check if the branch exists and when it was modified // Check if the branch exists and when it was modified
if o.BranchTimestamp.IsZero() { if o.BranchTimestamp.IsZero() {
branch := GetBranchTimestamp(giteaClient, o.TargetOwner, o.TargetRepo, o.TargetBranch, branchTimestampCache) branch := GetBranchTimestamp(giteaClient, o.TargetOwner, o.TargetRepo, o.TargetBranch)
if branch == nil { if branch == nil || branch.Branch == "" {
html.ReturnErrorPage(ctx, fasthttp.StatusFailedDependency) html.ReturnErrorPage(ctx,
fmt.Sprintf("could not get timestamp of branch %q", o.TargetBranch),
http.StatusFailedDependency)
return true return true
} }
o.TargetBranch = branch.Branch o.TargetBranch = branch.Branch
o.BranchTimestamp = branch.Timestamp o.BranchTimestamp = branch.Timestamp
} }
if o.TargetOwner == "" || o.TargetRepo == "" || o.TargetBranch == "" {
html.ReturnErrorPage(ctx, fasthttp.StatusBadRequest)
return true
}
// Check if the browser has a cached version // Check if the browser has a cached version
if ifModifiedSince, err := time.Parse(time.RFC1123, string(ctx.Request.Header.Peek("If-Modified-Since"))); err == nil { if ctx.Response() != nil {
if !ifModifiedSince.Before(o.BranchTimestamp) { if ifModifiedSince, err := time.Parse(time.RFC1123, string(ctx.Response().Header.Get(headerIfModifiedSince))); err == nil {
ctx.Response.SetStatusCode(fasthttp.StatusNotModified) if !ifModifiedSince.Before(o.BranchTimestamp) {
return true ctx.RespWriter.WriteHeader(http.StatusNotModified)
log.Trace().Msg("check response against last modified: valid")
return true
}
} }
log.Trace().Msg("check response against last modified: outdated")
} }
log.Debug().Msg("Preparing") log.Debug().Msg("Preparing")
// Make a GET request to the upstream URL reader, header, statusCode, err := giteaClient.ServeRawContent(o.TargetOwner, o.TargetRepo, o.TargetBranch, o.TargetPath)
uri := o.generateUri() if reader != nil {
var res *fasthttp.Response defer reader.Close()
var cachedResponse gitea.FileResponse
var err error
if cachedValue, ok := fileResponseCache.Get(uri + "?timestamp=" + o.timestamp()); ok && !cachedValue.(gitea.FileResponse).IsEmpty() {
cachedResponse = cachedValue.(gitea.FileResponse)
} else {
res, err = giteaClient.ServeRawContent(o.generateUriClientArgs())
} }
log.Debug().Msg("Aquisting") log.Debug().Msg("Aquisting")
// Handle errors // Handle not found error
if (err != nil && errors.Is(err, gitea.ErrorNotFound)) || (res == nil && !cachedResponse.Exists) { if err != nil && errors.Is(err, gitea.ErrorNotFound) {
if o.TryIndexPages { if o.TryIndexPages {
// copy the o struct & try if an index page exists // copy the o struct & try if an index page exists
optionsForIndexPages := *o optionsForIndexPages := *o
@ -97,25 +104,20 @@ func (o *Options) Upstream(ctx *fasthttp.RequestCtx, giteaClient *gitea.Client,
optionsForIndexPages.appendTrailingSlash = true optionsForIndexPages.appendTrailingSlash = true
for _, indexPage := range upstreamIndexPages { for _, indexPage := range upstreamIndexPages {
optionsForIndexPages.TargetPath = strings.TrimSuffix(o.TargetPath, "/") + "/" + indexPage optionsForIndexPages.TargetPath = strings.TrimSuffix(o.TargetPath, "/") + "/" + indexPage
if optionsForIndexPages.Upstream(ctx, giteaClient, branchTimestampCache, fileResponseCache) { if optionsForIndexPages.Upstream(ctx, giteaClient) {
_ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), gitea.FileResponse{
Exists: false,
}, fileCacheTimeout)
return true return true
} }
} }
// compatibility fix for GitHub Pages (/example → /example.html) // compatibility fix for GitHub Pages (/example → /example.html)
optionsForIndexPages.appendTrailingSlash = false optionsForIndexPages.appendTrailingSlash = false
optionsForIndexPages.redirectIfExists = strings.TrimSuffix(string(ctx.Request.URI().Path()), "/") + ".html" optionsForIndexPages.redirectIfExists = strings.TrimSuffix(ctx.Path(), "/") + ".html"
optionsForIndexPages.TargetPath = o.TargetPath + ".html" optionsForIndexPages.TargetPath = o.TargetPath + ".html"
if optionsForIndexPages.Upstream(ctx, giteaClient, branchTimestampCache, fileResponseCache) { if optionsForIndexPages.Upstream(ctx, giteaClient) {
_ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), gitea.FileResponse{
Exists: false,
}, fileCacheTimeout)
return true return true
} }
} }
ctx.Response.SetStatusCode(fasthttp.StatusNotFound)
ctx.StatusCode = http.StatusNotFound
if o.TryIndexPages { if o.TryIndexPages {
// copy the o struct & try if a not found page exists // copy the o struct & try if a not found page exists
optionsForNotFoundPages := *o optionsForNotFoundPages := *o
@ -123,94 +125,84 @@ func (o *Options) Upstream(ctx *fasthttp.RequestCtx, giteaClient *gitea.Client,
optionsForNotFoundPages.appendTrailingSlash = false optionsForNotFoundPages.appendTrailingSlash = false
for _, notFoundPage := range upstreamNotFoundPages { for _, notFoundPage := range upstreamNotFoundPages {
optionsForNotFoundPages.TargetPath = "/" + notFoundPage optionsForNotFoundPages.TargetPath = "/" + notFoundPage
if optionsForNotFoundPages.Upstream(ctx, giteaClient, branchTimestampCache, fileResponseCache) { if optionsForNotFoundPages.Upstream(ctx, giteaClient) {
_ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), gitea.FileResponse{
Exists: false,
}, fileCacheTimeout)
return true return true
} }
} }
} }
if res != nil {
// Update cache if the request is fresh
_ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), gitea.FileResponse{
Exists: false,
}, fileCacheTimeout)
}
return false return false
} }
if res != nil && (err != nil || res.StatusCode() != fasthttp.StatusOK) {
log.Warn().Msgf("Couldn't fetch contents from %q: %v (status code %d)", uri, err, res.StatusCode()) // handle unexpected client errors
html.ReturnErrorPage(ctx, fasthttp.StatusInternalServerError) if err != nil || reader == nil || statusCode != http.StatusOK {
log.Debug().Msg("Handling error")
var msg string
if err != nil {
msg = "gitea client returned unexpected error"
log.Error().Err(err).Msg(msg)
msg = fmt.Sprintf("%s: %v", msg, err)
}
if reader == nil {
msg = "gitea client returned no reader"
log.Error().Msg(msg)
}
if statusCode != http.StatusOK {
msg = fmt.Sprintf("Couldn't fetch contents (status code %d)", statusCode)
log.Error().Msg(msg)
}
html.ReturnErrorPage(ctx, msg, http.StatusInternalServerError)
return true return true
} }
// Append trailing slash if missing (for index files), and redirect to fix filenames in general // Append trailing slash if missing (for index files), and redirect to fix filenames in general
// o.appendTrailingSlash is only true when looking for index pages // o.appendTrailingSlash is only true when looking for index pages
if o.appendTrailingSlash && !bytes.HasSuffix(ctx.Request.URI().Path(), []byte{'/'}) { if o.appendTrailingSlash && !strings.HasSuffix(ctx.Path(), "/") {
ctx.Redirect(string(ctx.Request.URI().Path())+"/", fasthttp.StatusTemporaryRedirect) ctx.Redirect(ctx.Path()+"/", http.StatusTemporaryRedirect)
return true return true
} }
if bytes.HasSuffix(ctx.Request.URI().Path(), []byte("/index.html")) { if strings.HasSuffix(ctx.Path(), "/index.html") {
ctx.Redirect(strings.TrimSuffix(string(ctx.Request.URI().Path()), "index.html"), fasthttp.StatusTemporaryRedirect) ctx.Redirect(strings.TrimSuffix(ctx.Path(), "index.html"), http.StatusTemporaryRedirect)
return true return true
} }
if o.redirectIfExists != "" { if o.redirectIfExists != "" {
ctx.Redirect(o.redirectIfExists, fasthttp.StatusTemporaryRedirect) ctx.Redirect(o.redirectIfExists, http.StatusTemporaryRedirect)
return true return true
} }
log.Debug().Msg("Handling error") // Set ETag & MIME
if eTag := header.Get(gitea.ETagHeader); eTag != "" {
// Set the MIME type ctx.RespWriter.Header().Set(gitea.ETagHeader, eTag)
mimeType := o.getMimeTypeByExtension()
ctx.Response.Header.SetContentType(mimeType)
// Set ETag
if cachedResponse.Exists {
ctx.Response.Header.SetBytesV(fasthttp.HeaderETag, cachedResponse.ETag)
} else if res != nil {
cachedResponse.ETag = res.Header.Peek(fasthttp.HeaderETag)
ctx.Response.Header.SetBytesV(fasthttp.HeaderETag, cachedResponse.ETag)
} }
if cacheIndicator := header.Get(gitea.PagesCacheIndicatorHeader); cacheIndicator != "" {
if ctx.Response.StatusCode() != fasthttp.StatusNotFound { ctx.RespWriter.Header().Set(gitea.PagesCacheIndicatorHeader, cacheIndicator)
// Everything's okay so far
ctx.Response.SetStatusCode(fasthttp.StatusOK)
} }
ctx.Response.Header.SetLastModified(o.BranchTimestamp) if length := header.Get(gitea.ContentLengthHeader); length != "" {
ctx.RespWriter.Header().Set(gitea.ContentLengthHeader, length)
}
if mime := header.Get(gitea.ContentTypeHeader); mime == "" || o.ServeRaw {
ctx.RespWriter.Header().Set(gitea.ContentTypeHeader, rawMime)
} else {
ctx.RespWriter.Header().Set(gitea.ContentTypeHeader, mime)
}
ctx.RespWriter.Header().Set(headerLastModified, o.BranchTimestamp.In(time.UTC).Format(time.RFC1123))
log.Debug().Msg("Prepare response") log.Debug().Msg("Prepare response")
// Write the response body to the original request ctx.RespWriter.WriteHeader(ctx.StatusCode)
var cacheBodyWriter bytes.Buffer
if res != nil {
if res.Header.ContentLength() > fileCacheSizeLimit {
// fasthttp else will set "Content-Length: 0"
ctx.Response.SetBodyStream(&strings.Reader{}, -1)
err = res.BodyWriteTo(ctx.Response.BodyWriter()) // Write the response body to the original request
} else { if reader != nil {
// TODO: cache is half-empty if request is cancelled - does the ctx.Err() below do the trick? _, err := io.Copy(ctx.RespWriter, reader)
err = res.BodyWriteTo(io.MultiWriter(ctx.Response.BodyWriter(), &cacheBodyWriter)) if err != nil {
log.Error().Err(err).Msgf("Couldn't write body for %q", o.TargetPath)
html.ReturnErrorPage(ctx, "", http.StatusInternalServerError)
return true
} }
} else {
_, err = ctx.Write(cachedResponse.Body)
}
if err != nil {
log.Error().Err(err).Msgf("Couldn't write body for %q", uri)
html.ReturnErrorPage(ctx, fasthttp.StatusInternalServerError)
return true
} }
log.Debug().Msg("Sending response") log.Debug().Msg("Sending response")
if res != nil && res.Header.ContentLength() <= fileCacheSizeLimit && ctx.Err() == nil {
cachedResponse.Exists = true
cachedResponse.MimeType = mimeType
cachedResponse.Body = cacheBodyWriter.Bytes()
_ = fileResponseCache.Set(uri+"?timestamp="+o.timestamp(), cachedResponse, fileCacheTimeout)
}
return true return true
} }

View file

@ -1,9 +1,11 @@
package utils package utils
import "bytes" import (
"strings"
)
func TrimHostPort(host []byte) []byte { func TrimHostPort(host string) string {
i := bytes.IndexByte(host, ':') i := strings.IndexByte(host, ':')
if i >= 0 { if i >= 0 {
return host[:i] return host[:i]
} }

View file

@ -7,7 +7,7 @@ import (
) )
func TestTrimHostPort(t *testing.T) { func TestTrimHostPort(t *testing.T) {
assert.EqualValues(t, "aa", TrimHostPort([]byte("aa"))) assert.EqualValues(t, "aa", TrimHostPort("aa"))
assert.EqualValues(t, "", TrimHostPort([]byte(":"))) assert.EqualValues(t, "", TrimHostPort(":"))
assert.EqualValues(t, "example.com", TrimHostPort([]byte("example.com:80"))) assert.EqualValues(t, "example.com", TrimHostPort("example.com:80"))
} }