Added rate limiting

This commit is contained in:
Uthman Fatih 2026-10-10 18:16:24 +01:00
parent 8548b106a2
commit aae2b6be66
7 changed files with 141 additions and 4 deletions

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
.env
cache.gob
prayercal
prayercal.ics
.git

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
.env
cache.gob cache.gob
prayercal prayercal
.env
prayercal.ics prayercal.ics
data/

14
Dockerfile Normal file
View 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
View File

@ -5,4 +5,5 @@ go 1.27.0
require ( require (
github.com/arran4/golang-ical v0.3.7 github.com/arran4/golang-ical v0.3.7
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
golang.org/x/time v0.16.0
) )

2
go.sum
View File

@ -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/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 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

118
main.go
View File

@ -17,6 +17,11 @@ import (
_ "time/tzdata" _ "time/tzdata"
ics "github.com/arran4/golang-ical" ics "github.com/arran4/golang-ical"
"net"
"strconv"
"strings"
"golang.org/x/time/rate"
) )
type prayerTimes struct { 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]} // 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") loc, err := time.LoadLocation("Europe/London")
time.Local = loc
if err != nil { if err != nil {
log.Fatal(err) 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() mu.Lock()
times, err := getTimes(url, "/data/cache.gob") times, err := getTimes(url, "/data/cache.gob")
mu.Unlock() mu.Unlock()
@ -64,7 +76,13 @@ func main() {
w.Header().Set("Content-Type", "text/calendar; charset=utf-8") w.Header().Set("Content-Type", "text/calendar; charset=utf-8")
io.WriteString(w, out) 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) { func buildCal(times prayerTimes, loc *time.Location) (string, error) {
@ -190,3 +208,97 @@ func getTimes(url, cache string) (prayerTimes, error) {
return form, nil 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
}

2
readme.md Normal file
View File

@ -0,0 +1,2 @@
# Prayercal