commit 7079f383d4580138fd9a3955d1e6ca061cc13bdc Author: Fabian Gasper Date: Sun Jul 12 18:32:01 2026 +0200 first commit 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) + } +}