[mirror] Scalable static site server for Git forges (like GitHub Pages)

Compare changes

Choose any two refs to compare.

+15 -12
.forgejo/workflows/ci.yaml
··· 10 10 11 11 jobs: 12 12 check: 13 - runs-on: codeberg-small-lazy 13 + runs-on: debian-trixie 14 14 container: 15 - image: docker.io/library/node:24-trixie-slim@sha256:fcdfd7bcd8f641c8c76a8950343c73912d68ba341e8dd1074e663b784d3e76f4 15 + image: docker.io/library/node:24-trixie-slim@sha256:b05474903f463ce4064c09986525e6588c3e66c51b69be9c93a39fb359f883ce 16 16 steps: 17 17 - name: Check out source code 18 - uses: https://code.forgejo.org/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 18 + uses: https://code.forgejo.org/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 19 19 - name: Set up toolchain 20 20 uses: https://code.forgejo.org/actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 21 21 with: ··· 28 28 - name: Build service 29 29 run: | 30 30 go build 31 + - name: Run tests 32 + run: | 33 + go test ./... 31 34 - name: Run static analysis 32 35 run: | 33 - go vet 34 - staticcheck 36 + go vet ./... 37 + staticcheck ./... 35 38 36 39 release: 37 40 # IMPORTANT: This workflow step will not work without the Releases unit enabled! 38 41 if: ${{ forge.ref == 'refs/heads/main' || startsWith(forge.event.ref, 'refs/tags/v') }} 39 42 needs: [check] 40 - runs-on: codeberg-medium-lazy 43 + runs-on: debian-trixie 41 44 container: 42 - image: docker.io/library/node:24-trixie-slim@sha256:fcdfd7bcd8f641c8c76a8950343c73912d68ba341e8dd1074e663b784d3e76f4 45 + image: docker.io/library/node:24-trixie-slim@sha256:b05474903f463ce4064c09986525e6588c3e66c51b69be9c93a39fb359f883ce 43 46 steps: 44 47 - name: Check out source code 45 - uses: https://code.forgejo.org/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 48 + uses: https://code.forgejo.org/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 46 49 - name: Set up toolchain 47 50 uses: https://code.forgejo.org/actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 48 51 with: ··· 72 75 package: 73 76 if: ${{ forge.ref == 'refs/heads/main' || startsWith(forge.event.ref, 'refs/tags/v') }} 74 77 needs: [check] 75 - runs-on: codeberg-medium-lazy 78 + runs-on: debian-trixie 76 79 container: 77 - image: docker.io/library/node:24-trixie-slim@sha256:fcdfd7bcd8f641c8c76a8950343c73912d68ba341e8dd1074e663b784d3e76f4 80 + image: docker.io/library/node:24-trixie-slim@sha256:b05474903f463ce4064c09986525e6588c3e66c51b69be9c93a39fb359f883ce 78 81 steps: 79 82 - name: Install dependencies 80 83 run: | 81 84 apt-get -y update 82 - apt-get -y install buildah ca-certificates 85 + apt-get -y install ca-certificates buildah qemu-user-binfmt 83 86 - name: Check out source code 84 - uses: https://code.forgejo.org/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 87 + uses: https://code.forgejo.org/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 85 88 - name: Authenticate with Docker 86 89 run: | 87 90 buildah login --authfile=/tmp/authfile-${FORGE}.json \
+8 -8
Dockerfile
··· 1 1 # Install CA certificates. 2 - FROM docker.io/library/alpine:latest@sha256:51183f2cfa6320055da30872f211093f9ff1d3cf06f39a0bdb212314c5dc7375 AS ca-certificates-builder 2 + FROM docker.io/library/alpine:3 AS ca-certificates-builder 3 3 RUN apk --no-cache add ca-certificates 4 4 5 5 # Build supervisor. 6 - FROM docker.io/library/golang:1.25-alpine@sha256:26111811bc967321e7b6f852e914d14bede324cd1accb7f81811929a6a57fea9 AS supervisor-builder 6 + FROM docker.io/library/golang:1.25-alpine@sha256:ac09a5f469f307e5da71e766b0bd59c9c49ea460a528cc3e6686513d64a6f1fb AS supervisor-builder 7 7 RUN apk --no-cache add git 8 8 WORKDIR /build 9 9 RUN git clone https://github.com/ochinchina/supervisord . && \ ··· 11 11 RUN GOBIN=/usr/bin go install -ldflags "-s -w" 12 12 13 13 # Build Caddy with S3 storage backend. 14 - FROM docker.io/library/caddy:2.10.2-builder@sha256:fe404674d209455fdef351db5437758ee0e70a6b59abe770663c09cfa05dbddf AS caddy-builder 14 + FROM docker.io/library/caddy:2.10.2-builder@sha256:6644af24bde2b4dbb07eb57637051abd2aa713e9787fa1eb544c3f31a0620898 AS caddy-builder 15 15 RUN xcaddy build ${CADDY_VERSION} \ 16 16 --with=github.com/ss098/certmagic-s3@v0.0.0-20250922022452-8af482af5f39 17 17 18 18 # Build git-pages. 19 - FROM docker.io/library/golang:1.25-alpine@sha256:26111811bc967321e7b6f852e914d14bede324cd1accb7f81811929a6a57fea9 AS git-pages-builder 19 + FROM docker.io/library/golang:1.25-alpine@sha256:ac09a5f469f307e5da71e766b0bd59c9c49ea460a528cc3e6686513d64a6f1fb AS git-pages-builder 20 20 RUN apk --no-cache add git 21 21 WORKDIR /build 22 22 COPY go.mod go.sum ./ ··· 26 26 RUN go build -ldflags "-s -w" -o git-pages . 27 27 28 28 # Compose git-pages and Caddy. 29 - FROM docker.io/library/busybox:1.37.0-musl@sha256:ef13e7482851632be3faf5bd1d28d4727c0810901d564b35416f309975a12a30 29 + FROM docker.io/library/busybox:1.37.0-musl@sha256:03db190ed4c1ceb1c55d179a0940e2d71d42130636a780272629735893292223 30 30 COPY --from=ca-certificates-builder /etc/ssl/cert.pem /etc/ssl/cert.pem 31 31 COPY --from=supervisor-builder /usr/bin/supervisord /bin/supervisord 32 32 COPY --from=caddy-builder /usr/bin/caddy /bin/caddy ··· 36 36 RUN mkdir /app/data 37 37 COPY conf/supervisord.conf /app/supervisord.conf 38 38 COPY conf/Caddyfile /app/Caddyfile 39 - COPY conf/config.example.toml /app/config.toml 39 + COPY conf/config.docker.toml /app/config.toml 40 40 41 41 # Caddy ports: 42 42 EXPOSE 80/tcp 443/tcp 443/udp ··· 46 46 # While the default command is to run git-pages standalone, the intended configuration 47 47 # is to use it with Caddy and store both site data and credentials to an S3-compatible 48 48 # object store. 49 - # * In a standalone configuration, the default, git-caddy listens on port 3000 (http). 50 - # * In a combined configuration, supervisord launches both git-caddy and Caddy, and 49 + # * In a standalone configuration, the default, git-pages listens on port 3000 (http). 50 + # * In a combined configuration, supervisord launches both git-pages and Caddy, and 51 51 # Caddy listens on ports 80 (http) and 443 (https). 52 52 CMD ["git-pages"] 53 53 # CMD ["supervisord"]
+5 -4
README.md
··· 137 137 * If `SENTRY_DSN` environment variable is set, panics are reported to Sentry. 138 138 * If `SENTRY_DSN` and `SENTRY_LOGS=1` environment variables are set, logs are uploaded to Sentry. 139 139 * If `SENTRY_DSN` and `SENTRY_TRACING=1` environment variables are set, traces are uploaded to Sentry. 140 - * Optional syslog integration allows transmitting application logs to a syslog daemon. When present, the `SYSLOG_ADDR` environment variable enables the integration, and the variable's value is used to configure the absolute path to a Unix socket (usually located at `/dev/log` on Unix systems) or a network address of one of the following formats: 141 - * for TLS over TCP: `tcp+tls://host:port`; 142 - * for plain TCP: `tcp://host:post`; 143 - * for UDP: `udp://host:port`. 140 + * Optional syslog integration allows transmitting application logs to a syslog daemon. When present, the `SYSLOG_ADDR` environment variable enables the integration, and the value is used to configure the syslog destination. The value must follow the format `family/address` and is usually one of the following: 141 + * a Unix datagram socket: `unixgram//dev/log`; 142 + * TLS over TCP: `tcp+tls/host:port`; 143 + * plain TCP: `tcp/host:post`; 144 + * UDP: `udp/host:port`. 144 145 145 146 146 147 Architecture (v2)
-6
conf/Caddyfile
··· 25 25 on_demand 26 26 } 27 27 28 - # initial PUT/POST for a new domain has to happen over HTTP 29 - @upgrade `method('GET') && protocol('http')` 30 - redir @upgrade https://{host}{uri} 301 31 - 32 28 reverse_proxy http://localhost:3000 33 - header Alt-Svc `h3=":443"; persist=1, h2=":443"; persist=1` 34 - encode 35 29 }
+4
conf/config.docker.toml
··· 1 + [server] 2 + pages = "tcp/:3000" 3 + caddy = "tcp/:3001" 4 + metrics = "tcp/:3002"
+4 -5
conf/config.example.toml
··· 2 2 # as the intrinsic default value. 3 3 4 4 log-format = "text" 5 - log-level = "info" 6 5 7 6 [server] 8 7 # Use "-" to disable the handler. 9 - pages = "tcp/:3000" 10 - caddy = "tcp/:3001" 11 - metrics = "tcp/:3002" 8 + pages = "tcp/localhost:3000" 9 + caddy = "tcp/localhost:3001" 10 + metrics = "tcp/localhost:3002" 12 11 13 12 [[wildcard]] # non-default section 14 13 domain = "codeberg.page" ··· 51 50 update-timeout = "60s" 52 51 max-heap-size-ratio = 0.5 # * RAM_size 53 52 forbidden-domains = [] 54 - # allowed-repository-url-prefixes = <nil> 53 + allowed-repository-url-prefixes = [] 55 54 allowed-custom-headers = ["X-Clacks-Overhead"] 56 55 57 56 [audit]
+24
flake.lock
··· 18 18 "type": "github" 19 19 } 20 20 }, 21 + "gomod2nix": { 22 + "inputs": { 23 + "flake-utils": [ 24 + "flake-utils" 25 + ], 26 + "nixpkgs": [ 27 + "nixpkgs" 28 + ] 29 + }, 30 + "locked": { 31 + "lastModified": 1763982521, 32 + "narHash": "sha256-ur4QIAHwgFc0vXiaxn5No/FuZicxBr2p0gmT54xZkUQ=", 33 + "owner": "nix-community", 34 + "repo": "gomod2nix", 35 + "rev": "02e63a239d6eabd595db56852535992c898eba72", 36 + "type": "github" 37 + }, 38 + "original": { 39 + "owner": "nix-community", 40 + "repo": "gomod2nix", 41 + "type": "github" 42 + } 43 + }, 21 44 "nix-filter": { 22 45 "locked": { 23 46 "lastModified": 1757882181, ··· 52 75 "root": { 53 76 "inputs": { 54 77 "flake-utils": "flake-utils", 78 + "gomod2nix": "gomod2nix", 55 79 "nix-filter": "nix-filter", 56 80 "nixpkgs": "nixpkgs" 57 81 }
+19 -4
flake.nix
··· 3 3 nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 4 4 flake-utils.url = "github:numtide/flake-utils"; 5 5 nix-filter.url = "github:numtide/nix-filter"; 6 + 7 + gomod2nix = { 8 + url = "github:nix-community/gomod2nix"; 9 + inputs.nixpkgs.follows = "nixpkgs"; 10 + inputs.flake-utils.follows = "flake-utils"; 11 + }; 6 12 }; 7 13 8 14 outputs = ··· 11 17 nixpkgs, 12 18 flake-utils, 13 19 nix-filter, 14 - }: 20 + ... 21 + }@inputs: 15 22 flake-utils.lib.eachDefaultSystem ( 16 23 system: 17 24 let 18 - pkgs = nixpkgs.legacyPackages.${system}; 25 + pkgs = import nixpkgs { 26 + inherit system; 27 + 28 + overlays = [ 29 + inputs.gomod2nix.overlays.default 30 + ]; 31 + }; 19 32 20 - git-pages = pkgs.buildGo125Module { 33 + git-pages = pkgs.buildGoApplication { 21 34 pname = "git-pages"; 22 35 version = "0"; 23 36 ··· 43 56 "-s -w" 44 57 ]; 45 58 46 - vendorHash = "sha256-CIEDUWnd5Sth3yYNtw+w1ucYqLCacO34G+EDXVe4+6o="; 59 + go = pkgs.go_1_25; 60 + modules = ./gomod2nix.toml; 47 61 }; 48 62 in 49 63 { ··· 56 70 57 71 packages = with pkgs; [ 58 72 caddy 73 + gomod2nix 59 74 ]; 60 75 }; 61 76
+14 -11
go.mod
··· 3 3 go 1.25.0 4 4 5 5 require ( 6 - codeberg.org/git-pages/go-headers v1.1.0 7 - codeberg.org/git-pages/go-slog-syslog v0.0.0-20251122144254-06c45d430fb9 6 + codeberg.org/git-pages/go-headers v1.1.1 7 + codeberg.org/git-pages/go-slog-syslog v0.0.0-20251207093707-892f654e80b7 8 8 github.com/KimMachineGun/automemlimit v0.7.5 9 9 github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 10 10 github.com/creasty/defaults v1.8.0 ··· 12 12 github.com/fatih/color v1.18.0 13 13 github.com/getsentry/sentry-go v0.40.0 14 14 github.com/getsentry/sentry-go/slog v0.40.0 15 - github.com/go-git/go-billy/v6 v6.0.0-20251206100608-d4862421331a 16 - github.com/go-git/go-git/v6 v6.0.0-20251206100705-e633db5b9a34 15 + github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd 16 + github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19 17 17 github.com/jpillora/backoff v1.0.0 18 18 github.com/kankanreno/go-snowflake v1.2.0 19 19 github.com/klauspost/compress v1.18.2 20 - github.com/maypok86/otter/v2 v2.2.1 20 + github.com/maypok86/otter/v2 v2.3.0 21 21 github.com/minio/minio-go/v7 v7.0.97 22 22 github.com/pelletier/go-toml/v2 v2.2.4 23 23 github.com/pquerna/cachecontrol v0.2.0 24 24 github.com/prometheus/client_golang v1.23.2 25 - github.com/samber/slog-multi v1.6.0 25 + github.com/samber/slog-multi v1.7.0 26 26 github.com/tj/go-redirects v0.0.0-20200911105812-fd1ba1020b37 27 27 github.com/valyala/fasttemplate v1.2.2 28 - google.golang.org/protobuf v1.36.10 28 + golang.org/x/net v0.48.0 29 + google.golang.org/protobuf v1.36.11 29 30 ) 30 31 31 32 require ( ··· 35 36 github.com/cespare/xxhash/v2 v2.3.0 // indirect 36 37 github.com/cloudflare/circl v1.6.1 // indirect 37 38 github.com/cyphar/filepath-securejoin v0.6.1 // indirect 39 + github.com/davecgh/go-spew v1.1.1 // indirect 38 40 github.com/dustin/go-humanize v1.0.1 // indirect 39 41 github.com/emirpasic/gods v1.18.1 // indirect 40 42 github.com/go-git/gcfg/v2 v2.0.2 // indirect ··· 54 56 github.com/philhofer/fwd v1.2.0 // indirect 55 57 github.com/pjbgf/sha1cd v0.5.0 // indirect 56 58 github.com/pkg/errors v0.9.1 // indirect 59 + github.com/pmezard/go-difflib v1.0.0 // indirect 57 60 github.com/prometheus/client_model v0.6.2 // indirect 58 61 github.com/prometheus/common v0.66.1 // indirect 59 62 github.com/prometheus/procfs v0.16.1 // indirect ··· 61 64 github.com/samber/lo v1.52.0 // indirect 62 65 github.com/samber/slog-common v0.19.0 // indirect 63 66 github.com/sergi/go-diff v1.4.0 // indirect 67 + github.com/stretchr/testify v1.11.1 // indirect 64 68 github.com/tinylib/msgp v1.3.0 // indirect 65 69 github.com/tj/assert v0.0.3 // indirect 66 70 github.com/valyala/bytebufferpool v1.0.0 // indirect 67 71 go.yaml.in/yaml/v2 v2.4.2 // indirect 68 - golang.org/x/crypto v0.45.0 // indirect 69 - golang.org/x/net v0.47.0 // indirect 70 - golang.org/x/sys v0.38.0 // indirect 71 - golang.org/x/text v0.31.0 // indirect 72 + golang.org/x/crypto v0.46.0 // indirect 73 + golang.org/x/sys v0.39.0 // indirect 74 + golang.org/x/text v0.32.0 // indirect 72 75 gopkg.in/yaml.v3 v3.0.1 // indirect 73 76 )
+26 -28
go.sum
··· 1 - codeberg.org/git-pages/go-headers v1.1.0 h1:rk7/SOSsn+XuL7PUQZFYUaWKHEaj6K8mXmUV9rF2VxE= 2 - codeberg.org/git-pages/go-headers v1.1.0/go.mod h1:N4gwH0U3YPwmuyxqH7xBA8j44fTPX+vOEP7ejJVBPts= 3 - codeberg.org/git-pages/go-slog-syslog v0.0.0-20251122144254-06c45d430fb9 h1:xfPDg8ThBt3+t+C+pvM3bEH4ePUzP5t5kY2v19TqgKc= 4 - codeberg.org/git-pages/go-slog-syslog v0.0.0-20251122144254-06c45d430fb9/go.mod h1:8NPSXbYcVb71qqNM5cIgn1/uQgMisLbu2dVD1BNxsUw= 1 + codeberg.org/git-pages/go-headers v1.1.1 h1:fpIBELKo66Z2k+gCeYl5mCEXVQ99Lmx1iup1nbo2shE= 2 + codeberg.org/git-pages/go-headers v1.1.1/go.mod h1:N4gwH0U3YPwmuyxqH7xBA8j44fTPX+vOEP7ejJVBPts= 3 + codeberg.org/git-pages/go-slog-syslog v0.0.0-20251207093707-892f654e80b7 h1:+rkrAxhNZo/eKEcKOqVOsF6ohAPv5amz0JLburOeRjs= 4 + codeberg.org/git-pages/go-slog-syslog v0.0.0-20251207093707-892f654e80b7/go.mod h1:8NPSXbYcVb71qqNM5cIgn1/uQgMisLbu2dVD1BNxsUw= 5 5 github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk= 6 6 github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= 7 7 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= ··· 31 31 github.com/dghubble/trie v0.1.0/go.mod h1:sOmnzfBNH7H92ow2292dDFWNsVQuh/izuD7otCYb1ak= 32 32 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 33 33 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 34 - github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= 35 - github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= 36 34 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 37 35 github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 38 36 github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= ··· 47 45 github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 48 46 github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo= 49 47 github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= 50 - github.com/go-git/go-billy/v6 v6.0.0-20251206100608-d4862421331a h1:8JM2eaLX/ObLssDAowWTqw53RIKrMKC9n6QUGq9hA8g= 51 - github.com/go-git/go-billy/v6 v6.0.0-20251206100608-d4862421331a/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k= 52 - github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251203093322-2d981fbae6b7 h1:f8lec5CHzeDgHKzEBZKD6MwAUeaYDfIT+aCL9bU/TqY= 53 - github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251203093322-2d981fbae6b7/go.mod h1:LzlZlYf8eQeXZKsd2azifbQGsaiTkcjI5WxzH1Wiyhg= 54 - github.com/go-git/go-git/v6 v6.0.0-20251206100705-e633db5b9a34 h1:zvQHay88dsz9zO+61k0CmmFo3VAcTBtGlxTwDbnHG0w= 55 - github.com/go-git/go-git/v6 v6.0.0-20251206100705-e633db5b9a34/go.mod h1:djt5SZ0fMrkORuVAxrZlwtRMw+hnqfZZVqWFH/uQAMI= 48 + github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd h1:Gd/f9cGi/3h1JOPaa6er+CkKUGyGX2DBJdFbDKVO+R0= 49 + github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd/go.mod h1:d3XQcsHu1idnquxt48kAv+h+1MUiYKLH/e7LAzjP+pI= 50 + github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146 h1:xYfxAopYyL44ot6dMBIb1Z1njFM0ZBQ99HdIB99KxLs= 51 + github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146/go.mod h1:QE/75B8tBSLNGyUUbA9tw3EGHoFtYOtypa2h8YJxsWI= 52 + github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19 h1:0lz2eJScP8v5YZQsrEw+ggWC5jNySjg4bIZo5BIh6iI= 53 + github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19/go.mod h1:L+Evfcs7EdTqxwv854354cb6+++7TFL3hJn3Wy4g+3w= 56 54 github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= 57 55 github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 58 56 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= ··· 90 88 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 91 89 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 92 90 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 93 - github.com/maypok86/otter/v2 v2.2.1 h1:hnGssisMFkdisYcvQ8L019zpYQcdtPse+g0ps2i7cfI= 94 - github.com/maypok86/otter/v2 v2.2.1/go.mod h1:1NKY9bY+kB5jwCXBJfE59u+zAwOt6C7ni1FTlFFMqVs= 91 + github.com/maypok86/otter/v2 v2.3.0 h1:8H8AVVFUSzJwIegKwv1uF5aGitTY+AIrtktg7OcLs8w= 92 + github.com/maypok86/otter/v2 v2.3.0/go.mod h1:XgIdlpmL6jYz882/CAx1E4C1ukfgDKSaw4mWq59+7l8= 95 93 github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q= 96 94 github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= 97 95 github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= ··· 132 130 github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= 133 131 github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI= 134 132 github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M= 135 - github.com/samber/slog-multi v1.6.0 h1:i1uBY+aaln6ljwdf7Nrt4Sys8Kk6htuYuXDHWJsHtZg= 136 - github.com/samber/slog-multi v1.6.0/go.mod h1:qTqzmKdPpT0h4PFsTN5rYRgLwom1v+fNGuIrl1Xnnts= 133 + github.com/samber/slog-multi v1.7.0 h1:GKhbkxU3ujkyMsefkuz4qvE6EcgtSuqjFisPnfdzVLI= 134 + github.com/samber/slog-multi v1.7.0/go.mod h1:qTqzmKdPpT0h4PFsTN5rYRgLwom1v+fNGuIrl1Xnnts= 137 135 github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= 138 136 github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 139 137 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= ··· 155 153 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 156 154 go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= 157 155 go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= 158 - golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 159 - golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 160 - golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 161 - golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 156 + golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= 157 + golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 158 + golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= 159 + golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 162 160 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 163 161 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 164 - golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 165 - golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 166 - golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= 167 - golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= 168 - golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 169 - golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 170 - google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= 171 - google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 162 + golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= 163 + golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 164 + golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= 165 + golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 166 + golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= 167 + golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 168 + google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= 169 + google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 172 170 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 173 171 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 174 172 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+204
gomod2nix.toml
··· 1 + schema = 3 2 + 3 + [mod] 4 + [mod."codeberg.org/git-pages/go-headers"] 5 + version = "v1.1.1" 6 + hash = "sha256-qgL7l1FHXxcBWhBnBLEI0yENd6P+frvwlKxEAXLA3VY=" 7 + [mod."codeberg.org/git-pages/go-slog-syslog"] 8 + version = "v0.0.0-20251207093707-892f654e80b7" 9 + hash = "sha256-ye+DBIyxqTEOViYRrQPWyGJCaLmyKSDwH5btlqDPizM=" 10 + [mod."github.com/KimMachineGun/automemlimit"] 11 + version = "v0.7.5" 12 + hash = "sha256-lH/ip9j2hbYUc2W/XIYve/5TScQPZtEZe3hu76CY//k=" 13 + [mod."github.com/Microsoft/go-winio"] 14 + version = "v0.6.2" 15 + hash = "sha256-tVNWDUMILZbJvarcl/E7tpSnkn7urqgSHa2Eaka5vSU=" 16 + [mod."github.com/ProtonMail/go-crypto"] 17 + version = "v1.3.0" 18 + hash = "sha256-TUG+C4MyeWglOmiwiW2/NUVurFHXLgEPRd3X9uQ1NGI=" 19 + [mod."github.com/beorn7/perks"] 20 + version = "v1.0.1" 21 + hash = "sha256-h75GUqfwJKngCJQVE5Ao5wnO3cfKD9lSIteoLp/3xJ4=" 22 + [mod."github.com/c2h5oh/datasize"] 23 + version = "v0.0.0-20231215233829-aa82cc1e6500" 24 + hash = "sha256-8MqL7xCvE6fIjanz2jwkaLP1OE5kLu62TOcQx452DHQ=" 25 + [mod."github.com/cespare/xxhash/v2"] 26 + version = "v2.3.0" 27 + hash = "sha256-7hRlwSR+fos1kx4VZmJ/7snR7zHh8ZFKX+qqqqGcQpY=" 28 + [mod."github.com/cloudflare/circl"] 29 + version = "v1.6.1" 30 + hash = "sha256-Dc69V12eIFnJoUNmwg6VKXHfAMijbAeEVSDe8AiOaLo=" 31 + [mod."github.com/creasty/defaults"] 32 + version = "v1.8.0" 33 + hash = "sha256-I1LE1cfOhMS5JxB7+fWTKieefw2Gge1UhIZh+A6pa6s=" 34 + [mod."github.com/cyphar/filepath-securejoin"] 35 + version = "v0.6.1" 36 + hash = "sha256-obqip8c1c9mjXFznyXF8aDnpcMw7ttzv+e28anCa/v0=" 37 + [mod."github.com/davecgh/go-spew"] 38 + version = "v1.1.1" 39 + hash = "sha256-nhzSUrE1fCkN0+RL04N4h8jWmRFPPPWbCuDc7Ss0akI=" 40 + [mod."github.com/dghubble/trie"] 41 + version = "v0.1.0" 42 + hash = "sha256-hVh7uYylpMCCSPcxl70hJTmzSwaA1MxBmJFBO5Xdncc=" 43 + [mod."github.com/dustin/go-humanize"] 44 + version = "v1.0.1" 45 + hash = "sha256-yuvxYYngpfVkUg9yAmG99IUVmADTQA0tMbBXe0Fq0Mc=" 46 + [mod."github.com/emirpasic/gods"] 47 + version = "v1.18.1" 48 + hash = "sha256-hGDKddjLj+5dn2woHtXKUdd49/3xdsqnhx7VEdCu1m4=" 49 + [mod."github.com/fatih/color"] 50 + version = "v1.18.0" 51 + hash = "sha256-pP5y72FSbi4j/BjyVq/XbAOFjzNjMxZt2R/lFFxGWvY=" 52 + [mod."github.com/getsentry/sentry-go"] 53 + version = "v0.40.0" 54 + hash = "sha256-mJ+EzM8WRzJ2Yp7ithDJNceU4+GbzQyi46yc8J8d13Y=" 55 + [mod."github.com/getsentry/sentry-go/slog"] 56 + version = "v0.40.0" 57 + hash = "sha256-uc9TpKiWMEpRbxwV2uGQeq1DDdZi+APOgu2StVzzEkw=" 58 + [mod."github.com/go-git/gcfg/v2"] 59 + version = "v2.0.2" 60 + hash = "sha256-icqMDeC/tEg/3979EuEN67Ml5KjdDA0R3QvR6iLLrSI=" 61 + [mod."github.com/go-git/go-billy/v6"] 62 + version = "v6.0.0-20251217170237-e9738f50a3cd" 63 + hash = "sha256-b2yunYcPUiLTU+Rr8qTBdsDEfsIhZDYmyqKW5udmpFY=" 64 + [mod."github.com/go-git/go-git/v6"] 65 + version = "v6.0.0-20251224103503-78aff6aa5ea9" 66 + hash = "sha256-kYjDqH0NZ+sxQnj5K8xKfO2WOVKtQ/7tWcqY6KYqAZE=" 67 + [mod."github.com/go-ini/ini"] 68 + version = "v1.67.0" 69 + hash = "sha256-V10ahGNGT+NLRdKUyRg1dos5RxLBXBk1xutcnquc/+4=" 70 + [mod."github.com/golang/groupcache"] 71 + version = "v0.0.0-20241129210726-2c02b8208cf8" 72 + hash = "sha256-AdLZ3dJLe/yduoNvZiXugZxNfmwJjNQyQGsIdzYzH74=" 73 + [mod."github.com/google/uuid"] 74 + version = "v1.6.0" 75 + hash = "sha256-VWl9sqUzdOuhW0KzQlv0gwwUQClYkmZwSydHG2sALYw=" 76 + [mod."github.com/jpillora/backoff"] 77 + version = "v1.0.0" 78 + hash = "sha256-uxHg68NN8hrwPCrPfLYYprZHf7dMyEoPoF46JFx0IHU=" 79 + [mod."github.com/kankanreno/go-snowflake"] 80 + version = "v1.2.0" 81 + hash = "sha256-713xGEqjwaUGIu2EHII5sldWmcquFpxZmte/7R/O6LA=" 82 + [mod."github.com/kevinburke/ssh_config"] 83 + version = "v1.4.0" 84 + hash = "sha256-UclxB7Ll1FZCgU2SrGkiGdr4CoSRJ127MNnZtxKTsvg=" 85 + [mod."github.com/klauspost/compress"] 86 + version = "v1.18.2" 87 + hash = "sha256-mRa+6qEi5joqQao13ZFogmq67rOQzHCVbCCjKA+HKEc=" 88 + [mod."github.com/klauspost/cpuid/v2"] 89 + version = "v2.3.0" 90 + hash = "sha256-50JhbQyT67BK38HIdJihPtjV7orYp96HknI2VP7A9Yc=" 91 + [mod."github.com/klauspost/crc32"] 92 + version = "v1.3.0" 93 + hash = "sha256-RsS/MDJbVzVB+i74whqABgwZJWMw+AutF6HhJBVgbag=" 94 + [mod."github.com/leodido/go-syslog/v4"] 95 + version = "v4.3.0" 96 + hash = "sha256-fCJ2rgrrPR/Ey/PoAsJhd8Sl8mblAnnMAmBuoWFBTgg=" 97 + [mod."github.com/mattn/go-colorable"] 98 + version = "v0.1.13" 99 + hash = "sha256-qb3Qbo0CELGRIzvw7NVM1g/aayaz4Tguppk9MD2/OI8=" 100 + [mod."github.com/mattn/go-isatty"] 101 + version = "v0.0.20" 102 + hash = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ=" 103 + [mod."github.com/maypok86/otter/v2"] 104 + version = "v2.3.0" 105 + hash = "sha256-ELzmi/s2WqDeUmzSGnfx+ys2Hs28XHqF7vlEzyRotIA=" 106 + [mod."github.com/minio/crc64nvme"] 107 + version = "v1.1.0" 108 + hash = "sha256-OwlE70X91WO4HdbpGsOaB4w12Qrk0duCpfLeAskiqY8=" 109 + [mod."github.com/minio/md5-simd"] 110 + version = "v1.1.2" 111 + hash = "sha256-vykcXvy2VBBAXnJott/XsGTT0gk2UL36JzZKfJ1KAUY=" 112 + [mod."github.com/minio/minio-go/v7"] 113 + version = "v7.0.97" 114 + hash = "sha256-IwF14tWVYjBi28jUG9iFYd4Lpbc7Fvyy0zRzEZ82UEE=" 115 + [mod."github.com/munnerz/goautoneg"] 116 + version = "v0.0.0-20191010083416-a7dc8b61c822" 117 + hash = "sha256-79URDDFenmGc9JZu+5AXHToMrtTREHb3BC84b/gym9Q=" 118 + [mod."github.com/pbnjay/memory"] 119 + version = "v0.0.0-20210728143218-7b4eea64cf58" 120 + hash = "sha256-QI+F1oPLOOtwNp8+m45OOoSfYFs3QVjGzE0rFdpF/IA=" 121 + [mod."github.com/pelletier/go-toml/v2"] 122 + version = "v2.2.4" 123 + hash = "sha256-8qQIPldbsS5RO8v/FW/se3ZsAyvLzexiivzJCbGRg2Q=" 124 + [mod."github.com/philhofer/fwd"] 125 + version = "v1.2.0" 126 + hash = "sha256-cGx2/0QQay46MYGZuamFmU0TzNaFyaO+J7Ddzlr/3dI=" 127 + [mod."github.com/pjbgf/sha1cd"] 128 + version = "v0.5.0" 129 + hash = "sha256-11XBkhdciQGsQ7jEMZ6PgphRKjruTSc7ZxfOwDuPCr8=" 130 + [mod."github.com/pkg/errors"] 131 + version = "v0.9.1" 132 + hash = "sha256-mNfQtcrQmu3sNg/7IwiieKWOgFQOVVe2yXgKBpe/wZw=" 133 + [mod."github.com/pmezard/go-difflib"] 134 + version = "v1.0.0" 135 + hash = "sha256-/FtmHnaGjdvEIKAJtrUfEhV7EVo5A/eYrtdnUkuxLDA=" 136 + [mod."github.com/pquerna/cachecontrol"] 137 + version = "v0.2.0" 138 + hash = "sha256-tuTERCFfwmqPepw/rs5cyv9fArCD30BqgjZqwMV+vzQ=" 139 + [mod."github.com/prometheus/client_golang"] 140 + version = "v1.23.2" 141 + hash = "sha256-3GD4fBFa1tJu8MS4TNP6r2re2eViUE+kWUaieIOQXCg=" 142 + [mod."github.com/prometheus/client_model"] 143 + version = "v0.6.2" 144 + hash = "sha256-q6Fh6v8iNJN9ypD47LjWmx66YITa3FyRjZMRsuRTFeQ=" 145 + [mod."github.com/prometheus/common"] 146 + version = "v0.66.1" 147 + hash = "sha256-bqHPaV9IV70itx63wqwgy2PtxMN0sn5ThVxDmiD7+Tk=" 148 + [mod."github.com/prometheus/procfs"] 149 + version = "v0.16.1" 150 + hash = "sha256-OBCvKlLW2obct35p0L9Q+1ZrxZjpTmbgHMP2rng9hpo=" 151 + [mod."github.com/rs/xid"] 152 + version = "v1.6.0" 153 + hash = "sha256-rJB7h3KuH1DPp5n4dY3MiGnV1Y96A10lf5OUl+MLkzU=" 154 + [mod."github.com/samber/lo"] 155 + version = "v1.52.0" 156 + hash = "sha256-xgMsPJv3rydHH10NZU8wz/DhK2VbbR8ymivOg1ChTp0=" 157 + [mod."github.com/samber/slog-common"] 158 + version = "v0.19.0" 159 + hash = "sha256-OYXVbZML7c3mFClVy8GEnNoWW+4OfcBsxWDtKh1u7B8=" 160 + [mod."github.com/samber/slog-multi"] 161 + version = "v1.6.0" 162 + hash = "sha256-uebbTcvsBP2LdOUIjDptES+HZOXxThnIt3+FKL0qJy4=" 163 + [mod."github.com/sergi/go-diff"] 164 + version = "v1.4.0" 165 + hash = "sha256-rs9NKpv/qcQEMRg7CmxGdP4HGuFdBxlpWf9LbA9wS4k=" 166 + [mod."github.com/stretchr/testify"] 167 + version = "v1.11.1" 168 + hash = "sha256-sWfjkuKJyDllDEtnM8sb/pdLzPQmUYWYtmeWz/5suUc=" 169 + [mod."github.com/tinylib/msgp"] 170 + version = "v1.3.0" 171 + hash = "sha256-PnpndO7k5Yl036vhWJGDsrcz0jsTX8sUiTqm/D3rAVw=" 172 + [mod."github.com/tj/assert"] 173 + version = "v0.0.3" 174 + hash = "sha256-4xhmZcHpUafabaXejE9ucVnGxG/txomvKzBg6cbkusg=" 175 + [mod."github.com/tj/go-redirects"] 176 + version = "v0.0.0-20200911105812-fd1ba1020b37" 177 + hash = "sha256-GpYpxdT4F7PkwGXLo7cYVcIRJrzd1sKHtFDH+bRb6Tk=" 178 + [mod."github.com/valyala/bytebufferpool"] 179 + version = "v1.0.0" 180 + hash = "sha256-I9FPZ3kCNRB+o0dpMwBnwZ35Fj9+ThvITn8a3Jr8mAY=" 181 + [mod."github.com/valyala/fasttemplate"] 182 + version = "v1.2.2" 183 + hash = "sha256-gp+lNXE8zjO+qJDM/YbS6V43HFsYP6PKn4ux1qa5lZ0=" 184 + [mod."go.yaml.in/yaml/v2"] 185 + version = "v2.4.2" 186 + hash = "sha256-oC8RWdf1zbMYCtmR0ATy/kCkhIwPR9UqFZSMOKLVF/A=" 187 + [mod."golang.org/x/crypto"] 188 + version = "v0.46.0" 189 + hash = "sha256-I8N/spcw3/h0DFA+V1WK38HctckWIB9ep93DEVCALxU=" 190 + [mod."golang.org/x/net"] 191 + version = "v0.48.0" 192 + hash = "sha256-oZpddsiJwWCH3Aipa+XXpy7G/xHY5fEagUSok7T0bXE=" 193 + [mod."golang.org/x/sys"] 194 + version = "v0.39.0" 195 + hash = "sha256-dxTBu/JAWUkPbjFIXXRFdhQWyn+YyEpIC+tWqGo0Y6U=" 196 + [mod."golang.org/x/text"] 197 + version = "v0.32.0" 198 + hash = "sha256-9PXtWBKKY9rG4AgjSP4N+I1DhepXhy8SF/vWSIDIoWs=" 199 + [mod."google.golang.org/protobuf"] 200 + version = "v1.36.11" 201 + hash = "sha256-7W+6jntfI/awWL3JP6yQedxqP5S9o3XvPgJ2XxxsIeE=" 202 + [mod."gopkg.in/yaml.v3"] 203 + version = "v3.0.1" 204 + hash = "sha256-FqL9TKYJ0XkNwJFnq9j0VvJ5ZUU1RvH/52h/f5bkYAU="
+3 -1
renovate.json
··· 14 14 "lockFileMaintenance": { 15 15 "enabled": true, 16 16 "automerge": false 17 - } 17 + }, 18 + "semanticCommits": "disabled", 19 + "commitMessagePrefix": "[Renovate]" 18 20 }
+1 -5
src/audit.go
··· 265 265 var _ Backend = (*auditedBackend)(nil) 266 266 267 267 func NewAuditedBackend(backend Backend) Backend { 268 - if config.Feature("audit") { 269 - return &auditedBackend{backend} 270 - } else { 271 - return backend 272 - } 268 + return &auditedBackend{backend} 273 269 } 274 270 275 271 // This function does not retry appending audit records; as such, if it returns an error,
+32 -7
src/auth.go
··· 12 12 "slices" 13 13 "strings" 14 14 "time" 15 + 16 + "golang.org/x/net/idna" 15 17 ) 16 18 17 19 type AuthError struct { ··· 42 44 return nil 43 45 } 44 46 47 + var idnaProfile = idna.New(idna.MapForLookup(), idna.BidiRule()) 48 + 45 49 func GetHost(r *http.Request) (string, error) { 46 - // FIXME: handle IDNA 47 50 host, _, err := net.SplitHostPort(r.Host) 48 51 if err != nil { 49 - // dirty but the go stdlib doesn't have a "split port if present" function 50 52 host = r.Host 51 53 } 52 - if strings.HasPrefix(host, ".") { 54 + // this also rejects invalid characters and labels 55 + host, err = idnaProfile.ToASCII(host) 56 + if err != nil { 57 + if config.Feature("relaxed-idna") { 58 + // unfortunately, the go IDNA library has some significant issues around its 59 + // Unicode TR46 implementation: https://github.com/golang/go/issues/76804 60 + // we would like to allow *just* the _ here, but adding `idna.StrictDomainName(false)` 61 + // would also accept domains like `*.foo.bar` which should clearly be disallowed. 62 + // as a workaround, accept a domain name if it is valid with all `_` characters 63 + // replaced with an alphanumeric character (we use `a`); this allows e.g. `foo_bar.xxx` 64 + // and `foo__bar.xxx`, as well as `_foo.xxx` and `foo_.xxx`. labels starting with 65 + // an underscore are explicitly rejected below. 66 + _, err = idnaProfile.ToASCII(strings.ReplaceAll(host, "_", "a")) 67 + } 68 + if err != nil { 69 + return "", AuthError{http.StatusBadRequest, 70 + fmt.Sprintf("malformed host name %q", host)} 71 + } 72 + } 73 + if strings.HasPrefix(host, ".") || strings.HasPrefix(host, "_") { 53 74 return "", AuthError{http.StatusBadRequest, 54 - fmt.Sprintf("host name %q is reserved", host)} 75 + fmt.Sprintf("reserved host name %q", host)} 55 76 } 56 77 host = strings.TrimSuffix(host, ".") 57 78 return host, nil 58 79 } 59 80 81 + func IsValidProjectName(name string) bool { 82 + return !strings.HasPrefix(name, ".") && !strings.Contains(name, "%") 83 + } 84 + 60 85 func GetProjectName(r *http.Request) (string, error) { 61 86 // path must be either `/` or `/foo/` (`/foo` is accepted as an alias) 62 87 path := strings.TrimPrefix(strings.TrimSuffix(r.URL.Path, "/"), "/") 63 - if path == ".index" || strings.HasPrefix(path, ".index/") { 88 + if !IsValidProjectName(path) { 64 89 return "", AuthError{http.StatusBadRequest, 65 90 fmt.Sprintf("directory name %q is reserved", ".index")} 66 91 } else if strings.Contains(path, "/") { ··· 436 461 } 437 462 438 463 func checkAllowedURLPrefix(repoURL string) error { 439 - if config.Limits.AllowedRepositoryURLPrefixes != nil { 464 + if len(config.Limits.AllowedRepositoryURLPrefixes) > 0 { 440 465 allowedPrefix := false 441 466 repoURL = strings.ToLower(repoURL) 442 467 for _, allowedRepoURLPrefix := range config.Limits.AllowedRepositoryURLPrefixes { ··· 658 683 return auth, nil 659 684 } 660 685 661 - if config.Limits.AllowedRepositoryURLPrefixes != nil { 686 + if len(config.Limits.AllowedRepositoryURLPrefixes) > 0 { 662 687 causes = append(causes, AuthError{http.StatusUnauthorized, "DNS challenge not allowed"}) 663 688 } else { 664 689 // DNS challenge gives absolute authority.
+4 -5
src/config.go
··· 63 63 Insecure bool `toml:"-" env:"insecure"` 64 64 Features []string `toml:"features"` 65 65 LogFormat string `toml:"log-format" default:"text"` 66 - LogLevel string `toml:"log-level" default:"info"` 67 66 Server ServerConfig `toml:"server"` 68 67 Wildcard []WildcardConfig `toml:"wildcard"` 69 68 Fallback FallbackConfig `toml:"fallback"` ··· 74 73 } 75 74 76 75 type ServerConfig struct { 77 - Pages string `toml:"pages" default:"tcp/:3000"` 78 - Caddy string `toml:"caddy" default:"tcp/:3001"` 79 - Metrics string `toml:"metrics" default:"tcp/:3002"` 76 + Pages string `toml:"pages" default:"tcp/localhost:3000"` 77 + Caddy string `toml:"caddy" default:"tcp/localhost:3001"` 78 + Metrics string `toml:"metrics" default:"tcp/localhost:3002"` 80 79 } 81 80 82 81 type WildcardConfig struct { ··· 140 139 // List of domains unconditionally forbidden for uploads. 141 140 ForbiddenDomains []string `toml:"forbidden-domains" default:"[]"` 142 141 // List of allowed repository URL prefixes. Setting this option prohibits uploading archives. 143 - AllowedRepositoryURLPrefixes []string `toml:"allowed-repository-url-prefixes"` 142 + AllowedRepositoryURLPrefixes []string `toml:"allowed-repository-url-prefixes" default:"[]"` 144 143 // List of allowed custom headers. Header name must be in the MIME canonical form, 145 144 // e.g. `Foo-Bar`. Setting this option permits including this custom header in `_headers`, 146 145 // unless it is fundamentally unsafe.
+20
src/extract.go
··· 9 9 "errors" 10 10 "fmt" 11 11 "io" 12 + "math" 12 13 "os" 13 14 "strings" 14 15 ··· 144 145 return nil, UnresolvedRefError{missing} 145 146 } 146 147 148 + // Ensure parent directories exist for all entries. 149 + EnsureLeadingDirectories(manifest) 150 + 147 151 logc.Printf(ctx, 148 152 "reuse: %s recycled, %s transferred\n", 149 153 datasize.ByteSize(dataBytesRecycled).HR(), ··· 153 157 return manifest, nil 154 158 } 155 159 160 + // Used for zstd decompression inside zip files, it is recommended to share this. 161 + var zstdDecomp = zstd.ZipDecompressor() 162 + 156 163 func ExtractZip(ctx context.Context, reader io.Reader, oldManifest *Manifest) (*Manifest, error) { 157 164 data, err := io.ReadAll(reader) 158 165 if err != nil { ··· 163 170 if err != nil { 164 171 return nil, err 165 172 } 173 + 174 + // Support zstd compression inside zip files. 175 + archive.RegisterDecompressor(zstd.ZipMethodWinZip, zstdDecomp) 176 + archive.RegisterDecompressor(zstd.ZipMethodPKWare, zstdDecomp) 166 177 167 178 // Detect and defuse zipbombs. 168 179 var totalSize uint64 169 180 for _, file := range archive.File { 181 + if totalSize+file.UncompressedSize64 < totalSize { 182 + // Would overflow 183 + totalSize = math.MaxUint64 184 + break 185 + } 170 186 totalSize += file.UncompressedSize64 171 187 } 172 188 if totalSize > config.Limits.MaxSiteSize.Bytes() { ··· 213 229 return nil, UnresolvedRefError{missing} 214 230 } 215 231 232 + // Ensure parent directories exist for all entries. 233 + EnsureLeadingDirectories(manifest) 234 + 216 235 logc.Printf(ctx, 217 236 "reuse: %s recycled, %s transferred\n", 218 237 datasize.ByteSize(dataBytesRecycled).HR(), ··· 221 240 222 241 return manifest, nil 223 242 } 243 +
+19 -6
src/fetch.go
··· 23 23 "google.golang.org/protobuf/proto" 24 24 ) 25 25 26 + var ErrRepositoryTooLarge = errors.New("repository too large") 27 + 26 28 func FetchRepository( 27 29 ctx context.Context, repoURL string, branch string, oldManifest *Manifest, 28 30 ) ( ··· 57 59 repo, err = git.CloneContext(ctx, storer, nil, &git.CloneOptions{ 58 60 Bare: true, 59 61 URL: repoURL, 60 - ReferenceName: plumbing.ReferenceName(branch), 62 + ReferenceName: plumbing.NewBranchReferenceName(branch), 61 63 SingleBranch: true, 62 64 Depth: 1, 63 65 Tags: git.NoTags, ··· 152 154 // This will only succeed if a `blob:none` filter isn't supported and we got a full 153 155 // clone despite asking for a partial clone. 154 156 for hash, manifestEntry := range blobsNeeded { 155 - if err := readGitBlob(repo, hash, manifestEntry); err == nil { 156 - dataBytesTransferred += manifestEntry.GetOriginalSize() 157 + if err := readGitBlob(repo, hash, manifestEntry, &dataBytesTransferred); err == nil { 157 158 delete(blobsNeeded, hash) 159 + } else if errors.Is(err, ErrRepositoryTooLarge) { 160 + return nil, err 158 161 } 159 162 } 160 163 ··· 193 196 194 197 // All remaining blobs should now be available. 195 198 for hash, manifestEntry := range blobsNeeded { 196 - if err := readGitBlob(repo, hash, manifestEntry); err != nil { 199 + if err := readGitBlob(repo, hash, manifestEntry, &dataBytesTransferred); err != nil { 197 200 return nil, err 198 201 } 199 - dataBytesTransferred += manifestEntry.GetOriginalSize() 200 202 delete(blobsNeeded, hash) 201 203 } 202 204 } ··· 210 212 return manifest, nil 211 213 } 212 214 213 - func readGitBlob(repo *git.Repository, hash plumbing.Hash, entry *Entry) error { 215 + func readGitBlob( 216 + repo *git.Repository, hash plumbing.Hash, entry *Entry, bytesTransferred *int64, 217 + ) error { 214 218 blob, err := repo.BlobObject(hash) 215 219 if err != nil { 216 220 return fmt.Errorf("git blob %s: %w", hash, err) ··· 239 243 entry.Transform = Transform_Identity.Enum() 240 244 entry.OriginalSize = proto.Int64(blob.Size) 241 245 entry.CompressedSize = proto.Int64(blob.Size) 246 + 247 + *bytesTransferred += blob.Size 248 + if uint64(*bytesTransferred) > config.Limits.MaxSiteSize.Bytes() { 249 + return fmt.Errorf("%w: fetch exceeds %s limit", 250 + ErrRepositoryTooLarge, 251 + config.Limits.MaxSiteSize.HR(), 252 + ) 253 + } 254 + 242 255 return nil 243 256 }
+34 -1
src/main.go
··· 14 14 "net/http/httputil" 15 15 "net/url" 16 16 "os" 17 + "path" 17 18 "runtime/debug" 18 19 "strings" 19 20 "time" ··· 217 218 "display audit log") 218 219 auditRead := flag.String("audit-read", "", 219 220 "extract contents of audit record `id` to files '<id>-*'") 221 + auditRollback := flag.String("audit-rollback", "", 222 + "restore site from contents of audit record `id`") 220 223 auditServer := flag.String("audit-server", "", 221 224 "listen for notifications on `endpoint` and spawn a process for each audit event") 222 225 runMigration := flag.String("run-migration", "", ··· 237 240 *unfreezeDomain != "", 238 241 *auditLog, 239 242 *auditRead != "", 243 + *auditRollback != "", 240 244 *auditServer != "", 241 245 *runMigration != "", 242 246 *traceGarbage, ··· 248 252 if cliOperations > 1 { 249 253 logc.Fatalln(ctx, "-list-blobs, -list-manifests, -get-blob, -get-manifest, -get-archive, "+ 250 254 "-update-site, -freeze-domain, -unfreeze-domain, -audit-log, -audit-read, "+ 251 - "-audit-server, -run-migration, and -trace-garbage are mutually exclusive") 255 + "-audit-rollback, -audit-server, -run-migration, and -trace-garbage are "+ 256 + "mutually exclusive") 252 257 } 253 258 254 259 if *configTomlPath != "" && *noConfig { ··· 480 485 } 481 486 482 487 if err = ExtractAuditRecord(ctx, id, record, "."); err != nil { 488 + logc.Fatalln(ctx, err) 489 + } 490 + 491 + case *auditRollback != "": 492 + ctx = WithPrincipal(ctx) 493 + GetPrincipal(ctx).CliAdmin = proto.Bool(true) 494 + 495 + id, err := ParseAuditID(*auditRollback) 496 + if err != nil { 497 + logc.Fatalln(ctx, err) 498 + } 499 + 500 + record, err := backend.QueryAuditLog(ctx, id) 501 + if err != nil { 502 + logc.Fatalln(ctx, err) 503 + } 504 + 505 + if record.GetManifest() == nil || record.GetDomain() == "" || record.GetProject() == "" { 506 + logc.Fatalln(ctx, "no manifest in audit record") 507 + } 508 + 509 + webRoot := path.Join(record.GetDomain(), record.GetProject()) 510 + err = backend.StageManifest(ctx, record.GetManifest()) 511 + if err != nil { 512 + logc.Fatalln(ctx, err) 513 + } 514 + err = backend.CommitManifest(ctx, webRoot, record.GetManifest(), ModifyManifestOptions{}) 515 + if err != nil { 483 516 logc.Fatalln(ctx, err) 484 517 } 485 518
+28 -4
src/manifest.go
··· 144 144 return fmt.Errorf("%s: %s", pathName, cause) 145 145 } 146 146 147 + // EnsureLeadingDirectories adds directory entries for any parent directories 148 + // that are implicitly referenced by files in the manifest but don't have 149 + // explicit directory entries. (This can be the case if an archive is created 150 + // via globs rather than including a whole directory.) 151 + func EnsureLeadingDirectories(manifest *Manifest) { 152 + for name := range manifest.Contents { 153 + for dir := path.Dir(name); dir != "." && dir != ""; dir = path.Dir(dir) { 154 + if _, exists := manifest.Contents[dir]; !exists { 155 + AddDirectory(manifest, dir) 156 + } 157 + } 158 + } 159 + } 160 + 147 161 func GetProblemReport(manifest *Manifest) []string { 148 162 var report []string 149 163 for _, problem := range manifest.Problems { 150 164 report = append(report, 151 - fmt.Sprintf("%s: %s", problem.GetPath(), problem.GetCause())) 165 + fmt.Sprintf("/%s: %s", problem.GetPath(), problem.GetCause())) 152 166 } 153 167 return report 154 168 } ··· 283 297 return nil 284 298 } 285 299 300 + var ErrSiteTooLarge = errors.New("site too large") 286 301 var ErrManifestTooLarge = errors.New("manifest too large") 287 302 288 303 // Uploads inline file data over certain size to the storage backend. Returns a copy of ··· 325 340 } 326 341 } 327 342 328 - // Compute the deduplicated storage size. 329 - var blobSizes = make(map[string]int64) 330 - for _, entry := range manifest.Contents { 343 + // Compute the total and deduplicated storage size. 344 + totalSize := int64(0) 345 + blobSizes := map[string]int64{} 346 + for _, entry := range extManifest.Contents { 347 + totalSize += entry.GetOriginalSize() 331 348 if entry.GetType() == Type_ExternalFile { 332 349 blobSizes[string(entry.Data)] = entry.GetCompressedSize() 333 350 } 351 + } 352 + if uint64(totalSize) > config.Limits.MaxSiteSize.Bytes() { 353 + return nil, fmt.Errorf("%w: contents size %s exceeds %s limit", 354 + ErrSiteTooLarge, 355 + datasize.ByteSize(totalSize).HR(), 356 + config.Limits.MaxSiteSize.HR(), 357 + ) 334 358 } 335 359 for _, blobSize := range blobSizes { 336 360 *extManifest.StoredSize += blobSize
+68 -58
src/observe.go
··· 13 13 "os" 14 14 "runtime/debug" 15 15 "strconv" 16 - "strings" 17 16 "sync" 18 17 "time" 19 18 ··· 52 51 return os.Getenv("SENTRY_DSN") != "" 53 52 } 54 53 54 + func chainSentryMiddleware( 55 + middleware ...func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event, 56 + ) func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { 57 + return func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { 58 + for idx := 0; idx < len(middleware) && event != nil; idx++ { 59 + event = middleware[idx](event, hint) 60 + } 61 + return event 62 + } 63 + } 64 + 65 + // sensitiveHTTPHeaders extends the list of sensitive headers defined in the Sentry Go SDK with our 66 + // own application-specific header field names. 67 + var sensitiveHTTPHeaders = map[string]struct{}{ 68 + "Forge-Authorization": {}, 69 + } 70 + 71 + // scrubSentryEvent removes sensitive HTTP header fields from the Sentry event. 72 + func scrubSentryEvent(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { 73 + if event.Request != nil && event.Request.Headers != nil { 74 + for key := range event.Request.Headers { 75 + if _, ok := sensitiveHTTPHeaders[key]; ok { 76 + delete(event.Request.Headers, key) 77 + } 78 + } 79 + } 80 + return event 81 + } 82 + 83 + // sampleSentryEvent returns a function that discards a Sentry event according to the sample rate, 84 + // unless the associated HTTP request triggers a mutation or it took too long to produce a response, 85 + // in which case the event is never discarded. 86 + func sampleSentryEvent(sampleRate float64) func(*sentry.Event, *sentry.EventHint) *sentry.Event { 87 + return func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { 88 + newSampleRate := sampleRate 89 + if event.Request != nil { 90 + switch event.Request.Method { 91 + case "PUT", "POST", "DELETE": 92 + newSampleRate = 1 93 + } 94 + } 95 + duration := event.Timestamp.Sub(event.StartTime) 96 + threshold := time.Duration(config.Observability.SlowResponseThreshold) 97 + if duration >= threshold { 98 + newSampleRate = 1 99 + } 100 + if rand.Float64() < newSampleRate { 101 + return event 102 + } 103 + return nil 104 + } 105 + } 106 + 55 107 func InitObservability() { 56 108 debug.SetPanicOnFault(true) 57 109 ··· 62 114 63 115 logHandlers := []slog.Handler{} 64 116 65 - logLevel := slog.LevelInfo 66 - switch strings.ToLower(config.LogLevel) { 67 - case "debug": 68 - logLevel = slog.LevelDebug 69 - case "info": 70 - logLevel = slog.LevelInfo 71 - case "warn": 72 - logLevel = slog.LevelWarn 73 - case "error": 74 - logLevel = slog.LevelError 75 - default: 76 - log.Println("unknown log level", config.LogLevel) 77 - } 78 - 79 117 switch config.LogFormat { 80 118 case "none": 81 119 // nothing to do 82 120 case "text": 83 121 logHandlers = append(logHandlers, 84 - slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel})) 122 + slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{})) 85 123 case "json": 86 124 logHandlers = append(logHandlers, 87 - slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel})) 125 + slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{})) 88 126 default: 89 127 log.Println("unknown log format", config.LogFormat) 90 128 } ··· 111 149 enableTracing := false 112 150 if value, err := strconv.ParseBool(os.Getenv("SENTRY_TRACING")); err == nil { 113 151 enableTracing = value 152 + } 153 + 154 + tracesSampleRate := 1.00 155 + switch environment { 156 + case "development", "staging": 157 + default: 158 + tracesSampleRate = 0.05 114 159 } 115 160 116 161 options := sentry.ClientOptions{} ··· 118 163 options.Environment = environment 119 164 options.EnableLogs = enableLogs 120 165 options.EnableTracing = enableTracing 121 - options.TracesSampleRate = 1 122 - switch environment { 123 - case "development", "staging": 124 - default: 125 - options.BeforeSendTransaction = func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { 126 - sampleRate := 0.05 127 - if trace, ok := event.Contexts["trace"]; ok { 128 - if data, ok := trace["data"].(map[string]any); ok { 129 - if method, ok := data["http.request.method"].(string); ok { 130 - switch method { 131 - case "PUT", "DELETE", "POST": 132 - sampleRate = 1 133 - default: 134 - duration := event.Timestamp.Sub(event.StartTime) 135 - threshold := time.Duration(config.Observability.SlowResponseThreshold) 136 - if duration >= threshold { 137 - sampleRate = 1 138 - } 139 - } 140 - } 141 - } 142 - } 143 - if rand.Float64() < sampleRate { 144 - return event 145 - } 146 - return nil 147 - } 148 - } 166 + options.TracesSampleRate = 1 // use our own custom sampling logic 167 + options.BeforeSend = scrubSentryEvent 168 + options.BeforeSendTransaction = chainSentryMiddleware( 169 + sampleSentryEvent(tracesSampleRate), 170 + scrubSentryEvent, 171 + ) 149 172 if err := sentry.Init(options); err != nil { 150 173 log.Fatalf("sentry: %s\n", err) 151 174 } ··· 153 176 if enableLogs { 154 177 logHandlers = append(logHandlers, sentryslog.Option{ 155 178 AddSource: true, 156 - LogLevel: levelsFromMinimum(logLevel), 157 179 }.NewSentryHandler(context.Background())) 158 180 } 159 181 } 160 182 161 183 slog.SetDefault(slog.New(slogmulti.Fanout(logHandlers...))) 162 - } 163 - 164 - // From sentryslog, because for some reason they don't make it public. 165 - func levelsFromMinimum(minLevel slog.Level) []slog.Level { 166 - allLevels := []slog.Level{slog.LevelDebug, slog.LevelInfo, slog.LevelWarn, slog.LevelError, sentryslog.LevelFatal} 167 - var result []slog.Level 168 - for _, level := range allLevels { 169 - if level >= minLevel { 170 - result = append(result, level) 171 - } 172 - } 173 - return result 174 184 } 175 185 176 186 func FiniObservability() {
+39 -22
src/pages.go
··· 132 132 err = nil 133 133 sitePath = strings.TrimPrefix(r.URL.Path, "/") 134 134 if projectName, projectPath, hasProjectSlash := strings.Cut(sitePath, "/"); projectName != "" { 135 - var projectManifest *Manifest 136 - var projectMetadata ManifestMetadata 137 - projectManifest, projectMetadata, err = backend.GetManifest( 138 - r.Context(), makeWebRoot(host, projectName), 139 - GetManifestOptions{BypassCache: bypassCache}, 140 - ) 141 - if err == nil { 142 - if !hasProjectSlash { 143 - writeRedirect(w, http.StatusFound, r.URL.Path+"/") 144 - return nil 135 + if IsValidProjectName(projectName) { 136 + var projectManifest *Manifest 137 + var projectMetadata ManifestMetadata 138 + projectManifest, projectMetadata, err = backend.GetManifest( 139 + r.Context(), makeWebRoot(host, projectName), 140 + GetManifestOptions{BypassCache: bypassCache}, 141 + ) 142 + if err == nil { 143 + if !hasProjectSlash { 144 + writeRedirect(w, http.StatusFound, r.URL.Path+"/") 145 + return nil 146 + } 147 + sitePath, manifest, metadata = projectPath, projectManifest, projectMetadata 145 148 } 146 - sitePath, manifest, metadata = projectPath, projectManifest, projectMetadata 147 149 } 148 150 } 149 151 if manifest == nil && (err == nil || errors.Is(err, ErrObjectNotFound)) { ··· 214 216 // we only offer `/.git-pages/archive.tar` and not the `.tar.gz`/`.tar.zst` variants 215 217 // because HTTP can already request compression using the `Content-Encoding` mechanism 216 218 acceptedEncodings := ParseAcceptEncodingHeader(r.Header.Get("Accept-Encoding")) 219 + w.Header().Add("Vary", "Accept-Encoding") 217 220 negotiated := acceptedEncodings.Negotiate("zstd", "gzip", "identity") 218 221 if negotiated != "" { 219 222 w.Header().Set("Content-Encoding", negotiated) ··· 325 328 326 329 var offeredEncodings []string 327 330 acceptedEncodings := ParseAcceptEncodingHeader(r.Header.Get("Accept-Encoding")) 331 + w.Header().Add("Vary", "Accept-Encoding") 328 332 negotiatedEncoding := true 329 333 switch entry.GetTransform() { 330 334 case Transform_Identity: ··· 415 419 io.Copy(w, reader) 416 420 } 417 421 } else { 418 - // consider content fresh for 60 seconds (the same as the freshness interval of 419 - // manifests in the S3 backend), and use stale content anyway as long as it's not 420 - // older than a hour; while it is cheap to handle If-Modified-Since queries 421 - // server-side, on the client `max-age=0, must-revalidate` causes every resource 422 - // to block the page load every time 423 - w.Header().Set("Cache-Control", "max-age=60, stale-while-revalidate=3600") 424 - // see https://web.dev/articles/stale-while-revalidate for details 422 + if _, hasCacheControl := w.Header()["Cache-Control"]; !hasCacheControl { 423 + // consider content fresh for 60 seconds (the same as the freshness interval of 424 + // manifests in the S3 backend), and use stale content anyway as long as it's not 425 + // older than a hour; while it is cheap to handle If-Modified-Since queries 426 + // server-side, on the client `max-age=0, must-revalidate` causes every resource 427 + // to block the page load every time 428 + w.Header().Set("Cache-Control", "max-age=60, stale-while-revalidate=3600") 429 + // see https://web.dev/articles/stale-while-revalidate for details 430 + } 425 431 426 432 // http.ServeContent handles conditional requests and range requests 427 433 http.ServeContent(w, r, entryPath, mtime, reader) ··· 597 603 598 604 switch result.outcome { 599 605 case UpdateError: 600 - if errors.Is(result.err, ErrManifestTooLarge) { 601 - w.WriteHeader(http.StatusRequestEntityTooLarge) 606 + if errors.Is(result.err, ErrSiteTooLarge) { 607 + w.WriteHeader(http.StatusUnprocessableEntity) 608 + } else if errors.Is(result.err, ErrManifestTooLarge) { 609 + w.WriteHeader(http.StatusUnprocessableEntity) 602 610 } else if errors.Is(result.err, errArchiveFormat) { 603 611 w.WriteHeader(http.StatusUnsupportedMediaType) 604 612 } else if errors.Is(result.err, ErrArchiveTooLarge) { 605 613 w.WriteHeader(http.StatusRequestEntityTooLarge) 614 + } else if errors.Is(result.err, ErrRepositoryTooLarge) { 615 + w.WriteHeader(http.StatusUnprocessableEntity) 606 616 } else if errors.Is(result.err, ErrMalformedPatch) { 607 617 w.WriteHeader(http.StatusUnprocessableEntity) 608 618 } else if errors.Is(result.err, ErrPreconditionFailed) { ··· 762 772 result := UpdateFromRepository(ctx, webRoot, repoURL, auth.branch) 763 773 resultChan <- result 764 774 observeSiteUpdate("webhook", &result) 765 - }(r.Context()) 775 + }(context.WithoutCancel(r.Context())) 766 776 767 777 var result UpdateResult 768 778 select { ··· 810 820 // any intentional deviation is an opportunity to miss an issue that will affect our 811 821 // visitors but not our health checks. 812 822 if r.Header.Get("Health-Check") == "" { 813 - logc.Println(r.Context(), "pages:", r.Method, r.Host, r.URL, r.Header.Get("Content-Type")) 823 + var mediaType string 824 + switch r.Method { 825 + case "HEAD", "GET": 826 + mediaType = r.Header.Get("Accept") 827 + default: 828 + mediaType = r.Header.Get("Content-Type") 829 + } 830 + logc.Println(r.Context(), "pages:", r.Method, r.Host, r.URL, mediaType) 814 831 if region := os.Getenv("FLY_REGION"); region != "" { 815 832 machine_id := os.Getenv("FLY_MACHINE_ID") 816 833 w.Header().Add("Server", fmt.Sprintf("git-pages (fly.io; %s; %s)", region, machine_id))
+55
src/pages_test.go
··· 1 + package git_pages 2 + 3 + import ( 4 + "net/http" 5 + "strings" 6 + "testing" 7 + ) 8 + 9 + func checkHost(t *testing.T, host string, expectOk string, expectErr string) { 10 + host, err := GetHost(&http.Request{Host: host}) 11 + if expectErr != "" { 12 + if err == nil || !strings.HasPrefix(err.Error(), expectErr) { 13 + t.Errorf("%s: expect err %s, got err %s", host, expectErr, err) 14 + } 15 + } 16 + if expectOk != "" { 17 + if err != nil { 18 + t.Errorf("%s: expect ok %s, got err %s", host, expectOk, err) 19 + } else if host != expectOk { 20 + t.Errorf("%s: expect ok %s, got ok %s", host, expectOk, host) 21 + } 22 + } 23 + } 24 + 25 + func TestHelloName(t *testing.T) { 26 + config = &Config{Features: []string{}} 27 + 28 + checkHost(t, "foo.bar", "foo.bar", "") 29 + checkHost(t, "foo-baz.bar", "foo-baz.bar", "") 30 + checkHost(t, "foo--baz.bar", "foo--baz.bar", "") 31 + checkHost(t, "foo.bar.", "foo.bar", "") 32 + checkHost(t, ".foo.bar", "", "reserved host name") 33 + checkHost(t, "..foo.bar", "", "reserved host name") 34 + 35 + checkHost(t, "รŸ.bar", "xn--zca.bar", "") 36 + checkHost(t, "xn--zca.bar", "xn--zca.bar", "") 37 + 38 + checkHost(t, "foo-.bar", "", "malformed host name") 39 + checkHost(t, "-foo.bar", "", "malformed host name") 40 + checkHost(t, "foo_.bar", "", "malformed host name") 41 + checkHost(t, "_foo.bar", "", "malformed host name") 42 + checkHost(t, "foo_baz.bar", "", "malformed host name") 43 + checkHost(t, "foo__baz.bar", "", "malformed host name") 44 + checkHost(t, "*.foo.bar", "", "malformed host name") 45 + 46 + config = &Config{Features: []string{"relaxed-idna"}} 47 + 48 + checkHost(t, "foo-.bar", "", "malformed host name") 49 + checkHost(t, "-foo.bar", "", "malformed host name") 50 + checkHost(t, "foo_.bar", "foo_.bar", "") 51 + checkHost(t, "_foo.bar", "", "reserved host name") 52 + checkHost(t, "foo_baz.bar", "foo_baz.bar", "") 53 + checkHost(t, "foo__baz.bar", "foo__baz.bar", "") 54 + checkHost(t, "*.foo.bar", "", "malformed host name") 55 + }
+29
src/signal.go
··· 1 + // See https://pkg.go.dev/os/signal#hdr-Windows for a description of what this module 2 + // will do on Windows (tl;dr nothing calls the reload handler, the interrupt handler works 3 + // more or less how you'd expect). 4 + 5 + package git_pages 6 + 7 + import ( 8 + "os" 9 + "os/signal" 10 + "syscall" 11 + ) 12 + 13 + func OnReload(handler func()) { 14 + sighup := make(chan os.Signal, 1) 15 + signal.Notify(sighup, syscall.SIGHUP) 16 + go func() { 17 + for { 18 + <-sighup 19 + handler() 20 + } 21 + }() 22 + } 23 + 24 + func WaitForInterrupt() { 25 + sigint := make(chan os.Signal, 1) 26 + signal.Notify(sigint, syscall.SIGINT, syscall.SIGTERM) 27 + <-sigint 28 + signal.Stop(sigint) 29 + }
-13
src/signal_other.go
··· 1 - //go:build !unix 2 - 3 - package git_pages 4 - 5 - func OnReload(handler func()) { 6 - // not implemented 7 - } 8 - 9 - func WaitForInterrupt() { 10 - for { 11 - // Ctrl+C not supported 12 - } 13 - }
-27
src/signal_posix.go
··· 1 - //go:build unix 2 - 3 - package git_pages 4 - 5 - import ( 6 - "os" 7 - "os/signal" 8 - "syscall" 9 - ) 10 - 11 - func OnReload(handler func()) { 12 - sighup := make(chan os.Signal, 1) 13 - signal.Notify(sighup, syscall.SIGHUP) 14 - go func() { 15 - for { 16 - <-sighup 17 - handler() 18 - } 19 - }() 20 - } 21 - 22 - func WaitForInterrupt() { 23 - sigint := make(chan os.Signal, 1) 24 - signal.Notify(sigint, syscall.SIGINT) 25 - <-sigint 26 - signal.Stop(sigint) 27 - }
+3
src/update.go
··· 182 182 // `*Manifest` objects, which should never be mutated. 183 183 newManifest := &Manifest{} 184 184 proto.Merge(newManifest, oldManifest) 185 + newManifest.RepoUrl = nil 186 + newManifest.Branch = nil 187 + newManifest.Commit = nil 185 188 if err := ApplyTarPatch(newManifest, reader, parents); err != nil { 186 189 return nil, err 187 190 } else {