Compare commits

..

7 Commits

20 changed files with 666 additions and 279 deletions

2
.gitignore vendored
View File

@ -1,5 +1,7 @@
/tmp
config.yml
whitelist.yml
blacklist.yml
relay_metadata.json
grain.exe
/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_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:
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
@ -63,33 +69,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
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:
enabled: true # Toggle to enable/disable event purging
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
import (
"encoding/json"
"fmt"
types "grain/config/types"
"grain/server/utils"
"io"
"log"
"os"
"strings"
"sync"
"time"
"github.com/gorilla/websocket"
"gopkg.in/yaml.v2"
)
// CheckBlacklist checks if a pubkey is in the blacklist based on event content
func CheckBlacklist(pubkey, eventContent string) (bool, string) {
blacklistConfig := GetConfig().Blacklist
blacklistConfig := GetBlacklistConfig()
if blacklistConfig == nil || !blacklistConfig.Enabled {
return false, ""
}
if !blacklistConfig.Enabled {
return false, ""
}
log.Printf("Checking blacklist for pubkey: %s", pubkey)
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
if isPubKeyPermanentlyBlacklisted(pubkey, blacklistConfig) {
log.Printf("Pubkey %s is permanently blacklisted", pubkey)
return true, fmt.Sprintf("pubkey %s is permanently blacklisted", pubkey)
}
// Check for temporary ban.
if isPubKeyTemporarilyBlacklisted(pubkey) {
log.Printf("Pubkey %s is temporarily blacklisted", pubkey)
return true, fmt.Sprintf("pubkey %s is temporarily blacklisted", pubkey)
}
// Check for temporary ban
if isPubKeyTemporarilyBlacklisted(pubkey) {
log.Printf("Pubkey %s is temporarily blacklisted", pubkey)
return true, fmt.Sprintf("pubkey %s is temporarily blacklisted", pubkey)
}
// Check for permanent ban based on wordlist.
for _, word := range blacklistConfig.PermanentBanWords {
if strings.Contains(eventContent, word) {
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
for _, word := range blacklistConfig.PermanentBanWords {
if strings.Contains(eventContent, word) {
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 temporary ban based on wordlist.
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"
}
}
// Check for temporary ban based on wordlist
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, ""
return false, ""
}
// Checks if a pubkey is temporarily blacklisted
func isPubKeyTemporarilyBlacklisted(pubkey string) bool {
mu.Lock()
@ -142,63 +145,216 @@ func AddToTemporaryBlacklist(pubkey string, blacklistConfig types.BlacklistConfi
return nil
}
// Checks if a pubkey is permanently blacklisted (only using config.yml)
func isPubKeyPermanentlyBlacklisted(pubKey string, blacklistConfig types.BlacklistConfig) bool {
if !blacklistConfig.Enabled {
return false
}
func isPubKeyPermanentlyBlacklisted(pubKey string, blacklistConfig *types.BlacklistConfig) bool {
if blacklistConfig == nil || !blacklistConfig.Enabled {
return false
}
// Check pubkeys
for _, blacklistedKey := range blacklistConfig.PermanentBlacklistPubkeys {
if pubKey == blacklistedKey {
return true
}
}
// Check pubkeys.
for _, blacklistedKey := range blacklistConfig.PermanentBlacklistPubkeys {
if pubKey == blacklistedKey {
return true
}
}
// Check npubs
for _, npub := range blacklistConfig.PermanentBlacklistNpubs {
decodedPubKey, err := utils.DecodeNpub(npub)
if err != nil {
fmt.Println("Error decoding npub:", err)
continue
}
if pubKey == decodedPubKey {
return true
}
}
// Check npubs.
for _, npub := range blacklistConfig.PermanentBlacklistNpubs {
decodedPubKey, err := utils.DecodeNpub(npub)
if err != nil {
fmt.Println("Error decoding npub:", err)
continue
}
if pubKey == decodedPubKey {
return true
}
}
return false
return false
}
func AddToPermanentBlacklist(pubkey string) error {
// Remove the mutex lock from here
blacklistConfig := GetConfig().Blacklist
blacklistConfig := GetBlacklistConfig()
if blacklistConfig == nil {
return fmt.Errorf("blacklist configuration is not loaded")
}
// Check if already blacklisted
if isPubKeyPermanentlyBlacklisted(pubkey, blacklistConfig) {
return fmt.Errorf("pubkey %s is already in the permanent blacklist", pubkey)
}
// Check if already blacklisted.
if isPubKeyPermanentlyBlacklisted(pubkey, blacklistConfig) {
return fmt.Errorf("pubkey %s is already in the permanent blacklist", pubkey)
}
// Add pubkey to the blacklist
blacklistConfig.PermanentBlacklistPubkeys = append(blacklistConfig.PermanentBlacklistPubkeys, pubkey)
// Add pubkey to the permanent blacklist.
blacklistConfig.PermanentBlacklistPubkeys = append(blacklistConfig.PermanentBlacklistPubkeys, pubkey)
// Persist changes to config.yml
return saveBlacklistConfig(blacklistConfig)
// Persist changes to blacklist.yml.
return saveBlacklistConfig(*blacklistConfig)
}
func saveBlacklistConfig(blacklistConfig types.BlacklistConfig) error {
configData := GetConfig()
configData.Blacklist = blacklistConfig
data, err := yaml.Marshal(blacklistConfig)
if err != nil {
return fmt.Errorf("failed to marshal blacklist config: %v", err)
}
data, err := yaml.Marshal(configData)
if err != nil {
return fmt.Errorf("failed to marshal config: %v", err)
}
err = os.WriteFile("blacklist.yml", data, 0644)
if err != nil {
return fmt.Errorf("failed to write config to file: %v", err)
}
err = os.WriteFile("config.yml", data, 0644)
if err != nil {
return fmt.Errorf("failed to write config to file: %v", err)
}
return nil
return nil
}
// FetchPubkeysFromLocalMuteList sends a REQ to the local relay for mute list events.
func FetchPubkeysFromLocalMuteList(localRelayURL string, muteListAuthors []string) ([]string, error) {
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"
)
// 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
}

View File

@ -6,33 +6,91 @@ import (
configTypes "grain/config/types"
"grain/server/utils"
"gopkg.in/yaml.v2"
)
var (
cfg *configTypes.ServerConfig
once sync.Once
cfg *configTypes.ServerConfig
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) {
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
})
// Adjust event time constraints after loading
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 {
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"`
PermanentBlacklistPubkeys []string `yaml:"permanent_blacklist_pubkeys"`
PermanentBlacklistNpubs []string `yaml:"permanent_blacklist_npubs"`
}
MuteListAuthors []string `yaml:"mutelist_authors"`
}

View File

@ -1,5 +1,12 @@
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 {
MongoDB struct {
URI string `yaml:"uri"`
@ -7,18 +14,16 @@ type ServerConfig struct {
} `yaml:"mongodb"`
Server struct {
Port string `yaml:"port"`
ReadTimeout int `yaml:"read_timeout"` // Timeout in seconds
WriteTimeout int `yaml:"write_timeout"` // Timeout in seconds
IdleTimeout int `yaml:"idle_timeout"` // Timeout in seconds
MaxConnections int `yaml:"max_connections"` // Maximum number of concurrent connections
MaxSubscriptionsPerClient int `yaml:"max_subscriptions_per_client"` // Maximum number of subscriptions per client
ReadTimeout int `yaml:"read_timeout"`
WriteTimeout int `yaml:"write_timeout"`
IdleTimeout int `yaml:"idle_timeout"`
MaxConnections int `yaml:"max_connections"`
MaxSubscriptionsPerClient int `yaml:"max_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"`
EventTimeConstraints EventTimeConstraints `yaml:"event_time_constraints"` // Added this field
}

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 (
github.com/btcsuite/btcd/btcec/v2 v2.3.4
github.com/gorilla/websocket v1.5.3
go.mongodb.org/mongo-driver v1.16.0
golang.org/x/net v0.27.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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
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/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=

103
main.go
View File

@ -21,64 +21,75 @@ 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("blacklist.yml", "app/static/examples/blacklist.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)
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)
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
_, err = config.LoadBlacklistConfig("blacklist.yml")
if err != nil {
log.Fatal("Error loading blacklist config: ", err)
}
client, err := mongo.InitDB(cfg)
if err != nil {
log.Fatal("Error initializing database: ", err)
}
// Start event purging in the background.
go mongo.ScheduleEventPurging(cfg)
config.SetRateLimit(cfg)
config.SetSizeLimit(cfg)
config.SetResourceLimit(&cfg.ResourceLimits)
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()
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()
mux := setupRoutes()
server := startServer(cfg, mux, &wg)
// 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
}
}
// Monitor for server restart or shutdown signals.
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 {

View File

@ -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
}
}

View File

@ -6,6 +6,7 @@ import (
"fmt"
"grain/config"
"grain/server/db/mongo"
"time"
"grain/server/handlers/response"
"grain/server/utils"
@ -16,7 +17,6 @@ import (
)
func HandleEvent(ws *websocket.Conn, message []interface{}) {
if len(message) != 2 {
fmt.Println("Invalid EVENT message format")
response.SendNotice(ws, "", "Invalid EVENT message format")
@ -44,13 +44,19 @@ func HandleEvent(ws *websocket.Conn, message []interface{}) {
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
if !utils.CheckSignature(evt) {
response.SendOK(ws, evt.ID, false, "invalid: signature verification failed")
return
}
eventSize := len(eventBytes) // Calculate event size
eventSize := len(eventBytes)
if !handleBlacklistAndWhitelist(ws, evt) {
return
@ -60,45 +66,120 @@ func HandleEvent(ws *websocket.Conn, message []interface{}) {
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)
fmt.Println("Event processed:", evt.ID)
}
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)
}
}
if blacklisted, msg := config.CheckBlacklist(evt.PubKey, evt.Content); blacklisted {
response.SendOK(ws, evt.ID, false, msg)
// Validate event timestamps against the configured min and max values
func validateEventTimestamp(evt nostr.Event) bool {
cfg := config.GetConfig()
if cfg == nil {
fmt.Println("Server configuration is not loaded")
return false
}
if config.GetConfig().KindWhitelist.Enabled && !config.IsKindWhitelisted(evt.Kind) {
response.SendOK(ws, evt.ID, false, "not allowed: event kind is not whitelisted")
return false
// Use current time for max and a fixed date for min if not specified
now := time.Now().Unix()
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) {
response.SendOK(ws, evt.ID, false, "not allowed: pubkey or npub is not whitelisted")
maxCreatedAt := cfg.EventTimeConstraints.MaxCreatedAt
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 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 {
rateLimiter := config.GetRateLimiter()
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()
}
}