diff --git a/.gitignore b/.gitignore index 4bc1c0d..dd0b93a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /tmp config.yml +whitelist.yml relay_metadata.json grain.exe /build diff --git a/app/static/examples/config.example.yml b/app/static/examples/config.example.yml index 2c1f0eb..78eaf92 100644 --- a/app/static/examples/config.example.yml +++ b/app/static/examples/config.example.yml @@ -63,19 +63,6 @@ rate_limit: limit: 25 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 diff --git a/app/static/examples/whitelist.example.yml b/app/static/examples/whitelist.example.yml new file mode 100644 index 0000000..99b4fd2 --- /dev/null +++ b/app/static/examples/whitelist.example.yml @@ -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" diff --git a/config/Whitelist.go b/config/Whitelist.go index bb6be0b..9022421 100644 --- a/config/Whitelist.go +++ b/config/Whitelist.go @@ -6,52 +6,50 @@ import ( "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 { - cfg := GetConfig() - if !cfg.PubkeyWhitelist.Enabled { - return true - } + cfg := GetWhitelistConfig() + if !cfg.PubkeyWhitelist.Enabled { + return true + } - // Check pubkeys - for _, whitelistedKey := range cfg.PubkeyWhitelist.Pubkeys { - if pubKey == whitelistedKey { - return true - } - } + for _, whitelistedKey := range cfg.PubkeyWhitelist.Pubkeys { + if pubKey == whitelistedKey { + return true + } + } - // Check npubs - for _, npub := range cfg.PubkeyWhitelist.Npubs { - decodedPubKey, err := utils.DecodeNpub(npub) - if err != nil { - fmt.Println("Error decoding npub:", err) - continue - } - if pubKey == decodedPubKey { - return true - } - } + for _, npub := range cfg.PubkeyWhitelist.Npubs { + decodedPubKey, err := utils.DecodeNpub(npub) + if err != nil { + fmt.Println("Error decoding npub:", err) + continue + } + if pubKey == decodedPubKey { + return true + } + } - return false + return false } +// Check if a kind is whitelisted func IsKindWhitelisted(kind int) bool { - cfg := GetConfig() - if !cfg.KindWhitelist.Enabled { - return true - } + cfg := GetWhitelistConfig() + if !cfg.KindWhitelist.Enabled { + return true + } - // Check event kinds - for _, whitelistedKindStr := range cfg.KindWhitelist.Kinds { - whitelistedKind, err := strconv.Atoi(whitelistedKindStr) - if err != nil { - fmt.Println("Error converting whitelisted kind to int:", err) - continue - } - if kind == whitelistedKind { - return true - } - } + for _, whitelistedKindStr := range cfg.KindWhitelist.Kinds { + whitelistedKind, err := strconv.Atoi(whitelistedKindStr) + if err != nil { + fmt.Println("Error converting whitelisted kind to int:", err) + continue + } + if kind == whitelistedKind { + return true + } + } - return false -} + return false +} \ No newline at end of file diff --git a/config/loadConfig.go b/config/loadConfig.go index c8a8104..d6bdcb5 100644 --- a/config/loadConfig.go +++ b/config/loadConfig.go @@ -10,29 +10,56 @@ import ( ) var ( - cfg *configTypes.ServerConfig - once sync.Once + cfg *configTypes.ServerConfig + whitelistCfg *configTypes.WhitelistConfig + once sync.Once + whitelistOnce sync.Once ) +// LoadConfig loads the server configuration from config.yml func LoadConfig(filename string) (*configTypes.ServerConfig, error) { - data, err := os.ReadFile(filename) - if err != nil { - return nil, err - } + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } - var config configTypes.ServerConfig - err = yaml.Unmarshal(data, &config) - if err != nil { - return nil, err - } + var config configTypes.ServerConfig + err = yaml.Unmarshal(data, &config) + if err != nil { + return nil, err + } - once.Do(func() { - cfg = &config - }) + once.Do(func() { + cfg = &config + }) - return cfg, nil + 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 { - return cfg + return cfg } + +func GetWhitelistConfig() *configTypes.WhitelistConfig { + return whitelistCfg +} \ No newline at end of file diff --git a/config/types/serverConfig.go b/config/types/serverConfig.go index 09e89c8..13d41b5 100644 --- a/config/types/serverConfig.go +++ b/config/types/serverConfig.go @@ -13,12 +13,9 @@ type ServerConfig struct { MaxConnections int `yaml:"max_connections"` // Maximum number of concurrent connections MaxSubscriptionsPerClient int `yaml:"max_subscriptions_per_client"` // Maximum number of subscriptions per client } `yaml:"server"` - RateLimit RateLimitConfig `yaml:"rate_limit"` - PubkeyWhitelist PubkeyWhitelistConfig `yaml:"pubkey_whitelist"` - KindWhitelist KindWhitelistConfig `yaml:"kind_whitelist"` - DomainWhitelist DomainWhitelistConfig `yaml:"domain_whitelist"` - Blacklist BlacklistConfig `yaml:"blacklist"` - ResourceLimits ResourceLimits `yaml:"resource_limits"` - Auth AuthConfig `yaml:"auth"` - EventPurge EventPurgeConfig `yaml:"event_purge"` + RateLimit RateLimitConfig `yaml:"rate_limit"` + Blacklist BlacklistConfig `yaml:"blacklist"` + ResourceLimits ResourceLimits `yaml:"resource_limits"` + Auth AuthConfig `yaml:"auth"` + EventPurge EventPurgeConfig `yaml:"event_purge"` } diff --git a/config/types/whitelistConfig.go b/config/types/whitelistConfig.go new file mode 100644 index 0000000..6a196f9 --- /dev/null +++ b/config/types/whitelistConfig.go @@ -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"` +} diff --git a/config/types/whitelistDomainConfig.go b/config/types/whitelistDomainConfig.go deleted file mode 100644 index 74e21f4..0000000 --- a/config/types/whitelistDomainConfig.go +++ /dev/null @@ -1,6 +0,0 @@ -package config - -type DomainWhitelistConfig struct { - Enabled bool `yaml:"enabled"` - Domains []string `yaml:"domains"` -} diff --git a/config/types/whitelistKindConfig.go b/config/types/whitelistKindConfig.go deleted file mode 100644 index 5d458d1..0000000 --- a/config/types/whitelistKindConfig.go +++ /dev/null @@ -1,6 +0,0 @@ -package config - -type KindWhitelistConfig struct { - Enabled bool `yaml:"enabled"` - Kinds []string `yaml:"kinds"` -} diff --git a/config/types/whitelistPubkeyConfig.go b/config/types/whitelistPubkeyConfig.go deleted file mode 100644 index a4f9fb3..0000000 --- a/config/types/whitelistPubkeyConfig.go +++ /dev/null @@ -1,7 +0,0 @@ -package config - -type PubkeyWhitelistConfig struct { - Enabled bool `yaml:"enabled"` - Pubkeys []string `yaml:"pubkeys"` - Npubs []string `yaml:"npubs"` -} diff --git a/main.go b/main.go index 1c33659..59b7bf5 100644 --- a/main.go +++ b/main.go @@ -21,64 +21,64 @@ import ( ) func main() { - utils.EnsureFileExists("config.yml", "app/static/examples/config.example.yml") - utils.EnsureFileExists("relay_metadata.json", "app/static/examples/relay_metadata.example.json") + utils.EnsureFileExists("config.yml", "app/static/examples/config.example.yml") + utils.EnsureFileExists("whitelist.yml", "app/static/examples/whitelist.example.yml") + utils.EnsureFileExists("relay_metadata.json", "app/static/examples/relay_metadata.example.json") - restartChan := make(chan struct{}) - go config.WatchConfigFile("config.yml", restartChan) // Critical goroutine + restartChan := make(chan struct{}) + go config.WatchConfigFile("config.yml", restartChan) - signalChan := make(chan os.Signal, 1) - signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) - var wg sync.WaitGroup - for { - wg.Add(1) // Add to WaitGroup for the server goroutine + var wg sync.WaitGroup + for { + wg.Add(1) - cfg, err := config.LoadConfig("config.yml") - if err != nil { - log.Fatal("Error loading config: ", err) - } + cfg, err := config.LoadConfig("config.yml") + if err != nil { + log.Fatal("Error loading config: ", err) + } - // Start event purging in the background - go mongo.ScheduleEventPurging(cfg) + _, err = config.LoadWhitelistConfig("whitelist.yml") + if err != nil { + log.Fatal("Error loading whitelist config: ", err) + } - config.SetResourceLimit(&cfg.ResourceLimits) // Apply limits once before starting the server + go mongo.ScheduleEventPurging(cfg) - client, err := mongo.InitDB(cfg) - if err != nil { - log.Fatal("Error initializing database: ", err) - } + config.SetResourceLimit(&cfg.ResourceLimits) + client, err := mongo.InitDB(cfg) + if err != nil { + log.Fatal("Error initializing database: ", err) + } - config.SetRateLimit(cfg) - config.SetSizeLimit(cfg) + config.SetRateLimit(cfg) + config.SetSizeLimit(cfg) + config.ClearTemporaryBans() - config.ClearTemporaryBans() + err = utils.LoadRelayMetadataJSON() + if err != nil { + log.Fatal("Failed to load relay metadata: ", err) + } - err = utils.LoadRelayMetadataJSON() - if err != nil { - log.Fatal("Failed to load relay metadata: ", err) - } + mux := setupRoutes() + server := startServer(cfg, mux, &wg) - mux := setupRoutes() - - // Start the server - server := startServer(cfg, mux, &wg) - - select { - case <-restartChan: - log.Println("Restarting server...") - server.Close() // Stop the current server instance - wg.Wait() // Wait for the server goroutine to finish - time.Sleep(3 * time.Second) - - case <-signalChan: - log.Println("Shutting down server...") - server.Close() // Stop the server - mongo.DisconnectDB(client) // Disconnect from MongoDB - wg.Wait() // Wait for all goroutines to finish - return - } - } + select { + case <-restartChan: + log.Println("Restarting server...") + server.Close() + wg.Wait() + time.Sleep(3 * time.Second) + case <-signalChan: + log.Println("Shutting down server...") + server.Close() + mongo.DisconnectDB(client) + wg.Wait() + return + } + } } func setupRoutes() *http.ServeMux { diff --git a/server/db/mongo/purgeEvents.go b/server/db/mongo/purgeEvents.go index 3915ad5..8b1af2d 100644 --- a/server/db/mongo/purgeEvents.go +++ b/server/db/mongo/purgeEvents.go @@ -2,6 +2,7 @@ package mongo import ( "context" + "grain/config" types "grain/config/types" "grain/server/utils" "log" @@ -10,6 +11,7 @@ import ( "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) { if !cfg.Enabled { return @@ -21,10 +23,12 @@ func PurgeOldEvents(cfg *types.EventPurgeConfig, whitelist []string) { // Calculate the cutoff time cutoff := time.Now().AddDate(0, 0, -cfg.KeepDurationDays).Unix() + // Create the filter for purging old events filter := bson.M{ "created_at": bson.M{"$lt": cutoff}, // Filter older events } + // Exclude whitelisted pubkeys if specified in the config if cfg.ExcludeWhitelisted && len(whitelist) > 0 { 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. func ScheduleEventPurging(cfg *types.ServerConfig) { // Use the purge interval from the configuration @@ -61,26 +64,37 @@ func ScheduleEventPurging(cfg *types.ServerConfig) { defer ticker.Stop() for range ticker.C { - whitelist := getWhitelistedPubKeys(cfg) + // Fetch the whitelisted pubkeys without passing cfg directly + whitelist := getWhitelistedPubKeys() 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. -func getWhitelistedPubKeys(cfg *types.ServerConfig) []string { - whitelistedPubkeys := cfg.PubkeyWhitelist.Pubkeys +// Fetch whitelisted pubkeys from both the whitelist config and any additional domains. +func getWhitelistedPubKeys() []string { + // 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 - if cfg.DomainWhitelist.Enabled { - domains := cfg.DomainWhitelist.Domains + if whitelistCfg.DomainWhitelist.Enabled { + domains := whitelistCfg.DomainWhitelist.Domains pubkeys, err := utils.FetchPubkeysFromDomains(domains) if err != nil { 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 whitelistedPubkeys = append(whitelistedPubkeys, pubkeys...) } return whitelistedPubkeys -} +} \ No newline at end of file diff --git a/server/handlers/event.go b/server/handlers/event.go index 9dc7b12..55573c9 100644 --- a/server/handlers/event.go +++ b/server/handlers/event.go @@ -68,37 +68,41 @@ func HandleEvent(ws *websocket.Conn, message []interface{}) { } func handleBlacklistAndWhitelist(ws *websocket.Conn, evt nostr.Event) bool { - if config.GetConfig().DomainWhitelist.Enabled { - domains := config.GetConfig().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 - } - for _, pubkey := range pubkeys { - config.GetConfig().PubkeyWhitelist.Pubkeys = append(config.GetConfig().PubkeyWhitelist.Pubkeys, pubkey) - } - } + whitelistCfg := config.GetWhitelistConfig() - if blacklisted, msg := config.CheckBlacklist(evt.PubKey, evt.Content); blacklisted { - response.SendOK(ws, evt.ID, false, msg) - 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...) + } - if config.GetConfig().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 or content is blacklisted + if blacklisted, msg := config.CheckBlacklist(evt.PubKey, evt.Content); blacklisted { + response.SendOK(ws, evt.ID, false, msg) + return false + } - if config.GetConfig().PubkeyWhitelist.Enabled && !config.IsPubKeyWhitelisted(evt.PubKey) { - response.SendOK(ws, evt.ID, false, "not allowed: pubkey or npub is not whitelisted") - return false - } + // 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 + } - return true + // 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 { rateLimiter := config.GetRateLimiter() sizeLimiter := config.GetSizeLimiter()