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
|
cache.gob
|
||||||
prayercal
|
prayercal
|
||||||
.env
|
|
||||||
prayercal.ics
|
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 (
|
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
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/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
118
main.go
@ -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
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user