Initial commit

This commit is contained in:
Dakota Walsh 2026-01-20 10:20:19 +13:00
commit 31f27c5092
4 changed files with 207 additions and 0 deletions

1
config.toml Normal file
View file

@ -0,0 +1 @@
Addr = ":1112"

11
go.mod Normal file
View file

@ -0,0 +1,11 @@
module git.nilsu.org/kota/donation-notifications
go 1.25.5
require (
github.com/BurntSushi/toml v1.6.0
github.com/justinas/alice v1.2.0
github.com/throttled/throttled/v2 v2.15.0
)
require github.com/hashicorp/golang-lru v0.5.4 // indirect

18
go.sum Normal file
View file

@ -0,0 +1,18 @@
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo=
github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA=
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/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/throttled/throttled/v2 v2.15.0 h1:7XLCECtmEx+Yz/e5opBNff9cPGpH0ia0xEj5kyDPotI=
github.com/throttled/throttled/v2 v2.15.0/go.mod h1:JlfSSSYoM/bjFoW2sCATGxJJXggjO67DFQu9xduGAWE=
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

177
main.go Normal file
View file

@ -0,0 +1,177 @@
package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
"runtime/debug"
"github.com/BurntSushi/toml"
"github.com/justinas/alice"
"github.com/throttled/throttled/v2"
throttledstore "github.com/throttled/throttled/v2/store/memstore"
)
type application struct {
infoLog *log.Logger
errLog *log.Logger
rateLimiter *throttled.HTTPRateLimiterCtx
}
func main() {
cfgPath := flag.String(
"config",
"/etc/donation-notification/config.toml",
"Path to configuration file",
)
flag.Parse()
infoLog := log.New(os.Stdout, "INFO ", log.Ldate|log.Ltime)
errLog := log.New(os.Stdout, "ERROR ", log.Ldate|log.Ltime|log.Lshortfile)
cfg, err := loadConfig(*cfgPath)
if err != nil {
errLog.Fatalln(err)
}
// Set up HTTP request throttling.
tstore, err := throttledstore.NewCtx(65536)
if err != nil {
errLog.Fatal(err)
}
quota := throttled.RateQuota{
MaxRate: throttled.PerMin(1),
MaxBurst: 2,
}
throttler, err := throttled.NewGCRARateLimiterCtx(tstore, quota)
if err != nil {
errLog.Fatal(err)
}
rateLimiter := &throttled.HTTPRateLimiterCtx{
RateLimiter: throttler,
VaryBy: &throttled.VaryBy{
Path: true,
Method: true,
Headers: []string{"X-Forwarded-For"},
},
}
app := &application{
infoLog: infoLog,
errLog: errLog,
rateLimiter: rateLimiter,
}
srv := &http.Server{
Addr: cfg.Addr,
ErrorLog: errLog,
Handler: app.routes(),
}
infoLog.Println("listening on", cfg.Addr)
err = srv.ListenAndServe()
errLog.Fatalln(err)
}
type config struct {
Addr string
}
func loadConfig(path string) (config, error) {
cfg := config{
Addr: ":1112",
}
_, err := toml.DecodeFile(path, &cfg)
if err != nil {
return config{}, fmt.Errorf("failed loading config: %v", err)
}
return cfg, nil
}
func (app *application) routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/webhook/stripe", app.stripe)
mux.HandleFunc("/webhook/paypal", app.paypal)
standard := alice.New(
app.recoverPanic,
app.rateLimiter.RateLimit,
app.logRequest,
app.secureHeaders,
)
return standard.Then(mux)
}
func (app *application) stripe(w http.ResponseWriter, r *http.Request) {}
func (app *application) paypal(w http.ResponseWriter, r *http.Request) {}
// logRequest is a middleware that prints each request to the info log.
func (app *application) logRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
addr := r.RemoteAddr
// Use correct address if behind proxy.
if proxyAddr := r.Header.Get("X-Forwarded-For"); proxyAddr != "" {
addr = proxyAddr
}
app.infoLog.Printf(
"%s - %s %s %s",
addr,
r.Proto,
r.Method,
r.URL.RequestURI(),
)
next.ServeHTTP(w, r)
})
}
// recoverPanic is a middleware which recovers from a panic and logs the error.
func (app *application) recoverPanic(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
w.Header().Set("Connection", "close")
app.serverError(w, fmt.Errorf("%s", err))
}
}()
next.ServeHTTP(w, r)
})
}
func (app *application) clientError(w http.ResponseWriter, status int) {
http.Error(w, http.StatusText(status), status)
}
func (app *application) serverError(w http.ResponseWriter, err error) {
trace := fmt.Sprintf("%s\n%s", err.Error(), debug.Stack())
_ = app.errLog.Output(2, trace) // Ignore failed error logging.
http.Error(
w,
http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError,
)
}
// secureHeaders is a middleware which adds strict CSP and other headers.
// A CSP nonce is stored in the request's context which can be retrieved with
// the nonce helper function.
func (app *application) secureHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(
"Content-Security-Policy",
"default-src 'none'; script-src 'none' style-src 'none' img-src 'none' https: data:",
)
w.Header().Set("Referrer-Policy", "no-referrer")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "deny")
w.Header().Set("X-XSS-Protection", "0")
next.ServeHTTP(w, r)
})
}