Initial commit
This commit is contained in:
commit
31f27c5092
4 changed files with 207 additions and 0 deletions
177
main.go
Normal file
177
main.go
Normal 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)
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue