From 7079f383d4580138fd9a3955d1e6ca061cc13bdc Mon Sep 17 00:00:00 2001 From: Fabian Gasper Date: Sun, 12 Jul 2026 18:32:01 +0200 Subject: [PATCH] first commit --- .gitignore | 2 + Dockerfile | 11 +++++ README.md | 40 +++++++++++++++++ build.sh | 17 +++++++ compose.yml | 11 +++++ config.go | 50 +++++++++++++++++++++ env.example | 9 ++++ go.mod | 32 +++++++++++++ go.sum | 108 ++++++++++++++++++++++++++++++++++++++++++++ main.go | 121 +++++++++++++++++++++++++++++++++++++++++++++++++ webhook.go | 127 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 528 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100755 build.sh create mode 100644 compose.yml create mode 100644 config.go create mode 100644 env.example create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 webhook.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6a0d56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +dist/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ee50e6f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.25-alpine AS builder +WORKDIR /app +COPY main.go webhook.go config.go . +RUN go mod init varde && \ + go mod tidy && \ + CGO_ENABLED=0 GOOS=linux go build -o server main.go webhook.go config.go + +FROM scratch +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /app/server /server +CMD ["/server"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..008c094 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# homedeploy +Homedeploy is a small webservice written in go. +It's intended to just serve static sites, sutch as generated by goHugo. + +## Features +### Serve static files +The main usecase is serving static files. +Just drop your files into the defined folder and start the service. + +### Serve WDK Keys for openPGP +The second option is to serve WDK Keys fro openPGP. The domain path is already hardcoded, so you just have to add your hash keys. + +## Deploy the service +The intended way to reploy the service is with docker compose. + +### compose.yml +```bash +services: + homepage: + container_name: homepage + image: git.lojr.de/ilvoen/heimdeploy:latest + ports: + - ports 80:80 + env_file: .env + volumes: + - ./content:/app/content + - ./keys:/app/keys:ro + restart: unless-stopped +``` + +### .env +```bash +CONTENT_DIR=/app/content +KEYS_DIR=/app/keys +PORT=80 +ENABLE_WEBHOOK=false +REPO_URL= +REPO_TOKEN= +WEBHOOK_SECRET= +``` \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..be17ade --- /dev/null +++ b/build.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e + +VERSION=${1:?"Usage: ./build.sh e.g. ./build.sh 1.0.0"} +REGISTRY="git.lojr.de/ilvoen/varde" +PLATFORMS="linux/amd64,linux/arm64,linux/arm/v7,linux/386" + +echo "→ Building static variant..." +docker buildx build \ + --platform ${PLATFORMS} \ + -f Dockerfile \ + -t ${REGISTRY}:${VERSION} \ + -t ${REGISTRY}:latest \ + --push . + +echo "✓ Done! Pushed:" +echo " ${REGISTRY}:${VERSION} + latest" diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..77906d4 --- /dev/null +++ b/compose.yml @@ -0,0 +1,11 @@ +services: + homepage: + container_name: homepage + image: git.lojr.de/ilvoen/varde:latest + ports: + - 80:80 + env_file: .env + volumes: + - ./content:/app/content + - ./keys:/app/keys:ro + restart: unless-stopped diff --git a/config.go b/config.go new file mode 100644 index 0000000..73af638 --- /dev/null +++ b/config.go @@ -0,0 +1,50 @@ +package main + +import ( + "flag" + "os" +) + +type Config struct { + ContentDir string + KeysDir string + Port string + RepoURL string + RepoToken string + WebhookSecret string + NtfyURL string + NtfyTopic string + EnableWebhook bool +} + +func loadConfig() *Config { + cfg := &Config{} + + flag.StringVar(&cfg.ContentDir, "content-dir", envOrDefault("CONTENT_DIR", "./content"), "directory to serve as content") + flag.StringVar(&cfg.KeysDir, "keys-dir", envOrDefault("KEYS_DIR", "./keys"), "directory to serve as WKD keys") + flag.StringVar(&cfg.Port, "port", envOrDefault("PORT", "80"), "port to listen on") + flag.StringVar(&cfg.RepoURL, "repo-url", envOrDefault("REPO_URL", ""), "git repo URL for webhook auto-update") + flag.StringVar(&cfg.RepoToken, "repo-token", envOrDefault("REPO_TOKEN", ""), "git repo access token") + flag.StringVar(&cfg.WebhookSecret, "webhook-secret", envOrDefault("WEBHOOK_SECRET", ""), "HMAC secret for webhook validation") + flag.StringVar(&cfg.NtfyURL, "ntfy-url", envOrDefault("NTFY_URL", ""), "ntfy server URL for failure notifications") + flag.StringVar(&cfg.NtfyTopic, "ntfy-topic", envOrDefault("NTFY_TOPIC", ""), "ntfy topic for failure notifications") + flag.BoolVar(&cfg.EnableWebhook, "webhook", envOrDefaultBool("ENABLE_WEBHOOK", false), "enable git-webhook auto-update feature") + + flag.Parse() + return cfg +} + +func envOrDefault(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} + +func envOrDefaultBool(key string, def bool) bool { + v := os.Getenv(key) + if v == "" { + return def + } + return v == "true" || v == "1" +} diff --git a/env.example b/env.example new file mode 100644 index 0000000..2bab4eb --- /dev/null +++ b/env.example @@ -0,0 +1,9 @@ +CONTENT_DIR=/app/content +KEYS_DIR=/app/keys +PORT=80 +ENABLE_WEBHOOK=false +REPO_URL= +REPO_TOKEN= +WEBHOOK_SECRET= +NTFY_URL=https:// +NTFY_TOPIC=varde-webhook diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b2bbdc1 --- /dev/null +++ b/go.mod @@ -0,0 +1,32 @@ +module varde + +go 1.25.0 + +require ( + github.com/go-git/go-git/v5 v5.19.1 + github.com/gorilla/mux v1.8.1 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.9.0 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/pjbgf/sha1cd v0.6.0 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/time v0.15.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0442645 --- /dev/null +++ b/go.sum @@ -0,0 +1,108 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= +github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= +github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= +github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..197a24f --- /dev/null +++ b/main.go @@ -0,0 +1,121 @@ +package main + +import ( + "log" + "net/http" + "os" + "strings" + "time" + "context" + "os/signal" + "syscall" + + "github.com/gorilla/mux" + "golang.org/x/time/rate" +) + +func noListing(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if strings.HasSuffix(req.URL.Path, "/") { + http.NotFound(w, req) + return + } + next.ServeHTTP(w, req) + }) +} + +func securityHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") + next.ServeHTTP(w, req) + }) +} + +var webhookLimiter = rate.NewLimiter(rate.Every(10*time.Second), 1) + +func rateLimit(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if !webhookLimiter.Allow() { + log.Println("webhook rate limit exceeded") + w.WriteHeader(http.StatusTooManyRequests) + return + } + next.ServeHTTP(w, req) + }) +} + +func wkdHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Access-Control-Allow-Origin", "*") + next.ServeHTTP(w, req) + }) +} + +func healthHandler(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) +} + +func main() { + cfg := loadConfig() + + if cfg.EnableWebhook { + cloneOrOpen(cfg) + } + + contentFS := http.FileServer(http.Dir(cfg.ContentDir)) + keysFS := http.FileServer(http.Dir(cfg.KeysDir)) + + + r := mux.NewRouter() + r.Use(securityHeaders) + + + api := r.PathPrefix("/api/").Subrouter() + api.HandleFunc("/healthz", healthHandler).Methods("GET", "HEAD") + + if cfg.EnableWebhook { + r.Handle("/webhook", rateLimit(webhookHandler(cfg))).Methods("POST") + } + + // Eigener Bereich für .well-known: kein Listing, WKD-Header + wkd := r.PathPrefix("/.well-known/").Subrouter() + wkd.Use(noListing) + wkd.Use(wkdHeaders) + wkd.PathPrefix("/").Handler(keysFS) + + // Alles andere: normaler Fileserver, Listing erlaubt, hier kommt später zB /downloads/ rein + r.PathPrefix("/").Handler(contentFS) + + srv := &http.Server{ + Addr: ":" + cfg.Port, + Handler: r, + } + + // Server in eigener Goroutine starten, damit main() weiterlaufen kann + go func() { + log.Printf("Listening on :%s, content=%s keys=%s (webhook: %v)", cfg.Port, cfg.ContentDir, cfg.KeysDir, cfg.EnableWebhook) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("server error: %v", err) + } + }() + + // Auf SIGTERM/SIGINT warten + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT) + <-stop + + log.Println("Shutdown signal received, draining connections...") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + log.Printf("graceful shutdown failed: %v", err) + } else { + log.Println("Server shut down cleanly") + } +} diff --git a/webhook.go b/webhook.go new file mode 100644 index 0000000..c7b5167 --- /dev/null +++ b/webhook.go @@ -0,0 +1,127 @@ +package main + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "io" + "log" + "net/http" + "os" + "bytes" + "fmt" + "time" + + "github.com/go-git/go-git/v5" + githttp "github.com/go-git/go-git/v5/plumbing/transport/http" +) + +var repo *git.Repository + +func cloneOrOpen(cfg *Config) { + auth := &githttp.BasicAuth{ + Username: "x-token", + Password: cfg.RepoToken, + } + + if _, err := os.Stat(cfg.ContentDir + "/.git"); os.IsNotExist(err) { + log.Println("Cloning repo...") + r, err := git.PlainClone(cfg.ContentDir, false, &git.CloneOptions{ + URL: cfg.RepoURL, + Auth: auth, + }) + if err != nil { + notifyFailure(cfg, "clone failed", err) // NEU + log.Fatalf("clone failed: %v", err) + } + repo = r + } else { + log.Println("Opening existing repo...") + r, err := git.PlainOpen(cfg.ContentDir) + if err != nil { + notifyFailure(cfg, "open failed", err) // NEU + log.Fatalf("open failed: %v", err) + } + repo = r + } +} + +func pull(cfg *Config) error { + w, err := repo.Worktree() + if err != nil { + return err + } + err = w.Pull(&git.PullOptions{ + Auth: &githttp.BasicAuth{ + Username: "x-token", + Password: cfg.RepoToken, + }, + }) + if err == git.NoErrAlreadyUpToDate { + return nil + } + return err +} + +func validateSignature(secret, signature string, body []byte) bool { + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write(body) + expected := hex.EncodeToString(mac.Sum(nil)) + return hmac.Equal([]byte(expected), []byte(signature)) +} + +func webhookHandler(cfg *Config) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + sig := r.Header.Get("X-Gitea-Signature") + if !validateSignature(cfg.WebhookSecret, sig, body) { + log.Println("invalid webhook signature") + w.WriteHeader(http.StatusUnauthorized) + return + } + + if err := pull(cfg); err != nil { + notifyFailure(cfg, "pull failed", err) // NEU + log.Printf("pull failed: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + log.Println("pulled new content") + w.WriteHeader(http.StatusOK) + } +} + +func notifyFailure(cfg *Config, context string, err error) { + if cfg.NtfyURL == "" || cfg.NtfyTopic == "" { + return + } + + msg := fmt.Sprintf("%s: %v", context, err) + url := fmt.Sprintf("%s/%s", cfg.NtfyURL, cfg.NtfyTopic) + + req, reqErr := http.NewRequest("POST", url, bytes.NewBufferString(msg)) + if reqErr != nil { + log.Printf("ntfy request build failed: %v", reqErr) + return + } + req.Header.Set("Title", "Varde Webhook Failure") + req.Header.Set("Priority", "high") + + client := &http.Client{Timeout: 5 * time.Second} + resp, sendErr := client.Do(req) + if sendErr != nil { + log.Printf("ntfy notification failed: %v", sendErr) + return + } + defer resp.Body.Close() + + if resp.StatusCode >= 300 { + log.Printf("ntfy notification returned status %d", resp.StatusCode) + } +}