Skip to content

Commit 4a153bd

Browse files
committed
feat(rate_limiter): implemented rate limiter for apis
1 parent d70f65c commit 4a153bd

File tree

5 files changed

+156
-3
lines changed

5 files changed

+156
-3
lines changed

go.mod

+8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ require (
77
github.com/mattn/go-sqlite3 v1.14.22
88
)
99

10+
require (
11+
github.com/didip/tollbooth v4.0.2+incompatible // indirect
12+
github.com/didip/tollbooth/v8 v8.0.1 // indirect
13+
github.com/go-pkgz/expirable-cache/v3 v3.0.0 // indirect
14+
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
15+
golang.org/x/time v0.9.0 // indirect
16+
)
17+
1018
require (
1119
github.com/charmbracelet/x/ansi v0.1.4 // indirect
1220
github.com/felixge/httpsnoop v1.0.3 // indirect

go.sum

+10
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@ github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/N
44
github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8=
55
github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM=
66
github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
7+
github.com/didip/tollbooth v4.0.2+incompatible h1:fVSa33JzSz0hoh2NxpwZtksAzAgd7zjmGO20HCZtF4M=
8+
github.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY=
9+
github.com/didip/tollbooth/v8 v8.0.1 h1:VAAapTo1t4Bn6bbpcHjuovwoa9u3JH++wgjbpWv+rB8=
10+
github.com/didip/tollbooth/v8 v8.0.1/go.mod h1:oEd9l+ep373d7DmvKLc0a5gasPOev2mTewi6KPQBGJ4=
711
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
812
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
13+
github.com/go-pkgz/expirable-cache/v3 v3.0.0 h1:u3/gcu3sabLYiTCevoRKv+WzjIn5oo7P8XtiXBeRDLw=
14+
github.com/go-pkgz/expirable-cache/v3 v3.0.0/go.mod h1:2OQiDyEGQalYecLWmXprm3maPXeVb5/6/X7yRPYTzec=
915
github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA=
1016
github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk=
1117
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
@@ -22,6 +28,8 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o
2228
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
2329
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
2430
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
31+
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
32+
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
2533
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
2634
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
2735
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -30,3 +38,5 @@ github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU
3038
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
3139
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
3240
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
41+
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
42+
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=

src/server/handlers/clap_counter.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ func GetClaps(w http.ResponseWriter, r *http.Request) {
111111
if clapCounter.Page == "" {
112112
err := decode_request(r, w, &clapCounter)
113113
if err != nil {
114+
log.Printf("Error decoding request: %v", err)
114115
return
115116
}
116117
}
@@ -123,9 +124,9 @@ func GetClaps(w http.ResponseWriter, r *http.Request) {
123124
if continue_counting {
124125
likes := get_likes(r, clapCounter)
125126
switch r.Method {
126-
case "GET":
127+
case http.MethodGet:
127128
clapCounter.SetClapCounter(clapCounter.URL, ClapCount, likes.Count, true)
128-
case "POST":
129+
case http.MethodPost:
129130
content_type_err := check_for_request_content_type(w, r)
130131
if content_type_err != nil {
131132
return
@@ -152,6 +153,7 @@ func GetClaps(w http.ResponseWriter, r *http.Request) {
152153
http.Error(w, err.Error(), http.StatusInternalServerError)
153154
return
154155
}
156+
w.Header().Set("Content-Type", "application/json")
155157
_, err = w.Write(jsonData)
156158
if err != nil {
157159
log.Printf("Error writing response: %v", err)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package handlers_test
2+
3+
// import (
4+
// "BloTils/src/server/handlers"
5+
// "encoding/json"
6+
// "net/http"
7+
// "net/http/httptest"
8+
// "strings"
9+
// "testing"
10+
// )
11+
12+
// func TestGetClaps(t *testing.T) {
13+
// tests := []struct {
14+
// name string
15+
// method string
16+
// url string
17+
// referer string
18+
// contentType string
19+
// expectedStatus int
20+
// expectedCount int
21+
// expectedOK bool
22+
// }{
23+
// {
24+
// name: "GET request success",
25+
// method: "GET",
26+
// url: "/claps",
27+
// referer: "http://example.com/page",
28+
// contentType: "application/json",
29+
// expectedStatus: http.StatusOK,
30+
// expectedCount: 0,
31+
// expectedOK: true,
32+
// },
33+
// {
34+
// name: "POST request invalid content type",
35+
// method: "POST",
36+
// url: "/claps",
37+
// referer: "http://example.com/page",
38+
// contentType: "text/plain",
39+
// expectedStatus: http.StatusUnsupportedMediaType,
40+
// expectedCount: 0,
41+
// expectedOK: false,
42+
// },
43+
// {
44+
// name: "Invalid HTTP method",
45+
// method: "PUT",
46+
// url: "/claps",
47+
// referer: "http://example.com/page",
48+
// contentType: "application/json",
49+
// expectedStatus: http.StatusMethodNotAllowed,
50+
// expectedCount: 0,
51+
// expectedOK: false,
52+
// },
53+
// {
54+
// name: "Missing referer",
55+
// method: "GET",
56+
// url: "/claps",
57+
// referer: "",
58+
// contentType: "application/json",
59+
// expectedStatus: http.StatusForbidden,
60+
// expectedCount: 0,
61+
// expectedOK: false,
62+
// },
63+
// }
64+
65+
// for _, tt := range tests {
66+
// t.Run(tt.name, func(t *testing.T) {
67+
// req := httptest.NewRequest(tt.method, tt.url, nil)
68+
// req.Header.Set("Content-Type", tt.contentType)
69+
// req.Header.Set("Referer", tt.referer)
70+
71+
// w := httptest.NewRecorder()
72+
// handlers.GetClaps(w, req)
73+
74+
// if w.Code != tt.expectedStatus {
75+
// t.Errorf("expected status %d, got %d", tt.expectedStatus, w.Code)
76+
// }
77+
78+
// if w.Code == http.StatusOK {
79+
// var response handlers.ClapCounter
80+
// err := json.NewDecoder(w.Body).Decode(&response)
81+
// if err != nil {
82+
// t.Fatalf("failed to decode response: %v", err)
83+
// }
84+
85+
// if response.Count != tt.expectedCount {
86+
// t.Errorf("expected count %d, got %d", tt.expectedCount, response.Count)
87+
// }
88+
89+
// }
90+
// })
91+
// }
92+
// }
93+
94+
// func TestGetClapsWithBody(t *testing.T) {
95+
// body := strings.NewReader(`{"URL": "http://example.com", "Page": "/test"}`)
96+
// req := httptest.NewRequest(http.MethodPost, "/claps", body)
97+
// req.Header.Set("Content-Type", "application/json")
98+
99+
// w := httptest.NewRecorder()
100+
// handlers.GetClaps(w, req)
101+
102+
// if w.Code != http.StatusOK {
103+
// t.Errorf("expected status 200, got %d", w.Code)
104+
// }
105+
106+
// var response handlers.ClapCounter
107+
// err := json.NewDecoder(w.Body).Decode(&response)
108+
// if err != nil {
109+
// t.Fatalf("failed to decode response: %v", err)
110+
// }
111+
112+
// if response.URL != "http://example.com" {
113+
// t.Errorf("expected URL http://example.com, got %s", response.URL)
114+
// }
115+
// }

src/server/routes/routes.go

+19-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import (
66
local_handlers "BloTils/src/server/handlers"
77
"net/http"
88
"os"
9+
"time"
910

11+
"github.com/didip/tollbooth/v8"
12+
"github.com/didip/tollbooth/v8/limiter"
1013
"github.com/gorilla/handlers"
1114
)
1215

@@ -21,11 +24,26 @@ func setRoutes(server *server.Server, path string, handler http.HandlerFunc, met
2124
// It calls setRoutes to add each route, specifying the path, handler function,
2225
// and HTTP methods allowed.
2326
func RegisterRoutes(server *server.Server) {
27+
lmt := init_rate_limiter()
2428
setRoutes(server, "/", local_handlers.IndexPage, http.MethodGet)
2529
setRoutes(server, "/clap_counter", local_handlers.ClapCounterPage, http.MethodGet)
2630
// API routes
2731
setRoutes(server, "/api/v1/ping", local_handlers.Ping, http.MethodGet)
28-
setRoutes(server, "/api/v1/count_like", local_handlers.GetClaps, http.MethodGet, http.MethodPost)
32+
setRoutes(server, "/api/v1/count_like", tollbooth.LimitFuncHandler(lmt, local_handlers.GetClaps).ServeHTTP, http.MethodGet, http.MethodPost)
33+
}
34+
35+
// init_rate_limiter creates a new rate limiter with a limit of 1 request per hour.
36+
// It sets the IP lookup method to use the RemoteAddr field, and configures the
37+
// error message and content type to be returned when the limit is reached.
38+
func init_rate_limiter() *limiter.Limiter {
39+
var lmt = tollbooth.NewLimiter(1, &limiter.ExpirableOptions{DefaultExpirationTTL: time.Hour})
40+
lmt.SetIPLookup(limiter.IPLookup{
41+
Name: "RemoteAddr",
42+
IndexFromRight: 0,
43+
})
44+
lmt.SetMessage("You have reached maximum request limit.")
45+
lmt.SetMessageContentType("application/json")
46+
return lmt
2947
}
3048

3149
// ServeStaticFiles registers a file server handler on the provided server to serve

0 commit comments

Comments
 (0)