diff --git a/app/static/examples/config.example.yml b/app/static/examples/config.example.yml index 3351225..1888e31 100644 --- a/app/static/examples/config.example.yml +++ b/app/static/examples/config.example.yml @@ -2,6 +2,10 @@ mongodb: uri: mongodb://localhost:27017/ database: grain +auth: + enabled: false # Enable or disable AUTH handling + relay_url: "wss://relay.example.com/" # Specify the relay URL + server: port: :8181 read_timeout: 10 # in seconds diff --git a/config/types/authConfig.go b/config/types/authConfig.go new file mode 100644 index 0000000..bb15bd0 --- /dev/null +++ b/config/types/authConfig.go @@ -0,0 +1,6 @@ +package config + +type AuthConfig struct { + Enabled bool `yaml:"enabled"` + RelayURL string `yaml:"relay_url"` +} \ No newline at end of file diff --git a/config/types/serverConfig.go b/config/types/serverConfig.go index 08b7a43..ba2d618 100644 --- a/config/types/serverConfig.go +++ b/config/types/serverConfig.go @@ -19,4 +19,5 @@ type ServerConfig struct { DomainWhitelist DomainWhitelistConfig `yaml:"domain_whitelist"` Blacklist BlacklistConfig `yaml:"blacklist"` ResourceLimits ResourceLimits `yaml:"resource_limits"` + Auth AuthConfig `yaml:"auth"` } diff --git a/server/handlers/auth.go b/server/handlers/auth.go new file mode 100644 index 0000000..1e4dc83 --- /dev/null +++ b/server/handlers/auth.go @@ -0,0 +1,139 @@ +package handlers + +import ( + "encoding/json" + "errors" + "fmt" + "grain/config" + "grain/server/handlers/response" + "grain/server/utils" + "time" + + relay "grain/server/types" + + "golang.org/x/net/websocket" +) + +// HandleAuth handles the "AUTH" message type as defined in NIP-42 +func HandleAuth(ws *websocket.Conn, message []interface{}) { + if !config.GetConfig().Auth.Enabled { + fmt.Println("AUTH is disabled in the configuration") + response.SendNotice(ws, "", "AUTH is disabled") + return + } + + if len(message) != 2 { + fmt.Println("Invalid AUTH message format") + response.SendNotice(ws, "", "Invalid AUTH message format") + return + } + + authData, ok := message[1].(map[string]interface{}) + if !ok { + fmt.Println("Invalid auth data format") + response.SendNotice(ws, "", "Invalid auth data format") + return + } + authBytes, err := json.Marshal(authData) + if err != nil { + fmt.Println("Error marshaling auth data:", err) + response.SendNotice(ws, "", "Error marshaling auth data") + return + } + + var authEvent relay.Event + err = json.Unmarshal(authBytes, &authEvent) + if err != nil { + fmt.Println("Error unmarshaling auth data:", err) + response.SendNotice(ws, "", "Error unmarshaling auth data") + return + } + + err = VerifyAuthEvent(authEvent) + if err != nil { + response.SendOK(ws, authEvent.ID, false, err.Error()) + return + } + + // Mark the session as authenticated after successful verification + SetAuthenticated(ws) + response.SendOK(ws, authEvent.ID, true, "") +} + +// VerifyAuthEvent verifies the authentication event according to NIP-42 +func VerifyAuthEvent(evt relay.Event) error { + if evt.Kind != 22242 { + return errors.New("invalid: event kind must be 22242") + } + + if time.Since(time.Unix(evt.CreatedAt, 0)) > 10*time.Minute { + return errors.New("invalid: event is too old") + } + + challenge, err := extractTag(evt.Tags, "challenge") + if err != nil { + return errors.New("invalid: challenge tag missing") + } + + relayURL, err := extractTag(evt.Tags, "relay") + if err != nil { + return errors.New("invalid: relay tag missing") + } + + expectedChallenge := GetChallengeForConnection(evt.PubKey) + if challenge != expectedChallenge { + return errors.New("invalid: challenge does not match") + } + + if relayURL != config.GetConfig().Auth.RelayURL { + return errors.New("invalid: relay URL does not match") + } + + if !utils.CheckSignature(evt) { + return errors.New("invalid: signature verification failed") + } + + return nil +} + +// extractTag extracts a specific tag from an event's tags +func extractTag(tags [][]string, key string) (string, error) { + for _, tag := range tags { + if len(tag) >= 2 && tag[0] == key { + return tag[1], nil + } + } + return "", errors.New("tag not found") +} + +// Map to store challenges associated with connections +var challenges = make(map[string]string) +var authSessions = make(map[*websocket.Conn]bool) + +// GetChallengeForConnection retrieves the challenge string for a given connection +func GetChallengeForConnection(pubKey string) string { + mu.Lock() + defer mu.Unlock() + return challenges[pubKey] +} + +// SetChallengeForConnection sets the challenge string for a given connection +func SetChallengeForConnection(pubKey, challenge string) { + mu.Lock() + defer mu.Unlock() + challenges[pubKey] = challenge +} + +// SetAuthenticated marks a connection as authenticated +func SetAuthenticated(ws *websocket.Conn) { + mu.Lock() + defer mu.Unlock() + authSessions[ws] = true +} + +// IsAuthenticated checks if a connection is authenticated +func IsAuthenticated(ws *websocket.Conn) bool { + mu.Lock() + defer mu.Unlock() + return authSessions[ws] +} diff --git a/server/relay.go b/server/relay.go index 1004408..9e73095 100644 --- a/server/relay.go +++ b/server/relay.go @@ -105,6 +105,12 @@ func WebSocketHandler(ws *websocket.Conn) { return } handlers.HandleReq(ws, message, subscriptions) + case "AUTH": + if config.GetConfig().Auth.Enabled { + handlers.HandleAuth(ws, message) + } else { + fmt.Println("Received AUTH message, but AUTH is disabled") + } case "CLOSE": handlers.HandleClose(ws, message) default: