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) }) }