mirror of
https://github.com/0ceanSlim/grain.git
synced 2024-11-22 08:37:13 +00:00
working rate limits!
This commit is contained in:
parent
8956eeea63
commit
f8138cb2cb
@ -6,12 +6,14 @@ server:
|
|||||||
port: ":8080" # Port for the server to listen on
|
port: ":8080" # Port for the server to listen on
|
||||||
|
|
||||||
rate_limit:
|
rate_limit:
|
||||||
#WS is not working as intended. It's only working for accepting Events, which is what event limit already does
|
#WS is not working as intended. It's only working for accepting Events, which is what event is supposed to do
|
||||||
ws_limit: 100 # Global rate limit for WebSocket messages (50 messages per second)
|
ws_limit: 100 # Global rate limit for WebSocket messages (50 messages per second)
|
||||||
ws_burst: 200 # Global burst limit for WebSocket messages (allows a burst of 100 messages)
|
ws_burst: 200 # Global burst limit for WebSocket messages (allows a burst of 100 messages)
|
||||||
#event limit doesn't seem to work either
|
#event limit doesn't seem to work either
|
||||||
event_limit: 50 # Global rate limit for events (25 events per second)
|
event_limit: 50 # Global rate limit for events (25 events per second)
|
||||||
event_burst: 100 # Global burst limit for events (allows a burst of 50 events)
|
event_burst: 100 # Global burst limit for events (allows a burst of 50 events)
|
||||||
|
req_limit: 50 # Added limit for REQ messages
|
||||||
|
req_burst: 100 # Added burst limit for REQ messages
|
||||||
|
|
||||||
category_limits: # Rate limits based on event categories
|
category_limits: # Rate limits based on event categories
|
||||||
regular:
|
regular:
|
||||||
@ -35,5 +37,5 @@ rate_limit:
|
|||||||
limit: 25 # Rate limit for events of kind 1 (100 events per second)
|
limit: 25 # Rate limit for events of kind 1 (100 events per second)
|
||||||
burst: 50 # Burst limit for events of kind 1 (allows a burst of 200 events)
|
burst: 50 # Burst limit for events of kind 1 (allows a burst of 200 events)
|
||||||
- kind: 3
|
- kind: 3
|
||||||
limit: 25 # Rate limit for events of kind 3 (25 events per second)
|
limit: 10 # Rate limit for events of kind 3 (25 events per second)
|
||||||
burst: 50 # Burst limit for events of kind 3 (allows a burst of 50 events)
|
burst: 20 # Burst limit for events of kind 3 (allows a burst of 50 events)
|
||||||
|
15
main.go
15
main.go
@ -1,3 +1,4 @@
|
|||||||
|
// main.go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -15,25 +16,24 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Load configuration
|
|
||||||
config, err := utils.LoadConfig("config.yml")
|
config, err := utils.LoadConfig("config.yml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Error loading config: ", err)
|
log.Fatal("Error loading config: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize MongoDB client
|
|
||||||
_, err = db.InitDB(config.MongoDB.URI, config.MongoDB.Database)
|
_, err = db.InitDB(config.MongoDB.URI, config.MongoDB.Database)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Error initializing database: ", err)
|
log.Fatal("Error initializing database: ", err)
|
||||||
}
|
}
|
||||||
defer db.DisconnectDB()
|
defer db.DisconnectDB()
|
||||||
|
|
||||||
// Initialize Rate Limiter
|
|
||||||
rateLimiter := utils.NewRateLimiter(
|
rateLimiter := utils.NewRateLimiter(
|
||||||
rate.Limit(config.RateLimit.WsLimit),
|
rate.Limit(config.RateLimit.WsLimit),
|
||||||
config.RateLimit.WsBurst,
|
config.RateLimit.WsBurst,
|
||||||
rate.Limit(config.RateLimit.EventLimit),
|
rate.Limit(config.RateLimit.EventLimit),
|
||||||
config.RateLimit.EventBurst,
|
config.RateLimit.EventBurst,
|
||||||
|
rate.Limit(config.RateLimit.ReqLimit),
|
||||||
|
config.RateLimit.ReqBurst,
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, kindLimit := range config.RateLimit.KindLimits {
|
for _, kindLimit := range config.RateLimit.KindLimits {
|
||||||
@ -46,21 +46,13 @@ func main() {
|
|||||||
|
|
||||||
utils.SetRateLimiter(rateLimiter)
|
utils.SetRateLimiter(rateLimiter)
|
||||||
|
|
||||||
// Create a new ServeMux
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
// Handle the root path
|
|
||||||
mux.HandleFunc("/", ListenAndServe)
|
mux.HandleFunc("/", ListenAndServe)
|
||||||
|
|
||||||
// Serve static files
|
|
||||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))
|
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))
|
||||||
|
|
||||||
// Serve the favicon
|
|
||||||
mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||||
http.ServeFile(w, r, "web/static/img/favicon.ico")
|
http.ServeFile(w, r, "web/static/img/favicon.ico")
|
||||||
})
|
})
|
||||||
|
|
||||||
// Start the Relay
|
|
||||||
fmt.Printf("Server is running on http://localhost%s\n", config.Server.Port)
|
fmt.Printf("Server is running on http://localhost%s\n", config.Server.Port)
|
||||||
err = http.ListenAndServe(config.Server.Port, mux)
|
err = http.ListenAndServe(config.Server.Port, mux)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -68,7 +60,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listener serves both WebSocket and HTML
|
|
||||||
func ListenAndServe(w http.ResponseWriter, r *http.Request) {
|
func ListenAndServe(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Header.Get("Upgrade") == "websocket" {
|
if r.Header.Get("Upgrade") == "websocket" {
|
||||||
websocket.Handler(func(ws *websocket.Conn) {
|
websocket.Handler(func(ws *websocket.Conn) {
|
||||||
|
21
readme.md
21
readme.md
@ -6,15 +6,15 @@ GRAIN is an open-source Nostr relay implementation written in Go. This project a
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **NIP-01 Protocol Support**: GRAIN (nearly)fully supports the NIP-01 for WebSocket communication.
|
- **NIP-01 Protocol Support**: GRAIN (nearly)fully supports NIP-01 for WebSocket communication.
|
||||||
- **Event Processing**: Handles events of kind 0 (user metadata) and kind 1 (text note).
|
- **Event Processing**: Handles all events by category and kind.
|
||||||
- **MongoDB 🍃**: Utilizes MongoDB to store and manage events efficiently.
|
- **MongoDB 🍃**: Utilizes MongoDB to store and manage events efficiently.
|
||||||
- **Scalability**: Built with Go, ensuring high performance and scalability.
|
- **Scalability**: Built with Go, ensuring high performance and scalability.
|
||||||
- **Open Source**: Licensed under the MIT License, making it free to use and modify.
|
- **Open Source**: Licensed under the MIT License, making it free to use and modify.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Configuration options can be set through environment variables or a configuration file.
|
Configuration options can be set through a configuration file.
|
||||||
|
|
||||||
There is an example config in this repo. Copy the example config to config.yml to get started
|
There is an example config in this repo. Copy the example config to config.yml to get started
|
||||||
|
|
||||||
@ -22,15 +22,13 @@ There is an example config in this repo. Copy the example config to config.yml t
|
|||||||
cp config.example.yml config.yml
|
cp config.example.yml config.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
### WebSocket Endpoints
|
|
||||||
|
|
||||||
- Connect: / - Clients can connect to this endpoint to start a WebSocket session.
|
|
||||||
- Publish Event: Send events of kind 0 (user metadata) or kind 1 (text note) to the relay.
|
|
||||||
|
|
||||||
### TODO
|
### TODO
|
||||||
|
|
||||||
- Explicitely Handle more kinds
|
- Handle Kind 5 explicitely to delete Events from the Database
|
||||||
- configurable event purging for regular events
|
- Handle Ephemeral event
|
||||||
|
- configurable amount of time to keep ephemeral notes
|
||||||
|
- configurable event purging
|
||||||
|
- by category
|
||||||
- by kind
|
- by kind
|
||||||
- by time since latest
|
- by time since latest
|
||||||
- create whitelist/blacklist functionality
|
- create whitelist/blacklist functionality
|
||||||
@ -40,9 +38,6 @@ cp config.example.yml config.yml
|
|||||||
- npub
|
- npub
|
||||||
- kind int
|
- kind int
|
||||||
- kind 1 wordlist
|
- kind 1 wordlist
|
||||||
- Rate limit Events.
|
|
||||||
- by kind
|
|
||||||
- configurable in config.yml
|
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// event.go
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -38,7 +39,6 @@ func HandleEvent(ws *websocket.Conn, message []interface{}) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the HandleKind function
|
|
||||||
HandleKind(context.TODO(), evt, ws)
|
HandleKind(context.TODO(), evt, ws)
|
||||||
|
|
||||||
fmt.Println("Event processed:", evt.ID)
|
fmt.Println("Event processed:", evt.ID)
|
||||||
@ -77,8 +77,8 @@ func HandleKind(ctx context.Context, evt relay.Event, ws *websocket.Conn) {
|
|||||||
category = "unknown"
|
category = "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
if !rateLimiter.AllowEvent(evt.Kind, category) {
|
if allowed, msg := rateLimiter.AllowEvent(evt.Kind, category); !allowed {
|
||||||
response.SendOK(ws, evt.ID, false, fmt.Sprintf("rate limit exceeded for category: %s", category))
|
response.SendOK(ws, evt.ID, false, msg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +99,6 @@ func HandleKind(ctx context.Context, evt relay.Event, ws *websocket.Conn) {
|
|||||||
case evt.Kind >= 10000 && evt.Kind < 20000:
|
case evt.Kind >= 10000 && evt.Kind < 20000:
|
||||||
err = kinds.HandleReplaceableKind(ctx, evt, collection, ws)
|
err = kinds.HandleReplaceableKind(ctx, evt, collection, ws)
|
||||||
case evt.Kind >= 20000 && evt.Kind < 30000:
|
case evt.Kind >= 20000 && evt.Kind < 30000:
|
||||||
// Ephemeral events are not stored
|
|
||||||
fmt.Println("Ephemeral event received and ignored:", evt.ID)
|
fmt.Println("Ephemeral event received and ignored:", evt.ID)
|
||||||
case evt.Kind >= 30000 && evt.Kind < 40000:
|
case evt.Kind >= 30000 && evt.Kind < 40000:
|
||||||
err = kinds.HandleParameterizedReplaceableKind(ctx, evt, collection, ws)
|
err = kinds.HandleParameterizedReplaceableKind(ctx, evt, collection, ws)
|
||||||
@ -114,4 +113,3 @@ func HandleKind(ctx context.Context, evt relay.Event, ws *websocket.Conn) {
|
|||||||
|
|
||||||
response.SendOK(ws, evt.ID, true, "")
|
response.SendOK(ws, evt.ID, true, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"grain/relay/handlers"
|
"grain/relay/handlers"
|
||||||
|
"grain/relay/utils"
|
||||||
|
|
||||||
"golang.org/x/net/websocket"
|
"golang.org/x/net/websocket"
|
||||||
)
|
)
|
||||||
@ -12,6 +13,8 @@ func WebSocketHandler(ws *websocket.Conn) {
|
|||||||
defer ws.Close()
|
defer ws.Close()
|
||||||
|
|
||||||
var msg string
|
var msg string
|
||||||
|
rateLimiter := utils.GetRateLimiter()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
err := websocket.Message.Receive(ws, &msg)
|
err := websocket.Message.Receive(ws, &msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -20,6 +23,11 @@ func WebSocketHandler(ws *websocket.Conn) {
|
|||||||
}
|
}
|
||||||
fmt.Println("Received message:", msg)
|
fmt.Println("Received message:", msg)
|
||||||
|
|
||||||
|
if allowed, msg := rateLimiter.AllowWs(); !allowed {
|
||||||
|
websocket.Message.Send(ws, fmt.Sprintf(`{"error": "%s"}`, msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var message []interface{}
|
var message []interface{}
|
||||||
err = json.Unmarshal([]byte(msg), &message)
|
err = json.Unmarshal([]byte(msg), &message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -42,6 +50,10 @@ func WebSocketHandler(ws *websocket.Conn) {
|
|||||||
case "EVENT":
|
case "EVENT":
|
||||||
handlers.HandleEvent(ws, message)
|
handlers.HandleEvent(ws, message)
|
||||||
case "REQ":
|
case "REQ":
|
||||||
|
if allowed, msg := rateLimiter.AllowReq(); !allowed {
|
||||||
|
websocket.Message.Send(ws, fmt.Sprintf(`{"error": "%s"}`, msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
handlers.HandleReq(ws, message)
|
handlers.HandleReq(ws, message)
|
||||||
case "CLOSE":
|
case "CLOSE":
|
||||||
handlers.HandleClose(ws, message)
|
handlers.HandleClose(ws, message)
|
||||||
|
@ -27,6 +27,8 @@ type RateLimitConfig struct {
|
|||||||
WsBurst int `yaml:"ws_burst"`
|
WsBurst int `yaml:"ws_burst"`
|
||||||
EventLimit float64 `yaml:"event_limit"`
|
EventLimit float64 `yaml:"event_limit"`
|
||||||
EventBurst int `yaml:"event_burst"`
|
EventBurst int `yaml:"event_burst"`
|
||||||
|
ReqLimit float64 `yaml:"req_limit"`
|
||||||
|
ReqBurst int `yaml:"req_burst"`
|
||||||
CategoryLimits map[string]KindLimitConfig `yaml:"category_limits"`
|
CategoryLimits map[string]KindLimitConfig `yaml:"category_limits"`
|
||||||
KindLimits []KindLimitConfig `yaml:"kind_limits"`
|
KindLimits []KindLimitConfig `yaml:"kind_limits"`
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
// rateLimiter.go
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
@ -21,6 +23,7 @@ type CategoryLimiter struct {
|
|||||||
type RateLimiter struct {
|
type RateLimiter struct {
|
||||||
wsLimiter *rate.Limiter
|
wsLimiter *rate.Limiter
|
||||||
eventLimiter *rate.Limiter
|
eventLimiter *rate.Limiter
|
||||||
|
reqLimiter *rate.Limiter
|
||||||
categoryLimiters map[string]*CategoryLimiter
|
categoryLimiters map[string]*CategoryLimiter
|
||||||
kindLimiters map[int]*KindLimiter
|
kindLimiters map[int]*KindLimiter
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
@ -39,40 +42,51 @@ func GetRateLimiter() *RateLimiter {
|
|||||||
return rateLimiterInstance
|
return rateLimiterInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRateLimiter(wsLimit rate.Limit, wsBurst int, eventLimit rate.Limit, eventBurst int) *RateLimiter {
|
func NewRateLimiter(wsLimit rate.Limit, wsBurst int, eventLimit rate.Limit, eventBurst int, reqLimit rate.Limit, reqBurst int) *RateLimiter {
|
||||||
return &RateLimiter{
|
return &RateLimiter{
|
||||||
wsLimiter: rate.NewLimiter(wsLimit, wsBurst),
|
wsLimiter: rate.NewLimiter(wsLimit, wsBurst),
|
||||||
eventLimiter: rate.NewLimiter(eventLimit, eventBurst),
|
eventLimiter: rate.NewLimiter(eventLimit, eventBurst),
|
||||||
|
reqLimiter: rate.NewLimiter(reqLimit, reqBurst),
|
||||||
categoryLimiters: make(map[string]*CategoryLimiter),
|
categoryLimiters: make(map[string]*CategoryLimiter),
|
||||||
kindLimiters: make(map[int]*KindLimiter),
|
kindLimiters: make(map[int]*KindLimiter),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *RateLimiter) AllowWs() bool {
|
func (rl *RateLimiter) AllowWs() (bool, string) {
|
||||||
return rl.wsLimiter.Allow()
|
if !rl.wsLimiter.Allow() {
|
||||||
|
return false, "WebSocket message rate limit exceeded"
|
||||||
|
}
|
||||||
|
return true, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *RateLimiter) AllowEvent(kind int, category string) bool {
|
func (rl *RateLimiter) AllowEvent(kind int, category string) (bool, string) {
|
||||||
rl.mu.RLock()
|
rl.mu.RLock()
|
||||||
defer rl.mu.RUnlock()
|
defer rl.mu.RUnlock()
|
||||||
|
|
||||||
if !rl.eventLimiter.Allow() {
|
if !rl.eventLimiter.Allow() {
|
||||||
return false
|
return false, "Global event rate limit exceeded"
|
||||||
}
|
}
|
||||||
|
|
||||||
if kindLimiter, exists := rl.kindLimiters[kind]; exists {
|
if kindLimiter, exists := rl.kindLimiters[kind]; exists {
|
||||||
if !kindLimiter.Limiter.Allow() {
|
if !kindLimiter.Limiter.Allow() {
|
||||||
return false
|
return false, fmt.Sprintf("Rate limit exceeded for kind: %d", kind)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if categoryLimiter, exists := rl.categoryLimiters[category]; exists {
|
if categoryLimiter, exists := rl.categoryLimiters[category]; exists {
|
||||||
if !categoryLimiter.Limiter.Allow() {
|
if !categoryLimiter.Limiter.Allow() {
|
||||||
return false
|
return false, fmt.Sprintf("Rate limit exceeded for category: %s", category)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *RateLimiter) AllowReq() (bool, string) {
|
||||||
|
if !rl.reqLimiter.Allow() {
|
||||||
|
return false, "REQ rate limit exceeded"
|
||||||
|
}
|
||||||
|
return true, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *RateLimiter) AddCategoryLimit(category string, limit rate.Limit, burst int) {
|
func (rl *RateLimiter) AddCategoryLimit(category string, limit rate.Limit, burst int) {
|
||||||
|
Loading…
Reference in New Issue
Block a user