mirror of
https://github.com/0ceanSlim/grain.git
synced 2024-11-23 09:07:12 +00:00
Compare commits
7 Commits
9df03646db
...
158f284be9
Author | SHA1 | Date | |
---|---|---|---|
158f284be9 | |||
e6188796d2 | |||
108142b801 | |||
3d88938b7e | |||
21c431dd22 | |||
3de1aeb998 | |||
5133c3a005 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,7 @@
|
||||
/tmp
|
||||
config.yml
|
||||
whitelist.yml
|
||||
blacklist.yml
|
||||
relay_metadata.json
|
||||
grain.exe
|
||||
/build
|
||||
|
16
app/static/examples/blacklist.example.yml
Normal file
16
app/static/examples/blacklist.example.yml
Normal 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.
|
@ -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
|
||||
|
20
app/static/examples/whitelist.example.yml
Normal file
20
app/static/examples/whitelist.example.yml
Normal 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"
|
@ -1,41 +1,43 @@
|
||||
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
|
||||
|
||||
if !blacklistConfig.Enabled {
|
||||
blacklistConfig := GetBlacklistConfig()
|
||||
if blacklistConfig == nil || !blacklistConfig.Enabled {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
log.Printf("Checking blacklist for pubkey: %s", pubkey)
|
||||
|
||||
// Check for permanent blacklist by pubkey or npub
|
||||
// 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
|
||||
// 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
|
||||
// Check for permanent ban based on wordlist.
|
||||
for _, word := range blacklistConfig.PermanentBanWords {
|
||||
if strings.Contains(eventContent, word) {
|
||||
err := AddToPermanentBlacklist(pubkey)
|
||||
@ -46,10 +48,10 @@ func CheckBlacklist(pubkey, eventContent string) (bool, string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for temporary ban based on wordlist
|
||||
// Check for temporary ban based on wordlist.
|
||||
for _, word := range blacklistConfig.TempBanWords {
|
||||
if strings.Contains(eventContent, word) {
|
||||
err := AddToTemporaryBlacklist(pubkey, blacklistConfig)
|
||||
err := AddToTemporaryBlacklist(pubkey, *blacklistConfig)
|
||||
if err != nil {
|
||||
return true, fmt.Sprintf("pubkey %s is temporarily banned and failed to save: %v", pubkey, err)
|
||||
}
|
||||
@ -60,6 +62,7 @@ func CheckBlacklist(pubkey, eventContent string) (bool, string) {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
|
||||
// Checks if a pubkey is temporarily blacklisted
|
||||
func isPubKeyTemporarilyBlacklisted(pubkey string) bool {
|
||||
mu.Lock()
|
||||
@ -142,20 +145,19 @@ 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 {
|
||||
func isPubKeyPermanentlyBlacklisted(pubKey string, blacklistConfig *types.BlacklistConfig) bool {
|
||||
if blacklistConfig == nil || !blacklistConfig.Enabled {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check pubkeys
|
||||
// Check pubkeys.
|
||||
for _, blacklistedKey := range blacklistConfig.PermanentBlacklistPubkeys {
|
||||
if pubKey == blacklistedKey {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check npubs
|
||||
// Check npubs.
|
||||
for _, npub := range blacklistConfig.PermanentBlacklistNpubs {
|
||||
decodedPubKey, err := utils.DecodeNpub(npub)
|
||||
if err != nil {
|
||||
@ -171,34 +173,188 @@ func isPubKeyPermanentlyBlacklisted(pubKey string, blacklistConfig types.Blackli
|
||||
}
|
||||
|
||||
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
|
||||
// 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
|
||||
// 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(configData)
|
||||
data, err := yaml.Marshal(blacklistConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal config: %v", err)
|
||||
return fmt.Errorf("failed to marshal blacklist config: %v", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile("config.yml", data, 0644)
|
||||
err = os.WriteFile("blacklist.yml", data, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write config to file: %v", err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -6,21 +6,19 @@ 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()
|
||||
cfg := GetWhitelistConfig()
|
||||
if !cfg.PubkeyWhitelist.Enabled {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check pubkeys
|
||||
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 {
|
||||
@ -35,13 +33,13 @@ func IsPubKeyWhitelisted(pubKey string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if a kind is whitelisted
|
||||
func IsKindWhitelisted(kind int) bool {
|
||||
cfg := GetConfig()
|
||||
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 {
|
||||
|
@ -6,14 +6,21 @@ import (
|
||||
|
||||
configTypes "grain/config/types"
|
||||
|
||||
"grain/server/utils"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
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 {
|
||||
@ -26,6 +33,9 @@ func LoadConfig(filename string) (*configTypes.ServerConfig, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Adjust event time constraints after loading
|
||||
utils.AdjustEventTimeConstraints(&config)
|
||||
|
||||
once.Do(func() {
|
||||
cfg = &config
|
||||
})
|
||||
@ -33,6 +43,54 @@ func LoadConfig(filename string) (*configTypes.ServerConfig, error) {
|
||||
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
|
||||
}
|
||||
|
||||
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.
@ -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"`
|
||||
}
|
@ -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"`
|
||||
EventTimeConstraints EventTimeConstraints `yaml:"event_time_constraints"` // Added this field
|
||||
}
|
||||
|
19
config/types/whitelistConfig.go
Normal file
19
config/types/whitelistConfig.go
Normal 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"`
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package config
|
||||
|
||||
type DomainWhitelistConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Domains []string `yaml:"domains"`
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package config
|
||||
|
||||
type KindWhitelistConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Kinds []string `yaml:"kinds"`
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package config
|
||||
|
||||
type PubkeyWhitelistConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Pubkeys []string `yaml:"pubkeys"`
|
||||
Npubs []string `yaml:"npubs"`
|
||||
}
|
1
go.mod
1
go.mod
@ -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
2
go.sum
@ -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=
|
||||
|
39
main.go
39
main.go
@ -22,28 +22,42 @@ import (
|
||||
|
||||
func main() {
|
||||
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
|
||||
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)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for {
|
||||
wg.Add(1) // Add to WaitGroup for the server goroutine
|
||||
wg.Add(1)
|
||||
|
||||
cfg, err := config.LoadConfig("config.yml")
|
||||
if err != nil {
|
||||
log.Fatal("Error loading config: ", err)
|
||||
}
|
||||
|
||||
// Start event purging in the background
|
||||
_, err = config.LoadWhitelistConfig("whitelist.yml")
|
||||
if err != nil {
|
||||
log.Fatal("Error loading whitelist config: ", err)
|
||||
}
|
||||
|
||||
_, err = config.LoadBlacklistConfig("blacklist.yml")
|
||||
if err != nil {
|
||||
log.Fatal("Error loading blacklist config: ", err)
|
||||
}
|
||||
|
||||
// Start event purging in the background.
|
||||
go mongo.ScheduleEventPurging(cfg)
|
||||
|
||||
config.SetResourceLimit(&cfg.ResourceLimits) // Apply limits once before starting the server
|
||||
|
||||
config.SetResourceLimit(&cfg.ResourceLimits)
|
||||
client, err := mongo.InitDB(cfg)
|
||||
if err != nil {
|
||||
log.Fatal("Error initializing database: ", err)
|
||||
@ -51,7 +65,6 @@ func main() {
|
||||
|
||||
config.SetRateLimit(cfg)
|
||||
config.SetSizeLimit(cfg)
|
||||
|
||||
config.ClearTemporaryBans()
|
||||
|
||||
err = utils.LoadRelayMetadataJSON()
|
||||
@ -60,22 +73,20 @@ func main() {
|
||||
}
|
||||
|
||||
mux := setupRoutes()
|
||||
|
||||
// Start the server
|
||||
server := startServer(cfg, mux, &wg)
|
||||
|
||||
// Monitor for server restart or shutdown signals.
|
||||
select {
|
||||
case <-restartChan:
|
||||
log.Println("Restarting server...")
|
||||
server.Close() // Stop the current server instance
|
||||
wg.Wait() // Wait for the server goroutine to finish
|
||||
server.Close()
|
||||
wg.Wait()
|
||||
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
|
||||
server.Close()
|
||||
mongo.DisconnectDB(client)
|
||||
wg.Wait()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -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,22 +64,33 @@ 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...)
|
||||
|
@ -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,38 +66,113 @@ 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
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 {
|
||||
if config.GetConfig().DomainWhitelist.Enabled {
|
||||
domains := config.GetConfig().DomainWhitelist.Domains
|
||||
// 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
|
||||
}
|
||||
for _, pubkey := range pubkeys {
|
||||
config.GetConfig().PubkeyWhitelist.Pubkeys = append(config.GetConfig().PubkeyWhitelist.Pubkeys, pubkey)
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
if config.GetConfig().KindWhitelist.Enabled && !config.IsKindWhitelisted(evt.Kind) {
|
||||
// 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
|
||||
}
|
||||
|
||||
if config.GetConfig().PubkeyWhitelist.Enabled && !config.IsPubKeyWhitelisted(evt.PubKey) {
|
||||
// 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
|
||||
}
|
||||
|
43
server/utils/adjustTimeContraints.go
Normal file
43
server/utils/adjustTimeContraints.go
Normal 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()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user