Compare commits

...

24 Commits

Author SHA1 Message Date
7d745fa08b Bump dependencies 2025-11-04 00:03:05 +01:00
b2a27cad72 feat: Do not send metrics if the client used DNT or GPC
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-09 21:02:17 +01:00
617b68f43e feat: Switch from Loki to something else
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-09 15:31:04 +01:00
4b4bc9792b feat: Only listen on http-port if we don't have ACME disabled
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-04 13:41:29 +01:00
aaab500049 fix: Always add authorization if a token is provided 2024-02-04 13:40:34 +01:00
fbb80c622f fix: Fix wrong logging output 2024-02-04 13:34:37 +01:00
48b6585eba feat: Allow reading an access token 2024-02-04 13:26:48 +01:00
3747f02ed8 fix: Fix style issue in test
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-03 16:45:13 +01:00
b9cc7f30e8 feat: Fix and test header parsing
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-02-03 16:42:08 +01:00
5b9aaf5e24 fix: Fix CNAME parsing 2024-02-03 15:51:23 +01:00
e3032c8233 feat: Move the CNAME into the rio.json 2024-02-03 15:39:31 +01:00
cf85380ddb feaT: Allow specifying custom headers in the rio.json 2024-02-03 15:18:37 +01:00
8630855374 fix: Use the correct domain for metrics
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-03 12:23:38 +01:00
315bb39f44 feat: Pass around one big global context
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-03 12:05:10 +01:00
cb123537d5 fix: Fix typo
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-02 21:36:58 +01:00
9abc268315 feat: Add more logging to the metrics code
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-02 21:28:16 +01:00
8f09aa959b feat: Implement simple page metrics
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-02 21:07:29 +01:00
0341ed8219 feat: Also send the content-length
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-01-14 19:29:09 +01:00
6028d3fe76 fix: Try to fix 'when' again
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-01-13 18:00:10 +01:00
f802efb6bd fix: Fix CI when
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-01-13 17:59:18 +01:00
183e77ea61 feat: Restrict the CI to Go files
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-01-13 17:58:21 +01:00
3af531fdcc docs: Document the alt CNAME approach 2024-01-13 17:54:02 +01:00
f264bd5604 feat: Allow usage for top-level domains
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-01-13 17:30:55 +01:00
9b971daf28 fix: Fix username extraction in the TLS handler 2024-01-11 22:10:32 +01:00
23 changed files with 3092 additions and 413 deletions

View File

@@ -4,8 +4,12 @@ steps:
commands:
- go build ./cmd/rio
- go test $(go list ./... | grep -v /vendor/)
when:
- path: "**/*.go"
lint:
image: "golang:1.21.5-alpine"
commands:
- go fmt $(go list ./... | grep -v /vendor/)
- go vet $(go list ./... | grep -v /vendor/)
- go vet $(go list ./... | grep -v /vendor/)
when:
- path: "**/*.go"

10
Containerfile Normal file
View File

@@ -0,0 +1,10 @@
FROM golang:1.25-alpine3.22 AS builder
COPY . /app
WORKDIR /app
RUN go build ./cmd/rio
FROM alpine:3.22
COPY --from=builder /app/rio /usr/local/bin/rio
ENTRYPOINT ["/usr/local/bin/rio"]

View File

@@ -72,6 +72,18 @@ _acme-challenge.cooldomain.rio. 3600 IN CNAME cooldomain.rio.pages.example
This additionally requires your repository to have a file named `CNAME` that contains
the domain to use (`cooldomain.rio` in this case).
### Alternate CNAME Domains
If there is a situation where you want to use a CNAME redirect on a (sub-) domain,
where having a CNAME is not feasible, you can configure an "alternate CNAME".
This is a special TXT record that contains the "CNAME" you want to specify.
```
; Example for Bind
cooldomain.rio. 3600 IN A <IPv4 of rio>
_rio-cname.cooldomain.rio. 3600 IN TXT "cname=user.pages.example.org"
```
### Repository Redirects
If you have multiple repositories with pages (and you use a CNAME), you can additionally

View File

@@ -7,13 +7,17 @@ import (
"net"
"net/http"
"os"
"regexp"
"strings"
"sync"
"time"
"git.polynom.me/rio/internal/acme"
"git.polynom.me/rio/internal/certificates"
"git.polynom.me/rio/internal/context"
"git.polynom.me/rio/internal/dns"
riogitea "git.polynom.me/rio/internal/gitea"
"git.polynom.me/rio/internal/metrics"
"git.polynom.me/rio/internal/pages"
"git.polynom.me/rio/internal/repo"
"git.polynom.me/rio/internal/server"
@@ -24,26 +28,14 @@ import (
"github.com/urfave/cli/v2"
)
// Extract the username from the domain name @domain that we're processing
// at the moment.
func extractUsername(pagesDomain, domain string) string {
suffixlessDomain := strings.TrimSuffix(domain, "."+pagesDomain)
usernameParts := strings.Split(suffixlessDomain, ".")
if len(usernameParts) == 1 {
return usernameParts[0]
}
return strings.Join(usernameParts, ".")
}
func handleSubdomain(pagesDomain, domain, cname, path, giteaUrl, defaultCsp string, giteaClient *repo.GiteaClient, w http.ResponseWriter) {
func handleSubdomain(ctx *context.GlobalContext, domain, cname, path string, req *http.Request, w http.ResponseWriter) {
username := ""
if cname != "" {
// If we are accessed via a CNAME, then CNAME contains our <user>.<pages domain> value.
username = extractUsername(pagesDomain, cname)
username = dns.ExtractUsername(ctx.PagesDomain, cname)
} else {
// If we are directly accessed, then domain contains our <user>.<pages domain> value.
username = extractUsername(pagesDomain, domain)
username = dns.ExtractUsername(ctx.PagesDomain, domain)
}
// Strip the leading /
@@ -64,7 +56,7 @@ func handleSubdomain(pagesDomain, domain, cname, path, giteaUrl, defaultCsp stri
domain,
cname,
path,
giteaClient,
ctx,
)
if err != nil {
log.Errorf("Failed to get repo: %s", err)
@@ -72,15 +64,27 @@ func handleSubdomain(pagesDomain, domain, cname, path, giteaUrl, defaultCsp stri
return
}
pages.ServeFile(username, repo.Name, path, defaultCsp, giteaClient, w)
c := &context.Context{
Username: username,
Reponame: repo.Name,
Domain: domain,
Path: path,
Referrer: req.Header.Get("Referer"),
UserAgent: req.Header.Get("User-Agent"),
DNT: req.Header.Get("DNT"),
GPC: req.Header.Get("Sec-GPC"),
Writer: w,
Global: ctx,
}
pages.ServeFile(c)
}
func Handler(pagesDomain, giteaUrl, defaultCsp string, giteaClient *repo.GiteaClient) http.HandlerFunc {
func Handler(ctx *context.GlobalContext) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Server", "rio")
// Is the direct domain requested?
if req.Host == pagesDomain {
if req.Host == ctx.PagesDomain {
log.Debug("Direct pages domain is requested.")
// TODO: Handle
@@ -89,9 +93,9 @@ func Handler(pagesDomain, giteaUrl, defaultCsp string, giteaClient *repo.GiteaCl
}
// Is a direct subdomain requested?
if strings.HasSuffix(req.Host, pagesDomain) {
if strings.HasSuffix(req.Host, ctx.PagesDomain) {
log.Debug("Domain can be directly handled")
handleSubdomain(pagesDomain, req.Host, "", req.URL.Path, giteaUrl, defaultCsp, giteaClient, w)
handleSubdomain(ctx, req.Host, "", req.URL.Path, req, w)
return
}
@@ -106,9 +110,9 @@ func Handler(pagesDomain, giteaUrl, defaultCsp string, giteaClient *repo.GiteaCl
// Is a direct subdomain requested after CNAME lookup?
// NOTE: We now require the leading dot because a CNAME to the direct
// pages domain makes no sense.
if strings.HasSuffix(cname, "."+pagesDomain) {
if strings.HasSuffix(cname, "."+ctx.PagesDomain) {
log.Debugf("%s is alias of %s and can be handled after a CNAME query", req.Host, cname)
handleSubdomain(pagesDomain, req.Host, cname, req.URL.Path, giteaUrl, defaultCsp, giteaClient, w)
handleSubdomain(ctx, req.Host, cname, req.URL.Path, req, w)
return
}
@@ -143,6 +147,9 @@ func runServer(ctx *cli.Context) error {
acmeDnsProvider := ctx.String("acme-dns-provider")
acmeDisable := ctx.Bool("acme-disable")
defaultCsp := ctx.String("default-csp")
metricsUrl := ctx.String("metrics-url")
metricsBotList := ctx.String("metrics-bot-list")
tokenFile := ctx.String("token-file")
// Init Logging
if ctx.Bool("debug") {
@@ -151,18 +158,50 @@ func runServer(ctx *cli.Context) error {
log.SetLevel(log.InfoLevel)
}
// Set up the Loki metrics
var lokiConfig metrics.MetricConfig
if metricsUrl == "" {
lokiConfig = metrics.MetricConfig{
Enabled: false,
}
} else {
var patterns []regexp.Regexp
if metricsBotList != "" {
patterns, _ = metrics.ReadBotPatterns(metricsBotList)
} else {
patterns = make([]regexp.Regexp, 0)
}
log.Infof("Read %d bot patterns from disk", len(patterns))
lokiConfig = metrics.MetricConfig{
Enabled: true,
BotUserAgents: &patterns,
Url: metricsUrl,
}
}
// If specified, read in an access token
token := ""
if tokenFile != "" {
t, err := readSecret(tokenFile)
if err != nil {
log.Warnf("Failed to read secret: %v", err)
}
token = t
}
// Setup the Gitea stuff
httpClient := http.Client{Timeout: 10 * time.Second}
giteaApiClient, err := gitea.NewClient(
giteaUrl,
gitea.SetHTTPClient(&httpClient),
gitea.SetToken(""),
gitea.SetToken(token),
gitea.SetUserAgent("rio"),
)
if err != nil {
return err
}
giteaClient := repo.NewGiteaClient(giteaUrl, giteaApiClient)
giteaClient := riogitea.NewGiteaClient(giteaUrl, token, giteaApiClient)
// Listen on the port
addr := ctx.String("listen-host") + ":" + ctx.String("listen-port")
@@ -173,14 +212,18 @@ func runServer(ctx *cli.Context) error {
return err
}
// Listen on the HTTP port
httpAddr := ctx.String("http-host") + ":" + ctx.String("http-port")
httpListener, err := net.Listen("tcp", httpAddr)
if err != nil {
fmt.Println(
fmt.Errorf("Failed to create HTTP listener: %v", err),
)
return err
// Prepare the context
cacheCtx := context.CacheContext{
RepositoryInformationCache: context.MakeRepoInfoCache(),
RepositoryPathCache: context.MakeRepoPathCache(),
UsernameCache: context.MakeUsernameCache(),
}
globalCtx := &context.GlobalContext{
DefaultCSP: defaultCsp,
PagesDomain: domain,
Gitea: &giteaClient,
MetricConfig: &lokiConfig,
Cache: &cacheCtx,
}
if !acmeDisable {
@@ -236,7 +279,7 @@ func runServer(ctx *cli.Context) error {
certsFile,
&cache,
acmeClient,
&giteaClient,
globalCtx,
)
listener = tls.NewListener(listener, tlsConfig)
}
@@ -251,14 +294,24 @@ func runServer(ctx *cli.Context) error {
go func() {
defer waitGroup.Done()
log.Debug("Listening on main HTTP server")
if err := http.Serve(listener, Handler(domain, giteaUrl, defaultCsp, &giteaClient)); err != nil {
log.Infof("Listening on main HTTP server %s", addr)
if err := http.Serve(listener, Handler(globalCtx)); err != nil {
log.Fatal(fmt.Errorf("Listening failed: %v", err))
}
log.Debug("Listening on main HTTP server done!")
}()
if !acmeDisable {
// Listen on the HTTP port
httpAddr := ctx.String("http-host") + ":" + ctx.String("http-port")
httpListener, err := net.Listen("tcp", httpAddr)
if err != nil {
fmt.Println(
fmt.Errorf("Failed to create HTTP listener: %v", err),
)
return err
}
go func() {
defer waitGroup.Done()
@@ -362,6 +415,24 @@ func main() {
Value: "",
EnvVars: []string{"DEFAULT_CSP"},
},
&cli.StringFlag{
Name: "metrics-url",
Usage: "The URL for metric pings",
Value: "",
EnvVars: []string{"METRICS_URL"},
},
&cli.StringFlag{
Name: "metrics-bot-list",
Usage: "File to read a list of regular expressions modelling bot user agents from",
Value: "",
EnvVars: []string{"METRICS_BOT_LIST"},
},
&cli.StringFlag{
Name: "token-file",
Usage: "File containing a access token. Required for serving private repositories",
Value: "",
EnvVars: []string{"TOKEN_FILE"},
},
},
}

16
cmd/rio/utils.go Normal file
View File

@@ -0,0 +1,16 @@
package main
import (
"os"
"strings"
)
// Read a secret file and return its (cleaned) content.
func readSecret(path string) (string, error) {
content, err := os.ReadFile(path)
if err != nil {
return "", err
}
return strings.Trim(string(content), "\n\r "), nil
}

298
go.mod
View File

@@ -1,162 +1,246 @@
module git.polynom.me/rio
go 1.20
go 1.24.7
require (
code.gitea.io/sdk/gitea v0.17.1
github.com/go-acme/lego/v4 v4.14.2
code.gitea.io/sdk/gitea v0.22.1
github.com/go-acme/lego/v4 v4.28.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/sirupsen/logrus v1.9.3
github.com/urfave/cli/v2 v2.27.1
github.com/urfave/cli/v2 v2.27.7
)
require (
cloud.google.com/go/compute v1.18.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute v1.49.1 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/42wim/httpsig v1.2.3 // indirect
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.24 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect
github.com/Azure/go-autorest/autorest v0.11.30 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.7 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.1 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect
github.com/Azure/go-autorest/logger v0.2.2 // indirect
github.com/Azure/go-autorest/tracing v0.6.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 // indirect
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/tea v1.3.13 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 // indirect
github.com/aliyun/credentials-go v1.4.8 // indirect
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
github.com/aws/aws-sdk-go-v2 v1.19.0 // indirect
github.com/aws/aws-sdk-go-v2/config v1.18.28 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.27 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.19.3 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/civo/civogo v0.3.11 // indirect
github.com/cloudflare/cloudflare-go v0.70.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.39.5 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.16 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.20 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.12 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.12 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.12 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.50.3 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.59.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.0 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.39.0 // indirect
github.com/aws/smithy-go v1.23.2 // indirect
github.com/aziontech/azionapi-go-sdk v0.143.0 // indirect
github.com/baidubce/bce-sdk-go v0.9.250 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/civo/civogo v0.6.4 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudflare/cloudflare-go v0.116.0 // indirect
github.com/cpu/goacmedns v0.1.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/deepmap/oapi-codegen v1.9.1 // indirect
github.com/deepmap/oapi-codegen v1.16.3 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/dnsimple/dnsimple-go v1.2.0 // indirect
github.com/exoscale/egoscale v0.100.1 // indirect
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
github.com/dnsimple/dnsimple-go/v4 v4.0.0 // indirect
github.com/exoscale/egoscale v0.102.4 // indirect
github.com/exoscale/egoscale/v3 v3.1.27 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-acme/alidns-20150109/v4 v4.6.1 // indirect
github.com/go-acme/tencentclouddnspod v1.1.25 // indirect
github.com/go-acme/tencentedgdeone v1.1.48 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/go-resty/resty/v2 v2.7.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/go-resty/resty/v2 v2.16.5 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
github.com/gophercloud/gophercloud v1.0.0 // indirect
github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/gophercloud/gophercloud v1.14.1 // indirect
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.4 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.174 // 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/v2 v2.11.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
github.com/labbsr0x/goh v1.0.1 // indirect
github.com/linode/linodego v1.17.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/linode/linodego v1.60.0 // indirect
github.com/liquidweb/go-lwApi v0.0.5 // indirect
github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
github.com/liquidweb/liquidweb-go v1.6.3 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/miekg/dns v1.1.55 // indirect
github.com/liquidweb/liquidweb-cli v0.7.0 // indirect
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
github.com/namedotcom/go/v4 v4.0.2 // indirect
github.com/nrdcg/auroradns v1.1.0 // indirect
github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 // indirect
github.com/nrdcg/desec v0.7.0 // indirect
github.com/nrdcg/bunny-go v0.1.0 // indirect
github.com/nrdcg/desec v0.11.1 // indirect
github.com/nrdcg/dnspod-go v0.4.0 // indirect
github.com/nrdcg/freemyip v0.2.0 // indirect
github.com/nrdcg/goinwx v0.8.2 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/nrdcg/freemyip v0.3.0 // indirect
github.com/nrdcg/goacmedns v0.2.0 // indirect
github.com/nrdcg/goinwx v0.11.0 // indirect
github.com/nrdcg/mailinabox v0.3.0 // indirect
github.com/nrdcg/namesilo v0.5.0 // indirect
github.com/nrdcg/nodion v0.1.0 // indirect
github.com/nrdcg/porkbun v0.2.0 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.103.0 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.103.0 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/nrdcg/vegadns v0.3.0 // indirect
github.com/nzdjb/go-metaname v1.0.0 // indirect
github.com/onsi/gomega v1.27.6 // indirect
github.com/onsi/gomega v1.37.0 // indirect
github.com/oracle/oci-go-sdk v24.3.0+incompatible // indirect
github.com/ovh/go-ovh v1.4.2 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/ovh/go-ovh v1.9.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/peterhellberg/link v1.2.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pquerna/otp v1.4.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/pquerna/otp v1.5.0 // indirect
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sacloud/api-client-go v0.2.8 // indirect
github.com/sacloud/go-http v0.1.6 // indirect
github.com/sacloud/iaas-api-go v1.11.1 // indirect
github.com/sacloud/packages-go v0.0.9 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.17 // indirect
github.com/sacloud/api-client-go v0.3.3 // indirect
github.com/sacloud/go-http v0.1.9 // indirect
github.com/sacloud/iaas-api-go v1.20.0 // indirect
github.com/sacloud/packages-go v0.0.11 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 // indirect
github.com/selectel/domains-go v1.1.0 // indirect
github.com/selectel/go-selvpcclient/v4 v4.1.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
github.com/softlayer/softlayer-go v1.1.2 // indirect
github.com/softlayer/softlayer-go v1.2.1 // indirect
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 // indirect
github.com/transip/gotransip/v6 v6.20.0 // indirect
github.com/ultradns/ultradns-go-sdk v1.5.0-20230427130837-23c9b0c // indirect
github.com/sony/gobreaker v1.0.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.48 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.1.25 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/transip/gotransip/v6 v6.26.1 // indirect
github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 // indirect
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
github.com/volcengine/volc-sdk-golang v1.0.225 // indirect
github.com/vultr/govultr/v2 v2.17.2 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f // indirect
github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 // indirect
github.com/vultr/govultr/v3 v3.24.0 // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
github.com/yandex-cloud/go-genproto v0.34.0 // indirect
github.com/yandex-cloud/go-sdk v0.27.0 // indirect
github.com/yandex-cloud/go-sdk/services/dns v0.0.16 // indirect
github.com/yandex-cloud/go-sdk/v2 v2.24.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect
go.opencensus.io v0.24.0 // indirect
go.uber.org/ratelimit v0.2.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/net v0.11.0 // indirect
golang.org/x/oauth2 v0.9.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.10.0 // indirect
google.golang.org/api v0.111.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923 // indirect
google.golang.org/grpc v1.53.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/ratelimit v0.3.1 // indirect
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/oauth2 v0.32.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/api v0.254.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20251103181224-f26f9409b101 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/ns1/ns1-go.v2 v2.7.6 // indirect
gopkg.in/ns1/ns1-go.v2 v2.15.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

1936
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
package context
import (
"net/http"
"git.polynom.me/rio/internal/gitea"
"git.polynom.me/rio/internal/metrics"
"github.com/patrickmn/go-cache"
)
type CacheContext struct {
// Cache for general repository information
RepositoryInformationCache cache.Cache
// Cache for path resolutions
RepositoryPathCache cache.Cache
// Cache for username lookups
UsernameCache cache.Cache
}
type GlobalContext struct {
DefaultCSP string
PagesDomain string
Gitea *gitea.GiteaClient
MetricConfig *metrics.MetricConfig
Cache *CacheContext
}
type Context struct {
Username string
Reponame string
Domain string
Path string
// HTTP Stuff
Referrer string
UserAgent string
DNT string
GPC string
Writer http.ResponseWriter
// Pointer to the global context
Global *GlobalContext
}

40
internal/context/info.go Normal file
View File

@@ -0,0 +1,40 @@
package context
import (
"time"
"github.com/patrickmn/go-cache"
)
type RepositoryInformation struct {
// Headers to include in every response
Headers map[string]string
CNAME string
}
func repoInfoKey(owner, name string) string {
return owner + ":" + name
}
func (c *CacheContext) GetRepositoryInformation(owner, repoName string) *RepositoryInformation {
data, found := c.RepositoryInformationCache.Get(repoInfoKey(owner, repoName))
if !found {
return nil
}
typedData := data.(RepositoryInformation)
return &typedData
}
func (c *CacheContext) SetRepositoryInformation(owner, repoName string, info RepositoryInformation) {
c.RepositoryInformationCache.Set(
repoInfoKey(owner, repoName),
info,
cache.DefaultExpiration,
)
}
func MakeRepoInfoCache() cache.Cache {
return *cache.New(24*time.Hour, 12*time.Hour)
}

39
internal/context/path.go Normal file
View File

@@ -0,0 +1,39 @@
package context
import (
"time"
"git.polynom.me/rio/internal/gitea"
"github.com/patrickmn/go-cache"
)
type RepositoryPathInformation struct {
Repository gitea.Repository
Path string
}
func pathInfoKey(domain, path string) string {
return domain + "/" + path
}
func (c *CacheContext) GetRepositoryPath(domain, path string) *RepositoryPathInformation {
data, found := c.RepositoryPathCache.Get(pathInfoKey(domain, path))
if !found {
return nil
}
typedData := data.(RepositoryPathInformation)
return &typedData
}
func (c *CacheContext) SetRepositoryPath(domain, path string, info RepositoryPathInformation) {
c.RepositoryPathCache.Set(
pathInfoKey(domain, path),
info,
cache.DefaultExpiration,
)
}
func MakeRepoPathCache() cache.Cache {
return *cache.New(24*time.Hour, 12*time.Hour)
}

24
internal/context/user.go Normal file
View File

@@ -0,0 +1,24 @@
package context
import (
"time"
"github.com/patrickmn/go-cache"
)
func (c *CacheContext) GetUser(username string) bool {
_, found := c.UsernameCache.Get(username)
return found
}
func (c *CacheContext) SetUser(username string) {
c.UsernameCache.Set(
username,
true,
cache.DefaultExpiration,
)
}
func MakeUsernameCache() cache.Cache {
return *cache.New(24*time.Hour, 12*time.Hour)
}

View File

@@ -2,6 +2,7 @@ package dns
import (
"net"
"errors"
"strings"
"time"
@@ -15,6 +16,10 @@ const (
// The key that the TXT record will have to start with, e.g.
// "repo=some-random-repo".
TxtRepoKey = "repo="
TxtCNAMERecord = "_rio-cname."
TxtCNAMEKey = "cname="
)
var (
@@ -61,14 +66,53 @@ func LookupCNAME(domain string) (string, error) {
return cname.(string), nil
}
cname, err := lookupCNAME(domain)
if err == nil {
cnameCache.Set(domain, cname, cache.DefaultExpiration)
return cname.(string), nil
}
altCname, err := lookupCNAMETxt(domain)
if err == nil {
cnameCache.Set(domain, altCname, cache.DefaultExpiration)
return altCname, nil
}
return "", err
}
// Lookup the CNAME by trying to find a CNAME RR. Contrary to net.LookupCNAME,
// this method fails if we find no CNAME RR.
func lookupCNAME(domain string) (string, error) {
query, err := net.LookupCNAME(domain)
if err == nil {
if query[len(query)-1] == '.' {
query = query[:len(query)-1]
}
cnameCache.Set(domain, query, cache.DefaultExpiration)
// Fail if we have no CNAME RR.
if query == domain {
return "", errors.New("CNAME is equal to domain")
}
return query, nil
}
return "", err
}
// Performs an alternative CNAME lookup by looking for a special TXT record.
func lookupCNAMETxt(domain string) (string, error) {
txts, err := net.LookupTXT(TxtCNAMERecord+domain)
if err == nil {
for _, txt := range txts {
if !strings.HasPrefix(txt, TxtCNAMEKey) {
continue
}
return strings.TrimPrefix(txt, TxtCNAMEKey), nil
}
}
return "", err
}

32
internal/dns/dns_test.go Normal file
View File

@@ -0,0 +1,32 @@
package dns
import "testing"
func cleanCache() {
cnameCache.Flush()
txtRepoCache.Flush()
}
func TestAltCNAME(t *testing.T) {
defer cleanCache()
res, err := lookupCNAMETxt("moxxy.org")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if res != "moxxy.pages.polynom.me" {
t.Fatalf("Unexpected alt CNAME: %s", res)
}
}
func TestLookupCNAMEWithAlt(t *testing.T) {
defer cleanCache()
res, err := LookupCNAME("moxxy.org")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if res != "moxxy.pages.polynom.me" {
t.Fatalf("Unexpected alt CNAME: %s", res)
}
}

15
internal/dns/username.go Normal file
View File

@@ -0,0 +1,15 @@
package dns
import "strings"
// Extract the username from the domain name @domain that we're processing
// at the moment.
func ExtractUsername(pagesDomain, domain string) string {
suffixlessDomain := strings.TrimSuffix(domain, "."+pagesDomain)
usernameParts := strings.Split(suffixlessDomain, ".")
if len(usernameParts) == 1 {
return usernameParts[0]
}
return strings.Join(usernameParts, ".")
}

View File

@@ -1,9 +1,9 @@
package main
package dns
import "testing"
func TestExtractUsernameSimple(t *testing.T) {
username := extractUsername(
username := ExtractUsername(
"pages.local",
"papatutuwawa.pages.local",
)
@@ -13,7 +13,7 @@ func TestExtractUsernameSimple(t *testing.T) {
}
func TestExtractUsernameDot(t *testing.T) {
username := extractUsername(
username := ExtractUsername(
"pages.local",
"polynom.me.pages.local",
)

View File

@@ -1,4 +1,4 @@
package repo
package gitea
import (
"fmt"
@@ -7,6 +7,7 @@ import (
"time"
"code.gitea.io/sdk/gitea"
log "github.com/sirupsen/logrus"
"git.polynom.me/rio/internal/dns"
)
@@ -38,17 +39,20 @@ type Repository struct {
}
type GiteaClient struct {
getRepository GetRepositoryMethod
hasBranch HasBranchMethod
hasUser HasUserMethod
Token string
GetRepository GetRepositoryMethod
HasBranch HasBranchMethod
HasUser HasUserMethod
GetFile GetFileMethod
lookupCNAME LookupCNAMEMethod
lookupRepoTXT LookupRepoTXTMethod
LookupCNAME LookupCNAMEMethod
LookupRepoTXT LookupRepoTXTMethod
}
func NewGiteaClient(giteaUrl string, giteaClient *gitea.Client) GiteaClient {
func NewGiteaClient(giteaUrl string, token string, giteaClient *gitea.Client) GiteaClient {
return GiteaClient{
getRepository: func(username, repositoryName string) (Repository, error) {
Token: token,
GetRepository: func(username, repositoryName string) (Repository, error) {
repo, _, err := giteaClient.GetRepo(username, repositoryName)
if err != nil {
return Repository{}, err
@@ -58,7 +62,7 @@ func NewGiteaClient(giteaUrl string, giteaClient *gitea.Client) GiteaClient {
Name: repo.Name,
}, nil
},
hasBranch: func(username, repositoryName, branchName string) bool {
HasBranch: func(username, repositoryName, branchName string) bool {
res, _, err := giteaClient.ListRepoBranches(username, repositoryName, gitea.ListRepoBranchesOptions{})
if err != nil {
return false
@@ -71,7 +75,7 @@ func NewGiteaClient(giteaUrl string, giteaClient *gitea.Client) GiteaClient {
}
return false
},
hasUser: func(username string) bool {
HasUser: func(username string) bool {
_, _, err := giteaClient.GetUserInfo(username)
return err == nil
},
@@ -86,12 +90,19 @@ func NewGiteaClient(giteaUrl string, giteaClient *gitea.Client) GiteaClient {
path,
branch,
)
log.Debugf("GetFile: Requesting '%s'", apiUrl)
client := &http.Client{}
req, err := http.NewRequest("GET", apiUrl, nil)
if since != nil {
sinceFormat := since.Format(time.RFC1123)
req.Header.Add("If-Modified-Since", sinceFormat)
}
// Add authentication, if we have a token
if token != "" {
req.Header.Add("Authorization", "token "+token)
}
resp, err := client.Do(req)
if err != nil {
return []byte{}, true, err
@@ -109,10 +120,10 @@ func NewGiteaClient(giteaUrl string, giteaClient *gitea.Client) GiteaClient {
return content, true, err
}
},
lookupCNAME: func(domain string) (string, error) {
LookupCNAME: func(domain string) (string, error) {
return dns.LookupCNAME(domain)
},
lookupRepoTXT: func(domain string) (string, error) {
LookupRepoTXT: func(domain string) (string, error) {
return dns.LookupRepoTXT(domain)
},
}

View File

@@ -0,0 +1,99 @@
package metrics
import (
"encoding/json"
"net/http"
"os"
"regexp"
"strings"
log "github.com/sirupsen/logrus"
)
type MetricConfig struct {
Url string
BotUserAgents *[]regexp.Regexp
Enabled bool
}
// Checks if we should send a metric ping to page-metrics based on the served path.
func (c *MetricConfig) ShouldSendMetrics(path, userAgent, dnt, gpc string) bool {
if !strings.HasSuffix(path, ".html") || !c.Enabled {
return false
}
// Ignore requests where the user have set "Do-Not-Track" or "Do-Not-Sell-My-Data", even though
// there is no user data and we're not selling it.
if dnt == "1" || gpc == "1" {
return false
}
// Filter out bots
for _, pattern := range *c.BotUserAgents {
if pattern.MatchString(userAgent) {
return false
}
}
return true
}
func (c *MetricConfig) SendMetricPing(domain, path, referrer string) {
data := map[string]string{
"domain": domain,
"path": path,
"referer": referrer,
}
jsonData, err := json.Marshal(data)
if err != nil {
log.Errorf("Failed to send metric ping: %v", err)
return
}
log.Debugf("Sending payload %s", string(jsonData))
// Send the ping to the server
go func() {
res, err := http.Post(
c.Url,
"application/json",
strings.NewReader(string(jsonData)),
)
if err != nil {
log.Errorf("Failed to send payload to: %v", err)
return
}
defer res.Body.Close()
if res.StatusCode != 200 {
log.Errorf("Server returned non-200 status code %d", res.StatusCode)
}
}()
}
// Reads a JSON array of bot user agents from disk and parses them
// into regular expressions.
func ReadBotPatterns(file string) ([]regexp.Regexp, error) {
content, err := os.ReadFile(file)
if err != nil {
log.Warnf("Failed to read bot metrics file: %v", err)
return []regexp.Regexp{}, err
}
var payload []string
err = json.Unmarshal(content, &payload)
if err != nil {
log.Warnf("Failed to unmarshal file: %v", err)
return []regexp.Regexp{}, err
}
patterns := make([]regexp.Regexp, 0)
for _, v := range payload {
patterns = append(
patterns,
*regexp.MustCompile(v),
)
}
return patterns, nil
}

View File

@@ -0,0 +1,32 @@
package metrics
import (
"regexp"
"testing"
)
func TestShouldPing(t *testing.T) {
cfg := MetricConfig{
Enabled: true,
Url: "",
BotUserAgents: &[]regexp.Regexp{
*regexp.MustCompile("random-bot/.*"),
},
}
if cfg.ShouldSendMetrics("/index.html", "random-bot/v23.5", "", "") {
t.Fatalf("Accepted bot user-agent")
}
if !cfg.ShouldSendMetrics("/index.html", "Firefox/...", "", "") {
t.Fatalf("Rejected real user-agent")
}
if cfg.ShouldSendMetrics("/index.html", "Firefox/...", "1", "") {
t.Fatalf("Ignored DNT")
}
if cfg.ShouldSendMetrics("/index.html", "Firefox/...", "", "1") {
t.Fatalf("Ignored GPC")
}
}

View File

@@ -3,11 +3,12 @@ package pages
import (
"mime"
"net/http"
"strconv"
"strings"
"time"
"git.polynom.me/rio/internal/constants"
"git.polynom.me/rio/internal/repo"
"git.polynom.me/rio/internal/context"
"github.com/patrickmn/go-cache"
log "github.com/sirupsen/logrus"
@@ -27,23 +28,33 @@ func makePageContentCacheEntry(username, path string) string {
return username + ":" + path
}
func addHeaders(csp, contentType string, w http.ResponseWriter) {
w.Header().Set("Content-Type", contentType)
func addHeaders(repoInfo *context.RepositoryInformation, contentType string, contentLength int, w http.ResponseWriter) {
// Always set a content type
if strings.Trim(contentType, " ") == "" {
w.Header().Set("Content-Type", "application/octet-stream")
} else {
w.Header().Set("Content-Type", contentType)
}
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Strict-Transport-Security", "max-age=31536000")
w.Header().Set("Content-Length", strconv.Itoa(contentLength))
if csp != "" {
w.Header().Set("Content-Security-Policy", csp)
if repoInfo != nil {
for key, value := range repoInfo.Headers {
w.Header().Set(key, value)
}
}
}
func ServeFile(username, reponame, path, defaultCsp string, giteaClient *repo.GiteaClient, w http.ResponseWriter) {
func ServeFile(context *context.Context) {
// Strip away a starting / as it messes with Gitea
path := context.Path
if path[:1] == "/" {
path = path[1:]
}
key := makePageContentCacheEntry(username, path)
key := makePageContentCacheEntry(context.Username, path)
entry, found := pageCache.Get(key)
var content []byte
var mimeType string
@@ -57,25 +68,28 @@ func ServeFile(username, reponame, path, defaultCsp string, giteaClient *repo.Gi
since = &sinceRaw
}
content, changed, err := giteaClient.GetFile(
username,
reponame,
content, changed, err := context.Global.Gitea.GetFile(
context.Username,
context.Reponame,
constants.PagesBranch,
path,
since,
)
csp := repo.GetCSPForRepository(username, reponame, "", giteaClient)
repoInfo := context.Global.Cache.GetRepositoryInformation(
context.Username,
context.Reponame,
)
if err != nil {
if !found {
log.Errorf("Failed to get file %s/%s/%s (%s)", username, reponame, path, err)
addHeaders(csp, "text/html", w)
w.WriteHeader(404)
log.Errorf("Failed to get file %s/%s/%s (%s)", context.Username, context.Reponame, path, err)
addHeaders(repoInfo, "text/html", 0, context.Writer)
context.Writer.WriteHeader(404)
} else {
log.Debugf("Request failed but page %s is cached in memory", path)
addHeaders(csp, mimeType, w)
w.WriteHeader(200)
w.Write(content)
addHeaders(repoInfo, mimeType, len(content), context.Writer)
context.Writer.WriteHeader(200)
context.Writer.Write(content)
}
return
@@ -83,9 +97,9 @@ func ServeFile(username, reponame, path, defaultCsp string, giteaClient *repo.Gi
if found && !changed {
log.Debugf("Page %s is unchanged and cached in memory", path)
addHeaders(csp, mimeType, w)
w.WriteHeader(200)
w.Write(content)
addHeaders(repoInfo, mimeType, len(content), context.Writer)
context.Writer.WriteHeader(200)
context.Writer.Write(content)
return
}
@@ -105,7 +119,12 @@ func ServeFile(username, reponame, path, defaultCsp string, giteaClient *repo.Gi
)
log.Debugf("Page %s requested from Gitea and cached in memory at %v", path, now)
addHeaders(csp, mimeType, w)
w.WriteHeader(200)
w.Write(content)
addHeaders(repoInfo, mimeType, len(content), context.Writer)
context.Writer.WriteHeader(200)
context.Writer.Write(content)
// Tell Loki about if, if desired
if context.Global.MetricConfig.ShouldSendMetrics(path, context.UserAgent, context.DNT, context.GPC) {
context.Global.MetricConfig.SendMetricPing(context.Domain, path, context.Referrer)
}
}

View File

@@ -3,119 +3,85 @@ package repo
//go:generate mockgen -destination mock_repo_test.go -package repo code.gitea.io/sdk/gitea Client
import (
"encoding/json"
"errors"
"slices"
"strings"
"time"
"git.polynom.me/rio/internal/constants"
"git.polynom.me/rio/internal/context"
"git.polynom.me/rio/internal/gitea"
"github.com/patrickmn/go-cache"
log "github.com/sirupsen/logrus"
)
var (
pathCache = cache.New(1*time.Hour, 1*time.Hour)
// Caching the existence of an user
userCache = cache.New(24*time.Hour, 12*time.Hour)
// Caches the existence of a Content-Security-Policy
// Mapping: Repository key -> CSPCacheEntry
cspCache = cache.New(24*time.Hour, 12*time.Hour)
ForbiddenHeaders = []string{
"content-length",
"content-type",
"date",
"location",
"strict-transport-security",
"set-cookie",
}
)
type PageCacheEntry struct {
Repository Repository
Path string
}
type CSPCacheEntry struct {
CSP string
LastRequested time.Time
}
func makePageCacheKey(domain, path string) string {
return domain + "/" + path
}
func makeCSPCacheKey(username, repositoryName string) string {
return username + ":" + repositoryName
}
func lookupRepositoryAndCache(username, reponame, branchName, host, domain, path, cname string, giteaClient *GiteaClient) (*Repository, error) {
func lookupRepositoryAndCache(username, reponame, branchName, host, domain, path, cname string, ctx *context.GlobalContext) (*gitea.Repository, error) {
log.Debugf("CNAME: %s", cname)
log.Debugf("Looking up repository %s/%s", username, reponame)
repo, err := giteaClient.getRepository(username, reponame)
repo, err := ctx.Gitea.GetRepository(username, reponame)
if err != nil {
return nil, err
}
if !giteaClient.hasBranch(username, reponame, branchName) {
if !ctx.Gitea.HasBranch(username, reponame, branchName) {
return nil, errors.New("Specified branch does not exist")
}
// Check if the CNAME file matches
if cname != "" {
log.Debug("Checking CNAME")
file, _, err := giteaClient.GetFile(
username,
reponame,
constants.PagesBranch,
"CNAME",
nil,
)
if err != nil {
log.Errorf(
"Could not verify CNAME of %s/%s@%s: %v\n",
username,
reponame,
constants.PagesBranch,
err,
)
return nil, err
repoInfo := GetRepositoryInformation(username, reponame, ctx)
if repoInfo == nil {
log.Warn("Repository does not contain a rio.json file")
return nil, errors.New("No CNAME available in repository")
}
cnameContent := strings.Trim(
string(file[:]),
"\n",
)
log.Debugf("CNAME Content: %s", cnameContent)
if cnameContent != host {
log.Warnf("CNAME mismatch: Repo '%s', Host '%s'", cnameContent, host)
log.Debugf("CNAME Content: \"%s\"", repoInfo.CNAME)
if repoInfo.CNAME != host {
log.Warnf("CNAME mismatch: Repo '%s', Host '%s'", repoInfo.CNAME, host)
return nil, errors.New("CNAME mismatch")
}
}
// Cache data
pathCache.Set(
makePageCacheKey(domain, path),
PageCacheEntry{
repo,
path,
ctx.Cache.SetRepositoryPath(
domain,
path,
context.RepositoryPathInformation{
Repository: repo,
Path: path,
},
cache.DefaultExpiration,
)
return &repo, nil
}
// host is the domain name we're accessed from. cname is the domain that host is pointing
// if, if we're accessed via a CNAME. If not, then cname is "".
func RepoFromPath(username, host, cname, path string, giteaClient *GiteaClient) (*Repository, string, error) {
func RepoFromPath(username, host, cname, path string, ctx *context.GlobalContext) (*gitea.Repository, string, error) {
domain := host
// Guess the repository
key := makePageCacheKey(domain, path)
entry, found := pathCache.Get(key)
if found {
pageEntry := entry.(PageCacheEntry)
return &pageEntry.Repository, pageEntry.Path, nil
entry := ctx.Cache.GetRepositoryPath(domain, path)
if entry != nil {
return &entry.Repository, entry.Path, nil
}
// Allow specifying the repository name in the TXT record
reponame := ""
if cname != "" {
repoLookup, err := giteaClient.lookupRepoTXT(host)
repoLookup, err := ctx.Gitea.LookupRepoTXT(host)
if err == nil && repoLookup != "" {
log.Infof(
"TXT lookup for %s resulted in choosing repository %s",
@@ -139,7 +105,7 @@ func RepoFromPath(username, host, cname, path string, giteaClient *GiteaClient)
domain,
modifiedPath,
cname,
giteaClient,
ctx,
)
if err == nil {
return repo, modifiedPath, nil
@@ -158,7 +124,7 @@ func RepoFromPath(username, host, cname, path string, giteaClient *GiteaClient)
domain,
path,
cname,
giteaClient,
ctx,
)
return repo, path, err
}
@@ -166,52 +132,92 @@ func RepoFromPath(username, host, cname, path string, giteaClient *GiteaClient)
// Checks if the username exists as an organisation or an user on the Gitea
// instance, so that an attacker can't just request certificates for random
// usernames.
func CanRequestCertificate(username string, giteaClient *GiteaClient) bool {
if _, found := userCache.Get(username); found {
func CanRequestCertificate(username string, ctx *context.GlobalContext) bool {
found := ctx.Cache.GetUser(username)
if found {
return true
}
hasUser := giteaClient.hasUser(username)
hasUser := ctx.Gitea.HasUser(username)
if hasUser {
userCache.Set(username, true, cache.DefaultExpiration)
ctx.Cache.SetUser(username)
}
return hasUser
}
// Checks the repository username/repository@PagesBranch for a file named CSP. If it exists,
// read it and return the value. If it does not exist, return defaultCsp.
func GetCSPForRepository(username, repositoryName, defaultCsp string, giteaClient *GiteaClient) string {
key := makeCSPCacheKey(username, repositoryName)
cachedCsp, found := cspCache.Get(key)
var since time.Time
if found {
since = cachedCsp.(CSPCacheEntry).LastRequested
}
func filterHeaders(headers map[string]interface{}) map[string]string {
newHeaders := make(map[string]string)
fetchedCsp, changed, err := giteaClient.GetFile(
username,
repositoryName,
constants.PagesBranch,
"CSP",
&since,
)
csp := ""
if err != nil {
if found {
return cachedCsp.(CSPCacheEntry).CSP
for key, value := range headers {
if slices.Contains[[]string, string](ForbiddenHeaders, strings.ToLower(key)) {
continue
}
csp = defaultCsp
} else {
csp = string(fetchedCsp)
if !found || changed {
cspCache.Set(key, CSPCacheEntry{
CSP: csp,
LastRequested: time.Now(),
}, cache.DefaultExpiration)
switch value.(type) {
case string:
newHeaders[key] = value.(string)
}
}
return csp
return newHeaders
}
func GetRepositoryInformation(owner, repoName string, ctx *context.GlobalContext) *context.RepositoryInformation {
res := ctx.Cache.GetRepositoryInformation(owner, repoName)
if res != nil {
return res
}
fetchedConfig, _, err := ctx.Gitea.GetFile(
owner,
repoName,
constants.PagesBranch,
"rio.json",
nil,
)
if err != nil {
log.Errorf("Failed to request rio.json for %s/%s:%v", owner, repoName, err)
return nil
}
var payload map[string]interface{}
err = json.Unmarshal(fetchedConfig, &payload)
if err != nil {
log.Errorf("Failed to unmarshal rio.json for %s/%s:%v", owner, repoName, err)
return nil
}
headers, found := payload["headers"]
if !found {
log.Warnf("Did not find headers key in rio.json for %s/%s", owner, repoName)
headers = make(map[string]interface{})
} else {
switch headers.(type) {
case map[string]interface{}:
// NOOP
default:
log.Warn("headers attribute has invalid data type")
headers = make(map[string]string)
}
}
cname, found := payload["CNAME"]
if found {
switch cname.(type) {
case string:
// NOOP
default:
log.Warnf("CNAME attribute is not a string for %s/%s", owner, repoName)
cname = ""
}
} else {
cname = ""
}
info := context.RepositoryInformation{
Headers: filterHeaders(headers.(map[string]interface{})),
CNAME: cname.(string),
}
ctx.Cache.SetRepositoryInformation(owner, repoName, info)
return &info
}

View File

@@ -2,28 +2,44 @@ package repo
import (
"errors"
"net/http"
"strings"
"testing"
"time"
"code.gitea.io/sdk/gitea"
"git.polynom.me/rio/internal/context"
"git.polynom.me/rio/internal/gitea"
log "github.com/sirupsen/logrus"
)
func clearCache() {
pathCache.Flush()
userCache.Flush()
cspCache.Flush()
func TestHeaderFilter(t *testing.T) {
map1 := filterHeaders(
map[string]interface{}{
"Content-Type": "hallo",
"content-Type": "welt",
"content-type": "uwu",
"CONTENT-TYPE": "lol",
"Content-Security-Policy": "none",
},
)
if len(map1) != 1 {
t.Fatalf("filterHeaders allowed %d != 1 headers", len(map1))
}
for key := range map1 {
if strings.ToLower(key) == "content-type" {
t.Fatalf("filterHeaders allowed Content-Type")
}
}
}
func TestPickingCorrectRepositoryDefault(t *testing.T) {
// Test that we default to the <username>.<pages domain> repository, if we have only
// one path component.
defer clearCache()
log.SetLevel(log.DebugLevel)
client := GiteaClient{
getRepository: func(username, repositoryName string) (Repository, error) {
client := gitea.GiteaClient{
GetRepository: func(username, repositoryName string) (gitea.Repository, error) {
if username != "example-user" {
t.Fatalf("Called with unknown user %s", username)
}
@@ -31,9 +47,9 @@ func TestPickingCorrectRepositoryDefault(t *testing.T) {
t.Fatalf("Called with unknown repository %s", repositoryName)
}
return Repository{}, nil
return gitea.Repository{}, nil
},
hasBranch: func(username, repositoryName, branchName string) bool {
HasBranch: func(username, repositoryName, branchName string) bool {
if username == "example-user" && repositoryName == "example-user.pages.example.org" && branchName == "pages" {
return true
}
@@ -44,17 +60,24 @@ func TestPickingCorrectRepositoryDefault(t *testing.T) {
t.Fatal("getFile called")
return []byte{}, true, nil
},
lookupCNAME: func(domain string) (string, error) {
t.Fatal("lookupCNAME called")
LookupCNAME: func(domain string) (string, error) {
t.Fatal("LookupCNAME called")
return "", nil
},
lookupRepoTXT: func(domain string) (string, error) {
t.Fatal("lookupRepoTXT called")
LookupRepoTXT: func(domain string) (string, error) {
t.Fatal("LookupRepoTXT called")
return "", nil
},
}
ctx := &context.GlobalContext{
Gitea: &client,
Cache: &context.CacheContext{
RepositoryInformationCache: context.MakeRepoInfoCache(),
RepositoryPathCache: context.MakeRepoPathCache(),
},
}
res, path, err := RepoFromPath("example-user", "example-user.pages.example.org", "", "index.html", &client)
res, path, err := RepoFromPath("example-user", "example-user.pages.example.org", "", "index.html", ctx)
if err != nil {
t.Fatalf("An error occured: %v", err)
}
@@ -69,24 +92,22 @@ func TestPickingCorrectRepositoryDefault(t *testing.T) {
func TestPickingCorrectRepositoryDefaultSubdirectory(t *testing.T) {
// Test that we return the default repository when the first path component does
// not correspong to an existing repository.
defer clearCache()
log.SetLevel(log.DebugLevel)
client := GiteaClient{
getRepository: func(username, repositoryName string) (Repository, error) {
client := gitea.GiteaClient{
GetRepository: func(username, repositoryName string) (gitea.Repository, error) {
if username != "example-user" {
t.Fatalf("Called with unknown user %s", username)
}
if repositoryName == "assets" {
return Repository{}, errors.New("Repository does not exist")
return gitea.Repository{}, errors.New("gitea.Repository does not exist")
} else if repositoryName == "example-user.pages.example.org" {
return Repository{}, nil
return gitea.Repository{}, nil
} else {
t.Fatalf("Called with unknown repository %s", repositoryName)
return Repository{}, nil
return gitea.Repository{}, nil
}
},
hasBranch: func(username, repositoryName, branchName string) bool {
HasBranch: func(username, repositoryName, branchName string) bool {
if username == "example-user" && repositoryName == "example-user.pages.example.org" && branchName == "pages" {
return true
}
@@ -97,17 +118,24 @@ func TestPickingCorrectRepositoryDefaultSubdirectory(t *testing.T) {
t.Fatal("getFile called")
return []byte{}, true, nil
},
lookupCNAME: func(domain string) (string, error) {
t.Fatal("lookupCNAME called")
LookupCNAME: func(domain string) (string, error) {
t.Fatal("LookupCNAME called")
return "", nil
},
lookupRepoTXT: func(domain string) (string, error) {
t.Fatal("lookupRepoTXT called")
LookupRepoTXT: func(domain string) (string, error) {
t.Fatal("LookupRepoTXT called")
return "", nil
},
}
ctx := &context.GlobalContext{
Gitea: &client,
Cache: &context.CacheContext{
RepositoryInformationCache: context.MakeRepoInfoCache(),
RepositoryPathCache: context.MakeRepoPathCache(),
},
}
res, path, err := RepoFromPath("example-user", "example-user.pages.example.org", "", "assets/index.css", &client)
res, path, err := RepoFromPath("example-user", "example-user.pages.example.org", "", "assets/index.css", ctx)
if err != nil {
t.Fatalf("An error occured: %v", err)
}
@@ -122,28 +150,26 @@ func TestPickingCorrectRepositoryDefaultSubdirectory(t *testing.T) {
func TestPickingCorrectRepositorySubdirectoryNoPagesBranch(t *testing.T) {
// Test that we're picking the correct repository when the first path component
// returns a repository without a pages branch.
defer clearCache()
log.SetLevel(log.DebugLevel)
client := GiteaClient{
getRepository: func(username, repositoryName string) (Repository, error) {
client := gitea.GiteaClient{
GetRepository: func(username, repositoryName string) (gitea.Repository, error) {
if username != "example-user" {
t.Fatalf("Called with unknown user %s", username)
}
if repositoryName == "blog" {
return Repository{
return gitea.Repository{
Name: "blog",
}, nil
} else if repositoryName == "example-user.pages.example.org" {
return Repository{
return gitea.Repository{
Name: "example-user.pages.example.org",
}, nil
} else {
t.Fatalf("Called with unknown repository %s", repositoryName)
return Repository{}, nil
return gitea.Repository{}, nil
}
},
hasBranch: func(username, repositoryName, branchName string) bool {
HasBranch: func(username, repositoryName, branchName string) bool {
if username == "example-user" && repositoryName == "example-user.pages.example.org" && branchName == "pages" {
return true
}
@@ -154,17 +180,24 @@ func TestPickingCorrectRepositorySubdirectoryNoPagesBranch(t *testing.T) {
t.Fatal("getFile called")
return []byte{}, true, nil
},
lookupCNAME: func(domain string) (string, error) {
t.Fatal("lookupCNAME called")
LookupCNAME: func(domain string) (string, error) {
t.Fatal("LookupCNAME called")
return "", nil
},
lookupRepoTXT: func(domain string) (string, error) {
t.Fatal("lookupRepoTXT called")
LookupRepoTXT: func(domain string) (string, error) {
t.Fatal("LookupRepoTXT called")
return "", nil
},
}
ctx := &context.GlobalContext{
Gitea: &client,
Cache: &context.CacheContext{
RepositoryInformationCache: context.MakeRepoInfoCache(),
RepositoryPathCache: context.MakeRepoPathCache(),
},
}
res, path, err := RepoFromPath("example-user", "example-user.pages.example.org", "", "blog/post1.html", &client)
res, path, err := RepoFromPath("example-user", "example-user.pages.example.org", "", "blog/post1.html", ctx)
if err != nil {
t.Fatalf("An error occured: %v", err)
}
@@ -181,21 +214,19 @@ func TestPickingCorrectRepositorySubdirectoryNoPagesBranch(t *testing.T) {
func TestPickingNoRepositoryInvalidCNAME(t *testing.T) {
// Test that we're not picking a repository if the CNAME validation fails.
defer clearCache()
log.SetLevel(log.DebugLevel)
client := GiteaClient{
getRepository: func(username, repositoryName string) (Repository, error) {
client := gitea.GiteaClient{
GetRepository: func(username, repositoryName string) (gitea.Repository, error) {
if username == "example-user" && repositoryName == "example-user.pages.example.org" {
return Repository{
return gitea.Repository{
Name: "example-user.pages.example.org",
}, nil
} else {
t.Fatalf("Called with unknown repository %s", repositoryName)
return Repository{}, nil
return gitea.Repository{}, nil
}
},
hasBranch: func(username, repositoryName, branchName string) bool {
HasBranch: func(username, repositoryName, branchName string) bool {
if username == "example-user" && repositoryName == "example-user.pages.example.org" && branchName == "pages" {
return true
}
@@ -203,44 +234,49 @@ func TestPickingNoRepositoryInvalidCNAME(t *testing.T) {
return false
},
GetFile: func(username, repositoryName, branch, path string, since *time.Time) ([]byte, bool, error) {
if username == "example-user" && repositoryName == "example-user.pages.example.org" && branch == "pages" && path == "CNAME" {
return []byte("some-other-domain.local"), true, nil
if username == "example-user" && repositoryName == "example-user.pages.example.org" && branch == "pages" && path == "rio.json" {
return []byte("{\"CNAME\": \"some-other-domain.local\"}"), true, nil
}
t.Fatalf("Invalid file requested: %s/%s@%s:%s", username, repositoryName, branch, path)
return []byte{}, true, nil
},
lookupCNAME: func(domain string) (string, error) {
LookupCNAME: func(domain string) (string, error) {
return "", errors.New("No CNAME")
},
lookupRepoTXT: func(domain string) (string, error) {
LookupRepoTXT: func(domain string) (string, error) {
return "", nil
},
}
ctx := &context.GlobalContext{
Gitea: &client,
Cache: &context.CacheContext{
RepositoryInformationCache: context.MakeRepoInfoCache(),
RepositoryPathCache: context.MakeRepoPathCache(),
},
}
_, _, err := RepoFromPath("example-user", "example-user.pages.example.org", "example-user.local", "index.html", &client)
_, _, err := RepoFromPath("example-user", "example-user.pages.example.org", "example-user.local", "index.html", ctx)
if err == nil {
t.Fatal("Repository returned even though CNAME validation should fail")
t.Fatal("gitea.Repository returned even though CNAME validation should fail")
}
}
func TestPickingRepositoryValidCNAME(t *testing.T) {
// Test that we're picking a repository, given a CNAME, if the CNAME validation succeeds.
defer clearCache()
log.SetLevel(log.DebugLevel)
client := GiteaClient{
getRepository: func(username, repositoryName string) (Repository, error) {
client := gitea.GiteaClient{
GetRepository: func(username, repositoryName string) (gitea.Repository, error) {
if username == "example-user" && repositoryName == "example-user.local" {
return Repository{
return gitea.Repository{
Name: "example-user.local",
}, nil
} else {
t.Fatalf("Called with unknown repository %s", repositoryName)
return Repository{}, nil
return gitea.Repository{}, nil
}
},
hasBranch: func(username, repositoryName, branchName string) bool {
HasBranch: func(username, repositoryName, branchName string) bool {
if username == "example-user" && repositoryName == "example-user.local" && branchName == "pages" {
return true
}
@@ -248,22 +284,29 @@ func TestPickingRepositoryValidCNAME(t *testing.T) {
return false
},
GetFile: func(username, repositoryName, branch, path string, since *time.Time) ([]byte, bool, error) {
if username == "example-user" && repositoryName == "example-user.local" && branch == "pages" && path == "CNAME" {
return []byte("example-user.local"), true, nil
if username == "example-user" && repositoryName == "example-user.local" && branch == "pages" && path == "rio.json" {
return []byte("{\"CNAME\": \"example-user.local\"}"), true, nil
}
t.Fatalf("Invalid file requested: %s/%s@%s:%s", username, repositoryName, branch, path)
return []byte{}, true, nil
},
lookupCNAME: func(domain string) (string, error) {
LookupCNAME: func(domain string) (string, error) {
return "", errors.New("No CNAME")
},
lookupRepoTXT: func(domain string) (string, error) {
LookupRepoTXT: func(domain string) (string, error) {
return "", nil
},
}
ctx := &context.GlobalContext{
Gitea: &client,
Cache: &context.CacheContext{
RepositoryInformationCache: context.MakeRepoInfoCache(),
RepositoryPathCache: context.MakeRepoPathCache(),
},
}
repo, _, err := RepoFromPath("example-user", "example-user.local", "example-user.pages.example.org", "index.html", &client)
repo, _, err := RepoFromPath("example-user", "example-user.local", "example-user.pages.example.org", "index.html", ctx)
if err != nil {
t.Fatalf("Error returned: %v", err)
}
@@ -275,21 +318,19 @@ func TestPickingRepositoryValidCNAME(t *testing.T) {
func TestPickingRepositoryValidCNAMEWithTXTLookup(t *testing.T) {
// Test that we're picking a repository, given a CNAME, if the CNAME validation succeeds
// and the TXT lookup returns something different.
defer clearCache()
log.SetLevel(log.DebugLevel)
client := GiteaClient{
getRepository: func(username, repositoryName string) (Repository, error) {
client := gitea.GiteaClient{
GetRepository: func(username, repositoryName string) (gitea.Repository, error) {
if username == "example-user" && repositoryName == "some-different-repository" {
return Repository{
return gitea.Repository{
Name: "some-different-repository",
}, nil
} else {
t.Fatalf("Called with unknown repository %s", repositoryName)
return Repository{}, nil
return gitea.Repository{}, nil
}
},
hasBranch: func(username, repositoryName, branchName string) bool {
HasBranch: func(username, repositoryName, branchName string) bool {
if username == "example-user" && repositoryName == "some-different-repository" && branchName == "pages" {
return true
}
@@ -297,25 +338,32 @@ func TestPickingRepositoryValidCNAMEWithTXTLookup(t *testing.T) {
return false
},
GetFile: func(username, repositoryName, branch, path string, since *time.Time) ([]byte, bool, error) {
if username == "example-user" && repositoryName == "some-different-repository" && branch == "pages" && path == "CNAME" {
return []byte("example-user.local"), true, nil
if username == "example-user" && repositoryName == "some-different-repository" && branch == "pages" && path == "rio.json" {
return []byte("{\"CNAME\": \"example-user.local\"}"), true, nil
}
t.Fatalf("Invalid file requested: %s/%s@%s:%s", username, repositoryName, branch, path)
return []byte{}, true, nil
},
lookupCNAME: func(domain string) (string, error) {
LookupCNAME: func(domain string) (string, error) {
return "", errors.New("No CNAME")
},
lookupRepoTXT: func(domain string) (string, error) {
LookupRepoTXT: func(domain string) (string, error) {
if domain == "example-user.local" {
return "some-different-repository", nil
}
return "", nil
},
}
ctx := &context.GlobalContext{
Gitea: &client,
Cache: &context.CacheContext{
RepositoryInformationCache: context.MakeRepoInfoCache(),
RepositoryPathCache: context.MakeRepoPathCache(),
},
}
repo, _, err := RepoFromPath("example-user", "example-user.local", "example-user.pages.example.org", "index.html", &client)
repo, _, err := RepoFromPath("example-user", "example-user.local", "example-user.pages.example.org", "index.html", ctx)
if err != nil {
t.Fatalf("Error returned: %v", err)
}
@@ -327,20 +375,18 @@ func TestPickingRepositoryValidCNAMEWithTXTLookup(t *testing.T) {
func TestPickingRepositoryValidCNAMEWithTXTLookupAndSubdirectory(t *testing.T) {
// Test that we're picking a repository, given a CNAME, if the CNAME validation succeeds
// and the TXT lookup returns something different. Additionally, we now have a subdirectory
defer clearCache()
log.SetLevel(log.DebugLevel)
client := GiteaClient{
getRepository: func(username, repositoryName string) (Repository, error) {
client := gitea.GiteaClient{
GetRepository: func(username, repositoryName string) (gitea.Repository, error) {
if username == "example-user" && repositoryName == "some-different-repository" {
return Repository{
return gitea.Repository{
Name: "some-different-repository",
}, nil
}
return Repository{}, errors.New("Unknown repository")
return gitea.Repository{}, errors.New("Unknown repository")
},
hasBranch: func(username, repositoryName, branchName string) bool {
HasBranch: func(username, repositoryName, branchName string) bool {
if username == "example-user" && repositoryName == "some-different-repository" && branchName == "pages" {
return true
}
@@ -348,25 +394,32 @@ func TestPickingRepositoryValidCNAMEWithTXTLookupAndSubdirectory(t *testing.T) {
return false
},
GetFile: func(username, repositoryName, branch, path string, since *time.Time) ([]byte, bool, error) {
if username == "example-user" && repositoryName == "some-different-repository" && branch == "pages" && path == "CNAME" {
return []byte("example-user.local"), true, nil
if username == "example-user" && repositoryName == "some-different-repository" && branch == "pages" && path == "rio.json" {
return []byte("{\"CNAME\": \"example-user.local\"}"), true, nil
}
t.Fatalf("Invalid file requested: %s/%s@%s:%s", username, repositoryName, branch, path)
return []byte{}, true, nil
},
lookupCNAME: func(domain string) (string, error) {
LookupCNAME: func(domain string) (string, error) {
return "", errors.New("No CNAME")
},
lookupRepoTXT: func(domain string) (string, error) {
LookupRepoTXT: func(domain string) (string, error) {
if domain == "example-user.local" {
return "some-different-repository", nil
}
return "", nil
},
}
ctx := &context.GlobalContext{
Gitea: &client,
Cache: &context.CacheContext{
RepositoryInformationCache: context.MakeRepoInfoCache(),
RepositoryPathCache: context.MakeRepoPathCache(),
},
}
repo, _, err := RepoFromPath("example-user", "example-user.local", "example-user.pages.example.org", "blog/index.html", &client)
repo, _, err := RepoFromPath("example-user", "example-user.local", "example-user.pages.example.org", "blog/index.html", ctx)
if err != nil {
t.Fatalf("Error returned: %v", err)
}
@@ -375,31 +428,123 @@ func TestPickingRepositoryValidCNAMEWithTXTLookupAndSubdirectory(t *testing.T) {
}
}
func TestGetCSPForRepositoryNegativeIntegration(t *testing.T) {
defer clearCache()
func TestHeaderParsingEmpty(t *testing.T) {
// Test that we are correctly handling a repository with no headers.
log.SetLevel(log.DebugLevel)
client := gitea.GiteaClient{
GetRepository: func(username, repositoryName string) (gitea.Repository, error) {
if username == "example-user" && repositoryName == "some-different-repository" {
return gitea.Repository{
Name: "some-different-repository",
}, nil
}
httpClient := http.Client{Timeout: 10 * time.Second}
giteaClient, err := gitea.NewClient(
"https://git.polynom.me",
gitea.SetHTTPClient(&httpClient),
gitea.SetToken(""),
gitea.SetUserAgent("rio-testing"),
)
if err != nil {
t.Fatalf("Failed to create Gitea client: %v", err)
return gitea.Repository{}, errors.New("Unknown repository")
},
HasBranch: func(username, repositoryName, branchName string) bool {
if username == "example-user" && repositoryName == "some-different-repository" && branchName == "pages" {
return true
}
return false
},
GetFile: func(username, repositoryName, branch, path string, since *time.Time) ([]byte, bool, error) {
if username == "example-user" && repositoryName == "some-different-repository" && branch == "pages" && path == "rio.json" {
return []byte("{\"CNAME\": \"example-user.local\"}"), true, nil
}
t.Fatalf("Invalid file requested: %s/%s@%s:%s", username, repositoryName, branch, path)
return []byte{}, true, nil
},
LookupCNAME: func(domain string) (string, error) {
return "", errors.New("No CNAME")
},
LookupRepoTXT: func(domain string) (string, error) {
if domain == "example-user.local" {
return "some-different-repository", nil
}
return "", nil
},
}
ctx := &context.GlobalContext{
Gitea: &client,
Cache: &context.CacheContext{
RepositoryInformationCache: context.MakeRepoInfoCache(),
RepositoryPathCache: context.MakeRepoPathCache(),
},
}
client := NewGiteaClient("https://git.polynom.me", giteaClient)
// The repository has no CSP file, so it should return the invalid value
defaultValue := "<INVALID>"
csp := GetCSPForRepository(
"papatutuwawa",
"rio",
defaultValue,
&client,
)
info := GetRepositoryInformation("example-user", "some-different-repository", ctx)
if info == nil {
t.Fatalf("No repository information returned")
}
if csp != defaultValue {
t.Fatalf("Unexpected CSP returned: %s", csp)
if len(info.Headers) > 0 {
t.Fatalf("Headers returned: %v", info.Headers)
}
}
func TestHeaderParsing(t *testing.T) {
// Test that we are correctly handling a repository with no headers.
log.SetLevel(log.DebugLevel)
client := gitea.GiteaClient{
GetRepository: func(username, repositoryName string) (gitea.Repository, error) {
if username == "example-user" && repositoryName == "some-different-repository" {
return gitea.Repository{
Name: "some-different-repository",
}, nil
}
return gitea.Repository{}, errors.New("Unknown repository")
},
HasBranch: func(username, repositoryName, branchName string) bool {
if username == "example-user" && repositoryName == "some-different-repository" && branchName == "pages" {
return true
}
return false
},
GetFile: func(username, repositoryName, branch, path string, since *time.Time) ([]byte, bool, error) {
if username == "example-user" && repositoryName == "some-different-repository" && branch == "pages" && path == "rio.json" {
return []byte("{\"CNAME\": \"example-user.local\", \"headers\": {\"X-Cool-Header\": \"Very nice!\"}}"), true, nil
}
t.Fatalf("Invalid file requested: %s/%s@%s:%s", username, repositoryName, branch, path)
return []byte{}, true, nil
},
LookupCNAME: func(domain string) (string, error) {
return "", errors.New("No CNAME")
},
LookupRepoTXT: func(domain string) (string, error) {
if domain == "example-user.local" {
return "some-different-repository", nil
}
return "", nil
},
}
ctx := &context.GlobalContext{
Gitea: &client,
Cache: &context.CacheContext{
RepositoryInformationCache: context.MakeRepoInfoCache(),
RepositoryPathCache: context.MakeRepoPathCache(),
},
}
info := GetRepositoryInformation("example-user", "some-different-repository", ctx)
if info == nil {
t.Fatalf("No repository information returned")
}
if len(info.Headers) != 1 {
t.Fatalf("len(info.Headers) != 1: %v", info.Headers)
}
header, found := info.Headers["X-Cool-Header"]
if !found {
t.Fatal("Header X-Cool-Header not found")
}
if header != "Very nice!" {
t.Fatalf("Invalid header value for X-Cool-Header: \"%s\"", header)
}
}

View File

@@ -7,6 +7,7 @@ import (
"sync"
"git.polynom.me/rio/internal/certificates"
"git.polynom.me/rio/internal/context"
"git.polynom.me/rio/internal/dns"
"git.polynom.me/rio/internal/repo"
@@ -44,7 +45,6 @@ func unlockDomain(domain string) {
}
func buildDomainList(domain, pagesDomain string) []string {
// TODO: For wildcards, we MUST use DNS01
if domain == pagesDomain || strings.HasSuffix(domain, pagesDomain) {
return []string{
pagesDomain,
@@ -56,7 +56,6 @@ func buildDomainList(domain, pagesDomain string) []string {
}
func getDomainKey(domain, pagesDomain string) string {
// TODO: For wildcards, we MUST use DNS01
if domain == pagesDomain || strings.HasSuffix(domain, pagesDomain) {
return "*." + pagesDomain
}
@@ -64,10 +63,6 @@ func getDomainKey(domain, pagesDomain string) string {
return domain
}
func usernameFromDomain(domain string) string {
return strings.Split(domain, ".")[0]
}
func getUsername(sni, pagesDomain string) (string, error) {
if !strings.HasSuffix(sni, pagesDomain) {
log.Debugf("'%s' is not a subdomain of '%s'", sni, pagesDomain)
@@ -81,13 +76,13 @@ func getUsername(sni, pagesDomain string) (string, error) {
return "", errors.New("CNAME does not resolve to subdomain of pages domain")
}
return usernameFromDomain(query), nil
return dns.ExtractUsername(pagesDomain, query), nil
}
return usernameFromDomain(sni), nil
return dns.ExtractUsername(pagesDomain, sni), nil
}
func MakeTlsConfig(pagesDomain, cachePath string, cache *certificates.CertificatesCache, acmeClient *lego.Client, giteaClient *repo.GiteaClient) *tls.Config {
func MakeTlsConfig(pagesDomain, cachePath string, cache *certificates.CertificatesCache, acmeClient *lego.Client, ctx *context.GlobalContext) *tls.Config {
return &tls.Config{
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
// Validate that we should even care about this domain
@@ -105,7 +100,7 @@ func MakeTlsConfig(pagesDomain, cachePath string, cache *certificates.Certificat
if cert.IsValid() {
return cert.TlsCertificate, nil
} else {
if !isPagesDomain && !repo.CanRequestCertificate(username, giteaClient) {
if !isPagesDomain && !repo.CanRequestCertificate(username, ctx) {
log.Warnf(
"Cannot renew certificate for %s because CanRequestCertificate(%s) returned false",
info.ServerName,
@@ -134,7 +129,7 @@ func MakeTlsConfig(pagesDomain, cachePath string, cache *certificates.Certificat
return newCert.TlsCertificate, nil
}
} else {
if !isPagesDomain && !repo.CanRequestCertificate(username, giteaClient) {
if !isPagesDomain && !repo.CanRequestCertificate(username, ctx) {
log.Warnf(
"Cannot request certificate for %s because CanRequestCertificate(%s) returned false",
info.ServerName,

View File

@@ -12,7 +12,7 @@ func equals(a, b []string) bool {
return false
}
for i, _ := range a {
for i := range a {
if a[i] != b[i] {
return false
}