Added rate limiting
This commit is contained in:
parent
8548b106a2
commit
aae2b6be66
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@ -0,0 +1,5 @@
|
||||
.env
|
||||
cache.gob
|
||||
prayercal
|
||||
prayercal.ics
|
||||
.git
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
.env
|
||||
cache.gob
|
||||
prayercal
|
||||
.env
|
||||
prayercal.ics
|
||||
data/
|
||||
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@ -0,0 +1,14 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:1.27 AS build
|
||||
ARG TARGETOS TARGETARCH TARGETVARIANT
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
|
||||
GOARM=${TARGETVARIANT#v} \
|
||||
go build -o /prayercal .
|
||||
|
||||
FROM gcr.io/distroless/static
|
||||
COPY --from=build /prayercal /prayercal
|
||||
EXPOSE 5543
|
||||
ENTRYPOINT ["/prayercal"]
|
||||
1
go.mod
1
go.mod
@ -5,4 +5,5 @@ go 1.27.0
|
||||
require (
|
||||
github.com/arran4/golang-ical v0.3.7
|
||||
github.com/joho/godotenv v1.5.1
|
||||
golang.org/x/time v0.16.0
|
||||
)
|
||||
|
||||
2
go.sum
2
go.sum
@ -10,5 +10,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
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=
|
||||
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
|
||||
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
118
main.go
118
main.go
@ -17,6 +17,11 @@ import (
|
||||
_ "time/tzdata"
|
||||
|
||||
ics "github.com/arran4/golang-ical"
|
||||
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type prayerTimes struct {
|
||||
@ -39,12 +44,19 @@ func main() {
|
||||
// example {1791571473 map[Asr:15:43 Dhuhr:12:53 Fajr:05:41 Isha:19:45 Maghrib:18:24 Sunrise:07:11]}
|
||||
|
||||
loc, err := time.LoadLocation("Europe/London")
|
||||
time.Local = loc
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
time.Local = loc
|
||||
|
||||
http.HandleFunc("/prayercal.ics", func(w http.ResponseWriter, r *http.Request) {
|
||||
lim := newLimiter(
|
||||
envFloat("RATE_LIMIT_RPS", 1),
|
||||
envInt("RATE_LIMIT_BURST", 5),
|
||||
)
|
||||
trustProxy := envBool("TRUST_PROXY", false)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/prayercal.ics", func(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
times, err := getTimes(url, "/data/cache.gob")
|
||||
mu.Unlock()
|
||||
@ -64,7 +76,13 @@ func main() {
|
||||
w.Header().Set("Content-Type", "text/calendar; charset=utf-8")
|
||||
io.WriteString(w, out)
|
||||
})
|
||||
log.Fatal(http.ListenAndServe(":5543", nil))
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":" + getenv("PORT", "5543"),
|
||||
Handler: lim.middleware(trustProxy, mux),
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
log.Fatal(srv.ListenAndServe())
|
||||
}
|
||||
|
||||
func buildCal(times prayerTimes, loc *time.Location) (string, error) {
|
||||
@ -189,4 +207,98 @@ func getTimes(url, cache string) (prayerTimes, error) {
|
||||
}
|
||||
|
||||
return form, nil
|
||||
}
|
||||
|
||||
func envFloat(key string, def float64) float64 {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if f, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
return f
|
||||
}
|
||||
log.Printf("bad %s=%q, using %v", key, v, def)
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func envInt(key string, def int) int {
|
||||
return int(envFloat(key, float64(def)))
|
||||
}
|
||||
|
||||
func envBool(key string, def bool) bool {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if b, err := strconv.ParseBool(v); err == nil {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
type client struct {
|
||||
lim *rate.Limiter
|
||||
seen time.Time
|
||||
}
|
||||
|
||||
type limiter struct {
|
||||
mu sync.Mutex
|
||||
clients map[string]*client
|
||||
rps rate.Limit
|
||||
burst int
|
||||
}
|
||||
|
||||
func newLimiter(rps float64, burst int) *limiter {
|
||||
l := &limiter{clients: map[string]*client{}, rps: rate.Limit(rps), burst: burst}
|
||||
go func() {
|
||||
for range time.Tick(time.Minute) {
|
||||
l.mu.Lock()
|
||||
for ip, c := range l.clients {
|
||||
if time.Since(c.seen) > 10*time.Minute {
|
||||
delete(l.clients, ip)
|
||||
}
|
||||
}
|
||||
l.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *limiter) allow(ip string) bool {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
c, ok := l.clients[ip]
|
||||
if !ok {
|
||||
c = &client{lim: rate.NewLimiter(l.rps, l.burst)}
|
||||
l.clients[ip] = c
|
||||
}
|
||||
c.seen = time.Now()
|
||||
return c.lim.Allow()
|
||||
}
|
||||
|
||||
func clientIP(r *http.Request, trustProxy bool) string {
|
||||
if trustProxy {
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
return strings.TrimSpace(strings.Split(xff, ",")[0])
|
||||
}
|
||||
}
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return r.RemoteAddr
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
func (l *limiter) middleware(trustProxy bool, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !l.allow(clientIP(r, trustProxy)) {
|
||||
w.Header().Set("Retry-After", "1")
|
||||
http.Error(w, "too many requests", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func getenv(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user