Compare commits

..

28 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
352f8bb4ce Merge pull request 'Wildcards' (#2) from feat/wildcards into master
Some checks are pending
ci/woodpecker/push/woodpecker Pipeline is pending
Reviewed-on: #2
2024-01-11 21:04:49 +01:00
b8eba55999 docs: Update README
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2024-01-11 20:58:47 +01:00
55e63eee5b docs: Add documentation for the method types 2024-01-11 20:53:31 +01:00
80234fd5ba fix: Prevent "leak" of raw gitea API response for CSP
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-01-11 20:50:48 +01:00
23 changed files with 3109 additions and 390 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/)
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

@@ -38,7 +38,8 @@ an ACME account), `--acme-file` (`ACME_FILE`; Path to the file where ACME accoun
are stored in) are additionally required. `--acme-server` (`ACME_SERVER`) should also
be set to your ACME CA's directory URL as this option defaults to the
[Let's Encrypt Staging Environment](https://letsencrypt.org/docs/staging-environment/). Note that using this optional implies that you accept your
configured ACME CA's Terms of Service.
configured ACME CA's Terms of Service. rio will also spawn an unencrypted HTTP server that is bound to the host specified with `--http-host` (`HTTP_HOST`) and
the port specified with `--http-port` (`HTTP_PORT`). The functionality of this second HTTP server is to upgrade plain HTTP requests to HTTPS requests.
You can also run with `--debug` (`DEBUG_ENABLE=1`) to enable debug logging.
@@ -71,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"
)
@@ -19,12 +20,18 @@ type GetRepositoryMethod func(username, repositoryName string) (Repository, erro
// <username>/<repository>@<branch> exists. If not, returns "", error.
type GetFileMethod func(username, repositoryName, branch, path string, since *time.Time) ([]byte, bool, error)
// Returns the result of a CNAME lookup for @domain.
type LookupCNAMEMethod func(domain string) (string, error)
// Return the repository the domain should point to by looking up the TXT record
// "_rio-pages.<domain>".
type LookupRepoTXTMethod func(domain string) (string, error)
// Check of the repository <username>/<repositoryName> contains the specified
// branch.
type HasBranchMethod func(username, repositoryName, branchName string) bool
// Check if the specified username exists.
type HasUserMethod func(username string) bool
type Repository struct {
@@ -32,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
@@ -52,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
@@ -65,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
},
@@ -80,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
@@ -97,14 +114,16 @@ func NewGiteaClient(giteaUrl string, giteaClient *gitea.Client) GiteaClient {
return []byte{}, true, err
} else if resp.StatusCode == 302 {
return []byte{}, false, nil
} else if resp.StatusCode == 404 {
return []byte{}, false, fmt.Errorf("File does not exist")
} else {
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,26 +2,44 @@ package repo
import (
"errors"
"strings"
"testing"
"time"
"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)
}
@@ -29,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
}
@@ -42,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)
}
@@ -67,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
}
@@ -95,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)
}
@@ -120,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
}
@@ -152,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)
}
@@ -179,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
}
@@ -201,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
}
@@ -246,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)
}
@@ -273,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
}
@@ -295,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)
}
@@ -325,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
}
@@ -346,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)
}
@@ -372,3 +427,124 @@ func TestPickingRepositoryValidCNAMEWithTXTLookupAndSubdirectory(t *testing.T) {
t.Fatalf("Invalid repository name returned: %s", repo.Name)
}
}
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
}
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(),
},
}
info := GetRepositoryInformation("example-user", "some-different-repository", ctx)
if info == nil {
t.Fatalf("No repository information returned")
}
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
}