Compare commits

...

7 Commits

20 changed files with 666 additions and 279 deletions

2
.gitignore vendored
View File

@ -1,5 +1,7 @@
/tmp /tmp
config.yml config.yml
whitelist.yml
blacklist.yml
relay_metadata.json relay_metadata.json
grain.exe grain.exe
/build /build

View File

@ -0,0 +1,16 @@
enabled: true
permanent_ban_words:
- nigger
temp_ban_words:
- crypto
- web3
- airdrop
max_temp_bans: 3
temp_ban_duration: 3600
permanent_blacklist_pubkeys:
- db0c9b8acd6101adb9b281c5321f98f6eebb33c5719d230ed1870997538a9765
permanent_blacklist_npubs:
- npub1x0r5gflnk2mn6h3c70nvnywpy2j46gzqwg6k7uw6fxswyz0md9qqnhshtn
mutelist_authors:
- 3fe0ab6cbdb7ee27148202249e3fb3b89423c6f6cda6ef43ea5057c3d93088e4
# mutelist Event MUST be stored in this relay for it to be retrieved.

View File

@ -14,6 +14,12 @@ server:
max_connections: 100 max_connections: 100
max_subscriptions_per_client: 10 max_subscriptions_per_client: 10
event_time_constraints:
min_created_at: 1577836800 # January 1, 2020, as Unix timestamp
# min_created_at_string: now-5m # Custom value to indicate 5 minutes in the past
# max_created_at: 0 # Set to 0 to use the default behavior of 'now'
max_created_at_string: now+5m # Use a string to set a date for max created at in the future or past from current time
resource_limits: resource_limits:
cpu_cores: 2 # Limit the number of CPU cores the application can use cpu_cores: 2 # Limit the number of CPU cores the application can use
memory_mb: 1024 # Cap the maximum amount of RAM in MB the application can use memory_mb: 1024 # Cap the maximum amount of RAM in MB the application can use
@ -63,33 +69,6 @@ rate_limit:
limit: 25 limit: 25
burst: 50 burst: 50
pubkey_whitelist:
enabled: false
pubkeys: [] # List of allowed public keys
npubs: [] # List of allowed npubs (Nostr public keys in bech32 format)
kind_whitelist:
enabled: false
kinds: [] # List of allowed event kinds
domain_whitelist:
enabled: false
domains: [] # List of allowed domains
blacklist: #Removing a pubkey from the Blacklist requires a hard restart; Blacklist overides the Whitelist
enabled: true
permanent_ban_words: [] # Words that trigger a permanent ban
temp_ban_words: # Words that trigger a temporary ban
- crypto
- web3
- airdrop
max_temp_bans: 3 # Number of temporary bans before a permanent ban
temp_ban_duration: 3600 # Temporary ban duration in seconds
permanent_blacklist_pubkeys: # List of permanently banned public keys
- db0c9b8acd6101adb9b281c5321f98f6eebb33c5719d230ed1870997538a9765
permanent_blacklist_npubs: # List of permanently banned npubs
- npub1x0r5gflnk2mn6h3c70nvnywpy2j46gzqwg6k7uw6fxswyz0md9qqnhshtn
event_purge: event_purge:
enabled: true # Toggle to enable/disable event purging enabled: true # Toggle to enable/disable event purging
keep_duration_days: 2 # Number of days to keep events keep_duration_days: 2 # Number of days to keep events

View File

@ -0,0 +1,20 @@
pubkey_whitelist:
enabled: false
pubkeys:
- pubkey1
- pubkey2
npubs:
- npub18ls2km9aklhzw9yzqgjfu0anhz2z83hkeknw7sl22ptu8kfs3rjq54am44
- npub2
kind_whitelist:
enabled: false
kinds:
- "1"
- "2"
domain_whitelist:
enabled: false
domains:
- "example.com"
- "anotherdomain.com"

View File

@ -1,65 +1,68 @@
package config package config
import ( import (
"encoding/json"
"fmt" "fmt"
types "grain/config/types" types "grain/config/types"
"grain/server/utils" "grain/server/utils"
"io"
"log" "log"
"os" "os"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/gorilla/websocket"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
// CheckBlacklist checks if a pubkey is in the blacklist based on event content // CheckBlacklist checks if a pubkey is in the blacklist based on event content
func CheckBlacklist(pubkey, eventContent string) (bool, string) { func CheckBlacklist(pubkey, eventContent string) (bool, string) {
blacklistConfig := GetConfig().Blacklist blacklistConfig := GetBlacklistConfig()
if blacklistConfig == nil || !blacklistConfig.Enabled {
return false, ""
}
if !blacklistConfig.Enabled { log.Printf("Checking blacklist for pubkey: %s", pubkey)
return false, ""
}
log.Printf("Checking blacklist for pubkey: %s", pubkey) // Check for permanent blacklist by pubkey or npub.
if isPubKeyPermanentlyBlacklisted(pubkey, blacklistConfig) {
log.Printf("Pubkey %s is permanently blacklisted", pubkey)
return true, fmt.Sprintf("pubkey %s is permanently blacklisted", pubkey)
}
// Check for permanent blacklist by pubkey or npub // Check for temporary ban.
if isPubKeyPermanentlyBlacklisted(pubkey, blacklistConfig) { if isPubKeyTemporarilyBlacklisted(pubkey) {
log.Printf("Pubkey %s is permanently blacklisted", pubkey) log.Printf("Pubkey %s is temporarily blacklisted", pubkey)
return true, fmt.Sprintf("pubkey %s is permanently blacklisted", pubkey) return true, fmt.Sprintf("pubkey %s is temporarily blacklisted", pubkey)
} }
// Check for temporary ban // Check for permanent ban based on wordlist.
if isPubKeyTemporarilyBlacklisted(pubkey) { for _, word := range blacklistConfig.PermanentBanWords {
log.Printf("Pubkey %s is temporarily blacklisted", pubkey) if strings.Contains(eventContent, word) {
return true, fmt.Sprintf("pubkey %s is temporarily blacklisted", pubkey) err := AddToPermanentBlacklist(pubkey)
} if err != nil {
return true, fmt.Sprintf("pubkey %s is permanently banned and failed to save: %v", pubkey, err)
}
return true, "blocked: pubkey is permanently banned"
}
}
// Check for permanent ban based on wordlist // Check for temporary ban based on wordlist.
for _, word := range blacklistConfig.PermanentBanWords { for _, word := range blacklistConfig.TempBanWords {
if strings.Contains(eventContent, word) { if strings.Contains(eventContent, word) {
err := AddToPermanentBlacklist(pubkey) err := AddToTemporaryBlacklist(pubkey, *blacklistConfig)
if err != nil { if err != nil {
return true, fmt.Sprintf("pubkey %s is permanently banned and failed to save: %v", pubkey, err) return true, fmt.Sprintf("pubkey %s is temporarily banned and failed to save: %v", pubkey, err)
} }
return true, "blocked: pubkey is permanently banned" return true, "blocked: pubkey is temporarily banned"
} }
} }
// Check for temporary ban based on wordlist return false, ""
for _, word := range blacklistConfig.TempBanWords {
if strings.Contains(eventContent, word) {
err := AddToTemporaryBlacklist(pubkey, blacklistConfig)
if err != nil {
return true, fmt.Sprintf("pubkey %s is temporarily banned and failed to save: %v", pubkey, err)
}
return true, "blocked: pubkey is temporarily banned"
}
}
return false, ""
} }
// Checks if a pubkey is temporarily blacklisted // Checks if a pubkey is temporarily blacklisted
func isPubKeyTemporarilyBlacklisted(pubkey string) bool { func isPubKeyTemporarilyBlacklisted(pubkey string) bool {
mu.Lock() mu.Lock()
@ -142,63 +145,216 @@ func AddToTemporaryBlacklist(pubkey string, blacklistConfig types.BlacklistConfi
return nil return nil
} }
// Checks if a pubkey is permanently blacklisted (only using config.yml) func isPubKeyPermanentlyBlacklisted(pubKey string, blacklistConfig *types.BlacklistConfig) bool {
func isPubKeyPermanentlyBlacklisted(pubKey string, blacklistConfig types.BlacklistConfig) bool { if blacklistConfig == nil || !blacklistConfig.Enabled {
if !blacklistConfig.Enabled { return false
return false }
}
// Check pubkeys // Check pubkeys.
for _, blacklistedKey := range blacklistConfig.PermanentBlacklistPubkeys { for _, blacklistedKey := range blacklistConfig.PermanentBlacklistPubkeys {
if pubKey == blacklistedKey { if pubKey == blacklistedKey {
return true return true
} }
} }
// Check npubs // Check npubs.
for _, npub := range blacklistConfig.PermanentBlacklistNpubs { for _, npub := range blacklistConfig.PermanentBlacklistNpubs {
decodedPubKey, err := utils.DecodeNpub(npub) decodedPubKey, err := utils.DecodeNpub(npub)
if err != nil { if err != nil {
fmt.Println("Error decoding npub:", err) fmt.Println("Error decoding npub:", err)
continue continue
} }
if pubKey == decodedPubKey { if pubKey == decodedPubKey {
return true return true
} }
} }
return false return false
} }
func AddToPermanentBlacklist(pubkey string) error { func AddToPermanentBlacklist(pubkey string) error {
// Remove the mutex lock from here blacklistConfig := GetBlacklistConfig()
blacklistConfig := GetConfig().Blacklist if blacklistConfig == nil {
return fmt.Errorf("blacklist configuration is not loaded")
}
// Check if already blacklisted // Check if already blacklisted.
if isPubKeyPermanentlyBlacklisted(pubkey, blacklistConfig) { if isPubKeyPermanentlyBlacklisted(pubkey, blacklistConfig) {
return fmt.Errorf("pubkey %s is already in the permanent blacklist", pubkey) return fmt.Errorf("pubkey %s is already in the permanent blacklist", pubkey)
} }
// Add pubkey to the blacklist // Add pubkey to the permanent blacklist.
blacklistConfig.PermanentBlacklistPubkeys = append(blacklistConfig.PermanentBlacklistPubkeys, pubkey) blacklistConfig.PermanentBlacklistPubkeys = append(blacklistConfig.PermanentBlacklistPubkeys, pubkey)
// Persist changes to config.yml // Persist changes to blacklist.yml.
return saveBlacklistConfig(blacklistConfig) return saveBlacklistConfig(*blacklistConfig)
} }
func saveBlacklistConfig(blacklistConfig types.BlacklistConfig) error { func saveBlacklistConfig(blacklistConfig types.BlacklistConfig) error {
configData := GetConfig() data, err := yaml.Marshal(blacklistConfig)
configData.Blacklist = blacklistConfig if err != nil {
return fmt.Errorf("failed to marshal blacklist config: %v", err)
}
data, err := yaml.Marshal(configData) err = os.WriteFile("blacklist.yml", data, 0644)
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal config: %v", err) return fmt.Errorf("failed to write config to file: %v", err)
} }
err = os.WriteFile("config.yml", data, 0644) return nil
if err != nil { }
return fmt.Errorf("failed to write config to file: %v", err)
} // FetchPubkeysFromLocalMuteList sends a REQ to the local relay for mute list events.
func FetchPubkeysFromLocalMuteList(localRelayURL string, muteListAuthors []string) ([]string, error) {
return nil var wg sync.WaitGroup
var mu sync.Mutex
var allPubkeys []string
results := make(chan []string, 1)
wg.Add(1)
go func() {
defer wg.Done()
conn, _, err := websocket.DefaultDialer.Dial(localRelayURL, nil)
if err != nil {
log.Printf("Failed to connect to local relay %s: %v", localRelayURL, err)
return
}
defer conn.Close()
subscriptionID := "mutelist-fetch"
// Create the REQ message to fetch the mute list events by IDs.
req := []interface{}{"REQ", subscriptionID, map[string]interface{}{
"authors": muteListAuthors,
"kinds": []int{10000}, // Mute list events kind.
}}
reqJSON, err := json.Marshal(req)
if err != nil {
log.Printf("Failed to marshal request: %v", err)
return
}
err = conn.WriteMessage(websocket.TextMessage, reqJSON)
if err != nil {
log.Printf("Failed to send request to local relay %s: %v", localRelayURL, err)
return
}
// Listen for messages from the local relay.
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Printf("Error reading message from local relay %s: %v", localRelayURL, err)
break
}
// Log the raw message for debugging.
log.Printf("Received raw message: %s", message)
var response []interface{}
err = json.Unmarshal(message, &response)
if err != nil || len(response) < 2 {
log.Printf("Invalid message format or failed to unmarshal: %v", err)
continue
}
// Check for "EVENT" type messages.
eventType, ok := response[0].(string)
if !ok {
log.Printf("Unexpected event type: %v", response[0])
continue
}
if eventType == "EOSE" {
// End of subscription events; send a "CLOSE" message to the relay.
closeReq := []interface{}{"CLOSE", subscriptionID}
closeReqJSON, err := json.Marshal(closeReq)
if err != nil {
log.Printf("Failed to marshal close request: %v", err)
} else {
if err = conn.WriteMessage(websocket.TextMessage, closeReqJSON); err != nil {
log.Printf("Failed to send close request to relay %s: %v", localRelayURL, err)
} else {
log.Println("Sent CLOSE request to end subscription.")
// Wait for a potential response or timeout
conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
_, _, err = conn.ReadMessage()
if err != nil {
if err == io.EOF {
log.Println("Connection closed by the server after CLOSE request (EOF)")
} else if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
log.Println("WebSocket closed normally after CLOSE request")
} else {
log.Printf("Unexpected error after CLOSE request: %v", err)
}
}
}
}
// Ensure we break the loop after handling EOSE
break
}
if eventType == "EVENT" {
// Safely cast the event data from the third element.
if len(response) < 3 {
log.Printf("Unexpected event format with insufficient data: %v", response)
continue
}
eventData, ok := response[2].(map[string]interface{})
if !ok {
log.Printf("Expected event data to be a map, got: %T", response[2])
continue
}
// Log event data for debugging.
log.Printf("Event data received: %+v", eventData)
pubkeys := extractPubkeysFromMuteListEvent(eventData)
results <- pubkeys
}
}
}()
go func() {
wg.Wait()
close(results)
}()
// Collect results from the relay.
for pubkeys := range results {
mu.Lock()
allPubkeys = append(allPubkeys, pubkeys...)
mu.Unlock()
}
return allPubkeys, nil
}
// extractPubkeysFromMuteListEvent extracts pubkeys from a mute list event.
func extractPubkeysFromMuteListEvent(eventData map[string]interface{}) []string {
var pubkeys []string
tags, ok := eventData["tags"].([]interface{})
if !ok {
log.Println("Tags field is missing or not an array")
return pubkeys
}
for _, tag := range tags {
tagArray, ok := tag.([]interface{})
if ok && len(tagArray) > 1 && tagArray[0] == "p" {
pubkey, ok := tagArray[1].(string)
if ok {
pubkeys = append(pubkeys, pubkey)
}
}
}
log.Printf("Extracted pubkeys: %v", pubkeys)
return pubkeys
} }

View File

@ -6,52 +6,50 @@ import (
"strconv" "strconv"
) )
// Helper function to check if a pubkey or npub is whitelisted // Check if a pubkey or npub is whitelisted
func IsPubKeyWhitelisted(pubKey string) bool { func IsPubKeyWhitelisted(pubKey string) bool {
cfg := GetConfig() cfg := GetWhitelistConfig()
if !cfg.PubkeyWhitelist.Enabled { if !cfg.PubkeyWhitelist.Enabled {
return true return true
} }
// Check pubkeys for _, whitelistedKey := range cfg.PubkeyWhitelist.Pubkeys {
for _, whitelistedKey := range cfg.PubkeyWhitelist.Pubkeys { if pubKey == whitelistedKey {
if pubKey == whitelistedKey { return true
return true }
} }
}
// Check npubs for _, npub := range cfg.PubkeyWhitelist.Npubs {
for _, npub := range cfg.PubkeyWhitelist.Npubs { decodedPubKey, err := utils.DecodeNpub(npub)
decodedPubKey, err := utils.DecodeNpub(npub) if err != nil {
if err != nil { fmt.Println("Error decoding npub:", err)
fmt.Println("Error decoding npub:", err) continue
continue }
} if pubKey == decodedPubKey {
if pubKey == decodedPubKey { return true
return true }
} }
}
return false return false
} }
// Check if a kind is whitelisted
func IsKindWhitelisted(kind int) bool { func IsKindWhitelisted(kind int) bool {
cfg := GetConfig() cfg := GetWhitelistConfig()
if !cfg.KindWhitelist.Enabled { if !cfg.KindWhitelist.Enabled {
return true return true
} }
// Check event kinds for _, whitelistedKindStr := range cfg.KindWhitelist.Kinds {
for _, whitelistedKindStr := range cfg.KindWhitelist.Kinds { whitelistedKind, err := strconv.Atoi(whitelistedKindStr)
whitelistedKind, err := strconv.Atoi(whitelistedKindStr) if err != nil {
if err != nil { fmt.Println("Error converting whitelisted kind to int:", err)
fmt.Println("Error converting whitelisted kind to int:", err) continue
continue }
} if kind == whitelistedKind {
if kind == whitelistedKind { return true
return true }
} }
}
return false return false
} }

View File

@ -6,33 +6,91 @@ import (
configTypes "grain/config/types" configTypes "grain/config/types"
"grain/server/utils"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
var ( var (
cfg *configTypes.ServerConfig cfg *configTypes.ServerConfig
once sync.Once whitelistCfg *configTypes.WhitelistConfig
blacklistCfg *configTypes.BlacklistConfig
once sync.Once
whitelistOnce sync.Once
blacklistOnce sync.Once
) )
// LoadConfig loads the server configuration from config.yml
func LoadConfig(filename string) (*configTypes.ServerConfig, error) { func LoadConfig(filename string) (*configTypes.ServerConfig, error) {
data, err := os.ReadFile(filename) data, err := os.ReadFile(filename)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var config configTypes.ServerConfig var config configTypes.ServerConfig
err = yaml.Unmarshal(data, &config) err = yaml.Unmarshal(data, &config)
if err != nil { if err != nil {
return nil, err return nil, err
} }
once.Do(func() { // Adjust event time constraints after loading
cfg = &config utils.AdjustEventTimeConstraints(&config)
})
return cfg, nil once.Do(func() {
cfg = &config
})
return cfg, nil
}
// LoadWhitelistConfig loads the whitelist configuration from whitelist.yml
func LoadWhitelistConfig(filename string) (*configTypes.WhitelistConfig, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
var config configTypes.WhitelistConfig
err = yaml.Unmarshal(data, &config)
if err != nil {
return nil, err
}
whitelistOnce.Do(func() {
whitelistCfg = &config
})
return whitelistCfg, nil
} }
func GetConfig() *configTypes.ServerConfig { func GetConfig() *configTypes.ServerConfig {
return cfg return cfg
}
func GetWhitelistConfig() *configTypes.WhitelistConfig {
return whitelistCfg
}
// LoadBlacklistConfig loads the blacklist configuration from blacklist.yml
func LoadBlacklistConfig(filename string) (*configTypes.BlacklistConfig, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
var config configTypes.BlacklistConfig
err = yaml.Unmarshal(data, &config)
if err != nil {
return nil, err
}
blacklistOnce.Do(func() {
blacklistCfg = &config
})
return blacklistCfg, nil
}
func GetBlacklistConfig() *configTypes.BlacklistConfig {
return blacklistCfg
} }

Binary file not shown.

View File

@ -8,4 +8,5 @@ type BlacklistConfig struct {
TempBanDuration int `yaml:"temp_ban_duration"` TempBanDuration int `yaml:"temp_ban_duration"`
PermanentBlacklistPubkeys []string `yaml:"permanent_blacklist_pubkeys"` PermanentBlacklistPubkeys []string `yaml:"permanent_blacklist_pubkeys"`
PermanentBlacklistNpubs []string `yaml:"permanent_blacklist_npubs"` PermanentBlacklistNpubs []string `yaml:"permanent_blacklist_npubs"`
MuteListAuthors []string `yaml:"mutelist_authors"`
} }

View File

@ -1,5 +1,12 @@
package config package config
type EventTimeConstraints struct {
MinCreatedAt int64 `yaml:"min_created_at"` // Minimum allowed timestamp
MinCreatedAtString string `yaml:"min_created_at_string"` // Original string value for parsing (e.g., "now-5m")
MaxCreatedAt int64 `yaml:"max_created_at"` // Maximum allowed timestamp
MaxCreatedAtString string `yaml:"max_created_at_string"` // Original string value for parsing (e.g., "now+5m")
}
type ServerConfig struct { type ServerConfig struct {
MongoDB struct { MongoDB struct {
URI string `yaml:"uri"` URI string `yaml:"uri"`
@ -7,18 +14,16 @@ type ServerConfig struct {
} `yaml:"mongodb"` } `yaml:"mongodb"`
Server struct { Server struct {
Port string `yaml:"port"` Port string `yaml:"port"`
ReadTimeout int `yaml:"read_timeout"` // Timeout in seconds ReadTimeout int `yaml:"read_timeout"`
WriteTimeout int `yaml:"write_timeout"` // Timeout in seconds WriteTimeout int `yaml:"write_timeout"`
IdleTimeout int `yaml:"idle_timeout"` // Timeout in seconds IdleTimeout int `yaml:"idle_timeout"`
MaxConnections int `yaml:"max_connections"` // Maximum number of concurrent connections MaxConnections int `yaml:"max_connections"`
MaxSubscriptionsPerClient int `yaml:"max_subscriptions_per_client"` // Maximum number of subscriptions per client MaxSubscriptionsPerClient int `yaml:"max_subscriptions_per_client"`
} `yaml:"server"` } `yaml:"server"`
RateLimit RateLimitConfig `yaml:"rate_limit"` RateLimit RateLimitConfig `yaml:"rate_limit"`
PubkeyWhitelist PubkeyWhitelistConfig `yaml:"pubkey_whitelist"` Blacklist BlacklistConfig `yaml:"blacklist"`
KindWhitelist KindWhitelistConfig `yaml:"kind_whitelist"` ResourceLimits ResourceLimits `yaml:"resource_limits"`
DomainWhitelist DomainWhitelistConfig `yaml:"domain_whitelist"` Auth AuthConfig `yaml:"auth"`
Blacklist BlacklistConfig `yaml:"blacklist"` EventPurge EventPurgeConfig `yaml:"event_purge"`
ResourceLimits ResourceLimits `yaml:"resource_limits"` EventTimeConstraints EventTimeConstraints `yaml:"event_time_constraints"` // Added this field
Auth AuthConfig `yaml:"auth"`
EventPurge EventPurgeConfig `yaml:"event_purge"`
} }

View File

@ -0,0 +1,19 @@
package config
type WhitelistConfig struct {
PubkeyWhitelist struct {
Enabled bool `yaml:"enabled"`
Pubkeys []string `yaml:"pubkeys"`
Npubs []string `yaml:"npubs"`
} `yaml:"pubkey_whitelist"`
KindWhitelist struct {
Enabled bool `yaml:"enabled"`
Kinds []string `yaml:"kinds"`
} `yaml:"kind_whitelist"`
DomainWhitelist struct {
Enabled bool `yaml:"enabled"`
Domains []string `yaml:"domains"`
} `yaml:"domain_whitelist"`
}

View File

@ -1,6 +0,0 @@
package config
type DomainWhitelistConfig struct {
Enabled bool `yaml:"enabled"`
Domains []string `yaml:"domains"`
}

View File

@ -1,6 +0,0 @@
package config
type KindWhitelistConfig struct {
Enabled bool `yaml:"enabled"`
Kinds []string `yaml:"kinds"`
}

View File

@ -1,7 +0,0 @@
package config
type PubkeyWhitelistConfig struct {
Enabled bool `yaml:"enabled"`
Pubkeys []string `yaml:"pubkeys"`
Npubs []string `yaml:"npubs"`
}

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.22.2
require ( require (
github.com/btcsuite/btcd/btcec/v2 v2.3.4 github.com/btcsuite/btcd/btcec/v2 v2.3.4
github.com/gorilla/websocket v1.5.3
go.mongodb.org/mongo-driver v1.16.0 go.mongodb.org/mongo-driver v1.16.0
golang.org/x/net v0.27.0 golang.org/x/net v0.27.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0

2
go.sum
View File

@ -28,6 +28,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=

103
main.go
View File

@ -21,64 +21,75 @@ import (
) )
func main() { func main() {
utils.EnsureFileExists("config.yml", "app/static/examples/config.example.yml") utils.EnsureFileExists("config.yml", "app/static/examples/config.example.yml")
utils.EnsureFileExists("relay_metadata.json", "app/static/examples/relay_metadata.example.json") utils.EnsureFileExists("whitelist.yml", "app/static/examples/whitelist.example.yml")
utils.EnsureFileExists("blacklist.yml", "app/static/examples/blacklist.example.yml")
utils.EnsureFileExists("relay_metadata.json", "app/static/examples/relay_metadata.example.json")
restartChan := make(chan struct{}) restartChan := make(chan struct{})
go config.WatchConfigFile("config.yml", restartChan) // Critical goroutine go config.WatchConfigFile("config.yml", restartChan)
go config.WatchConfigFile("whitelist.yml", restartChan)
go config.WatchConfigFile("blacklist.yml", restartChan)
go config.WatchConfigFile("relay_metadata.json", restartChan)
signalChan := make(chan os.Signal, 1) signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
var wg sync.WaitGroup var wg sync.WaitGroup
for { for {
wg.Add(1) // Add to WaitGroup for the server goroutine wg.Add(1)
cfg, err := config.LoadConfig("config.yml") cfg, err := config.LoadConfig("config.yml")
if err != nil { if err != nil {
log.Fatal("Error loading config: ", err) log.Fatal("Error loading config: ", err)
} }
// Start event purging in the background _, err = config.LoadWhitelistConfig("whitelist.yml")
go mongo.ScheduleEventPurging(cfg) if err != nil {
log.Fatal("Error loading whitelist config: ", err)
}
config.SetResourceLimit(&cfg.ResourceLimits) // Apply limits once before starting the server _, err = config.LoadBlacklistConfig("blacklist.yml")
if err != nil {
log.Fatal("Error loading blacklist config: ", err)
}
client, err := mongo.InitDB(cfg) // Start event purging in the background.
if err != nil { go mongo.ScheduleEventPurging(cfg)
log.Fatal("Error initializing database: ", err)
}
config.SetRateLimit(cfg) config.SetResourceLimit(&cfg.ResourceLimits)
config.SetSizeLimit(cfg) client, err := mongo.InitDB(cfg)
if err != nil {
log.Fatal("Error initializing database: ", err)
}
config.ClearTemporaryBans() config.SetRateLimit(cfg)
config.SetSizeLimit(cfg)
config.ClearTemporaryBans()
err = utils.LoadRelayMetadataJSON() err = utils.LoadRelayMetadataJSON()
if err != nil { if err != nil {
log.Fatal("Failed to load relay metadata: ", err) log.Fatal("Failed to load relay metadata: ", err)
} }
mux := setupRoutes() mux := setupRoutes()
server := startServer(cfg, mux, &wg)
// Start the server // Monitor for server restart or shutdown signals.
server := startServer(cfg, mux, &wg) select {
case <-restartChan:
select { log.Println("Restarting server...")
case <-restartChan: server.Close()
log.Println("Restarting server...") wg.Wait()
server.Close() // Stop the current server instance time.Sleep(3 * time.Second)
wg.Wait() // Wait for the server goroutine to finish case <-signalChan:
time.Sleep(3 * time.Second) log.Println("Shutting down server...")
server.Close()
case <-signalChan: mongo.DisconnectDB(client)
log.Println("Shutting down server...") wg.Wait()
server.Close() // Stop the server return
mongo.DisconnectDB(client) // Disconnect from MongoDB }
wg.Wait() // Wait for all goroutines to finish }
return
}
}
} }
func setupRoutes() *http.ServeMux { func setupRoutes() *http.ServeMux {

View File

@ -2,6 +2,7 @@ package mongo
import ( import (
"context" "context"
"grain/config"
types "grain/config/types" types "grain/config/types"
"grain/server/utils" "grain/server/utils"
"log" "log"
@ -10,6 +11,7 @@ import (
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
) )
// PurgeOldEvents removes old events based on the configuration and a list of whitelisted pubkeys.
func PurgeOldEvents(cfg *types.EventPurgeConfig, whitelist []string) { func PurgeOldEvents(cfg *types.EventPurgeConfig, whitelist []string) {
if !cfg.Enabled { if !cfg.Enabled {
return return
@ -21,10 +23,12 @@ func PurgeOldEvents(cfg *types.EventPurgeConfig, whitelist []string) {
// Calculate the cutoff time // Calculate the cutoff time
cutoff := time.Now().AddDate(0, 0, -cfg.KeepDurationDays).Unix() cutoff := time.Now().AddDate(0, 0, -cfg.KeepDurationDays).Unix()
// Create the filter for purging old events
filter := bson.M{ filter := bson.M{
"created_at": bson.M{"$lt": cutoff}, // Filter older events "created_at": bson.M{"$lt": cutoff}, // Filter older events
} }
// Exclude whitelisted pubkeys if specified in the config
if cfg.ExcludeWhitelisted && len(whitelist) > 0 { if cfg.ExcludeWhitelisted && len(whitelist) > 0 {
filter["pubkey"] = bson.M{"$nin": whitelist} // Exclude whitelisted pubkeys filter["pubkey"] = bson.M{"$nin": whitelist} // Exclude whitelisted pubkeys
} }
@ -52,7 +56,6 @@ func PurgeOldEvents(cfg *types.EventPurgeConfig, whitelist []string) {
} }
} }
// Example of a periodic purging task
// ScheduleEventPurging runs the event purging at a configurable interval. // ScheduleEventPurging runs the event purging at a configurable interval.
func ScheduleEventPurging(cfg *types.ServerConfig) { func ScheduleEventPurging(cfg *types.ServerConfig) {
// Use the purge interval from the configuration // Use the purge interval from the configuration
@ -61,22 +64,33 @@ func ScheduleEventPurging(cfg *types.ServerConfig) {
defer ticker.Stop() defer ticker.Stop()
for range ticker.C { for range ticker.C {
whitelist := getWhitelistedPubKeys(cfg) // Fetch the whitelisted pubkeys without passing cfg directly
whitelist := getWhitelistedPubKeys()
PurgeOldEvents(&cfg.EventPurge, whitelist) PurgeOldEvents(&cfg.EventPurge, whitelist)
log.Printf("Purged old events, keeping whitelisted pubkeys: %v", whitelist)
} }
} }
// Fetch whitelisted pubkeys from both the config and any additional domains. // Fetch whitelisted pubkeys from both the whitelist config and any additional domains.
func getWhitelistedPubKeys(cfg *types.ServerConfig) []string { func getWhitelistedPubKeys() []string {
whitelistedPubkeys := cfg.PubkeyWhitelist.Pubkeys // Get the whitelist configuration
whitelistCfg := config.GetWhitelistConfig()
if whitelistCfg == nil {
log.Println("whitelistCfg is nil, returning an empty list of whitelisted pubkeys.")
return []string{}
}
// Start with the statically defined pubkeys
whitelistedPubkeys := whitelistCfg.PubkeyWhitelist.Pubkeys
// Fetch pubkeys from domains if domain whitelist is enabled // Fetch pubkeys from domains if domain whitelist is enabled
if cfg.DomainWhitelist.Enabled { if whitelistCfg.DomainWhitelist.Enabled {
domains := cfg.DomainWhitelist.Domains domains := whitelistCfg.DomainWhitelist.Domains
pubkeys, err := utils.FetchPubkeysFromDomains(domains) pubkeys, err := utils.FetchPubkeysFromDomains(domains)
if err != nil { if err != nil {
log.Printf("Error fetching pubkeys from domains: %v", err) log.Printf("Error fetching pubkeys from domains: %v", err)
return whitelistedPubkeys // Return existing whitelisted pubkeys in case of error // Return the existing statically whitelisted pubkeys in case of an error
return whitelistedPubkeys
} }
// Append fetched pubkeys from domains to the whitelisted pubkeys // Append fetched pubkeys from domains to the whitelisted pubkeys
whitelistedPubkeys = append(whitelistedPubkeys, pubkeys...) whitelistedPubkeys = append(whitelistedPubkeys, pubkeys...)

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"grain/config" "grain/config"
"grain/server/db/mongo" "grain/server/db/mongo"
"time"
"grain/server/handlers/response" "grain/server/handlers/response"
"grain/server/utils" "grain/server/utils"
@ -16,7 +17,6 @@ import (
) )
func HandleEvent(ws *websocket.Conn, message []interface{}) { func HandleEvent(ws *websocket.Conn, message []interface{}) {
if len(message) != 2 { if len(message) != 2 {
fmt.Println("Invalid EVENT message format") fmt.Println("Invalid EVENT message format")
response.SendNotice(ws, "", "Invalid EVENT message format") response.SendNotice(ws, "", "Invalid EVENT message format")
@ -44,13 +44,19 @@ func HandleEvent(ws *websocket.Conn, message []interface{}) {
return return
} }
// Validate event timestamps
if !validateEventTimestamp(evt) {
response.SendOK(ws, evt.ID, false, "invalid: event created_at timestamp is out of allowed range")
return
}
// Signature check moved here // Signature check moved here
if !utils.CheckSignature(evt) { if !utils.CheckSignature(evt) {
response.SendOK(ws, evt.ID, false, "invalid: signature verification failed") response.SendOK(ws, evt.ID, false, "invalid: signature verification failed")
return return
} }
eventSize := len(eventBytes) // Calculate event size eventSize := len(eventBytes)
if !handleBlacklistAndWhitelist(ws, evt) { if !handleBlacklistAndWhitelist(ws, evt) {
return return
@ -60,45 +66,120 @@ func HandleEvent(ws *websocket.Conn, message []interface{}) {
return return
} }
// This is where I'll handle storage for multiple database types in the future // Store the event in MongoDB or other storage
mongo.StoreMongoEvent(context.TODO(), evt, ws) mongo.StoreMongoEvent(context.TODO(), evt, ws)
fmt.Println("Event processed:", evt.ID) fmt.Println("Event processed:", evt.ID)
} }
func handleBlacklistAndWhitelist(ws *websocket.Conn, evt nostr.Event) bool { // Validate event timestamps against the configured min and max values
if config.GetConfig().DomainWhitelist.Enabled { func validateEventTimestamp(evt nostr.Event) bool {
domains := config.GetConfig().DomainWhitelist.Domains cfg := config.GetConfig()
pubkeys, err := utils.FetchPubkeysFromDomains(domains) if cfg == nil {
if err != nil { fmt.Println("Server configuration is not loaded")
fmt.Println("Error fetching pubkeys from domains:", err)
response.SendNotice(ws, "", "Error fetching pubkeys from domains")
return false
}
for _, pubkey := range pubkeys {
config.GetConfig().PubkeyWhitelist.Pubkeys = append(config.GetConfig().PubkeyWhitelist.Pubkeys, pubkey)
}
}
if blacklisted, msg := config.CheckBlacklist(evt.PubKey, evt.Content); blacklisted {
response.SendOK(ws, evt.ID, false, msg)
return false return false
} }
if config.GetConfig().KindWhitelist.Enabled && !config.IsKindWhitelisted(evt.Kind) { // Use current time for max and a fixed date for min if not specified
response.SendOK(ws, evt.ID, false, "not allowed: event kind is not whitelisted") now := time.Now().Unix()
return false minCreatedAt := cfg.EventTimeConstraints.MinCreatedAt
if minCreatedAt == 0 {
// Use January 1, 2020, as the default minimum timestamp
minCreatedAt = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix()
} }
if config.GetConfig().PubkeyWhitelist.Enabled && !config.IsPubKeyWhitelisted(evt.PubKey) { maxCreatedAt := cfg.EventTimeConstraints.MaxCreatedAt
response.SendOK(ws, evt.ID, false, "not allowed: pubkey or npub is not whitelisted") if maxCreatedAt == 0 {
// Default to the current time if not set
maxCreatedAt = now
}
// Check if the event's created_at timestamp falls within the allowed range
if evt.CreatedAt < minCreatedAt || evt.CreatedAt > maxCreatedAt {
fmt.Printf("Event %s created_at timestamp %d is out of range [%d, %d]\n", evt.ID, evt.CreatedAt, minCreatedAt, maxCreatedAt)
return false return false
} }
return true return true
} }
func handleBlacklistAndWhitelist(ws *websocket.Conn, evt nostr.Event) bool {
// Get the current whitelist configuration
whitelistCfg := config.GetWhitelistConfig()
if whitelistCfg == nil {
fmt.Println("Whitelist configuration is not loaded.")
response.SendNotice(ws, "", "Internal server error: whitelist configuration is missing")
return false
}
// If domain whitelisting is enabled, dynamically fetch pubkeys from domains
if whitelistCfg.DomainWhitelist.Enabled {
domains := whitelistCfg.DomainWhitelist.Domains
pubkeys, err := utils.FetchPubkeysFromDomains(domains)
if err != nil {
fmt.Println("Error fetching pubkeys from domains:", err)
response.SendNotice(ws, "", "Error fetching pubkeys from domains")
return false
}
// Update the whitelisted pubkeys dynamically
whitelistCfg.PubkeyWhitelist.Pubkeys = append(whitelistCfg.PubkeyWhitelist.Pubkeys, pubkeys...)
}
// Check if the event's pubkey or content is blacklisted
if blacklisted, msg := config.CheckBlacklist(evt.PubKey, evt.Content); blacklisted {
response.SendOK(ws, evt.ID, false, msg)
return false
}
// Check mutelist blacklist
cfg := config.GetConfig()
if cfg == nil {
fmt.Println("Server configuration is not loaded")
response.SendNotice(ws, "", "Internal server error: server configuration is missing")
return false
}
blacklistCfg := config.GetBlacklistConfig()
if blacklistCfg == nil {
fmt.Println("Blacklist configuration is not loaded")
response.SendNotice(ws, "", "Internal server error: blacklist configuration is missing")
return false
}
// Only proceed if there are mutelist event IDs specified
if len(blacklistCfg.MuteListAuthors) > 0 {
localRelayURL := fmt.Sprintf("ws://localhost%s", cfg.Server.Port)
mutelistedPubkeys, err := config.FetchPubkeysFromLocalMuteList(localRelayURL, blacklistCfg.MuteListAuthors)
if err != nil {
fmt.Println("Error fetching pubkeys from mutelist:", err)
response.SendNotice(ws, "", "Error fetching pubkeys from mutelist")
return false
}
for _, mutelistedPubkey := range mutelistedPubkeys {
if evt.PubKey == mutelistedPubkey {
response.SendOK(ws, evt.ID, false, "not allowed: pubkey is in mutelist")
return false
}
}
} else {
fmt.Println("No mutelist event IDs specified in the blacklist configuration")
}
// Check if the event's kind is whitelisted
if whitelistCfg.KindWhitelist.Enabled && !config.IsKindWhitelisted(evt.Kind) {
response.SendOK(ws, evt.ID, false, "not allowed: event kind is not whitelisted")
return false
}
// Check if the event's pubkey is whitelisted
if whitelistCfg.PubkeyWhitelist.Enabled && !config.IsPubKeyWhitelisted(evt.PubKey) {
response.SendOK(ws, evt.ID, false, "not allowed: pubkey or npub is not whitelisted")
return false
}
return true
}
func handleRateAndSizeLimits(ws *websocket.Conn, evt nostr.Event, eventSize int) bool { func handleRateAndSizeLimits(ws *websocket.Conn, evt nostr.Event, eventSize int) bool {
rateLimiter := config.GetRateLimiter() rateLimiter := config.GetRateLimiter()
sizeLimiter := config.GetSizeLimiter() sizeLimiter := config.GetSizeLimiter()

View File

@ -0,0 +1,43 @@
package utils
import (
"fmt"
config "grain/config/types"
"strings"
"time"
)
// Adjusts the event time constraints based on the configuration
func AdjustEventTimeConstraints(cfg *config.ServerConfig) {
now := time.Now()
// Adjust min_created_at based on string value or default to January 1, 2020
if strings.HasPrefix(cfg.EventTimeConstraints.MinCreatedAtString, "now") {
offset := strings.TrimPrefix(cfg.EventTimeConstraints.MinCreatedAtString, "now")
duration, err := time.ParseDuration(offset)
if err != nil {
fmt.Printf("Invalid time offset for min_created_at: %s\n", offset)
cfg.EventTimeConstraints.MinCreatedAt = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix()
} else {
cfg.EventTimeConstraints.MinCreatedAt = now.Add(duration).Unix()
}
} else if cfg.EventTimeConstraints.MinCreatedAt == 0 {
// Default to January 1, 2020, if not set
cfg.EventTimeConstraints.MinCreatedAt = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix()
}
// Adjust max_created_at based on string value or default to current time
if strings.HasPrefix(cfg.EventTimeConstraints.MaxCreatedAtString, "now") {
offset := strings.TrimPrefix(cfg.EventTimeConstraints.MaxCreatedAtString, "now")
duration, err := time.ParseDuration(offset)
if err != nil {
fmt.Printf("Invalid time offset for max_created_at: %s\n", offset)
cfg.EventTimeConstraints.MaxCreatedAt = now.Unix() // Default to now if parsing fails
} else {
cfg.EventTimeConstraints.MaxCreatedAt = now.Add(duration).Unix()
}
} else if cfg.EventTimeConstraints.MaxCreatedAt == 0 {
// Default to the current time if it's set to zero and no "now" keyword is used
cfg.EventTimeConstraints.MaxCreatedAt = now.Unix()
}
}