diff --git a/nostr/nostr.go b/nostr/nostr.go deleted file mode 100644 index d2b2626..0000000 --- a/nostr/nostr.go +++ /dev/null @@ -1,191 +0,0 @@ -package nostr - -import ( - "context" - "encoding/json" - "fmt" - "net" - "net/http" - "strings" - - "git.devvul.com/asara/gologger" - "git.devvul.com/asara/well-goknown/config" - "github.com/fiatjaf/eventstore/postgresql" - "github.com/fiatjaf/khatru" - "github.com/fiatjaf/khatru/policies" - "github.com/jmoiron/sqlx" - "github.com/nbd-wtf/go-nostr" -) - -var ( - DB *sqlx.DB - RelayDb postgresql.PostgresBackend - relay *khatru.Relay -) - -type nostrUser struct { - Pubkey string `db:"pubkey"` - Relays *string `db:"relays,omitempty"` -} - -type nostrWellKnown struct { - Names map[string]string `json:"names"` - Relays map[string][]string `json:"relays,omitempty"` -} - -func GetNostrAddr(w http.ResponseWriter, r *http.Request) { - l := gologger.Get(config.GetConfig().LogLevel).With().Str("context", "nostr").Logger() - - // get query string for username - r.ParseForm() - name := strings.ToLower(r.FormValue("name")) - - // normalize domain - domain, _, err := net.SplitHostPort(r.Host) - if err != nil { - domain = r.Host - } - - // get owner id for nip05 request - var uid int - err = DB.QueryRow("SELECT owner_id FROM nip05s WHERE name=$1 AND domain=$2", name, domain).Scan(&uid) - if err != nil { - l.Error().Msgf("user (%s@%s) doesn't exist: %s", name, domain, err.Error()) - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) - return - } - - // get the pubkey and relays associated with the nip05 - user := nostrUser{} - err = DB.QueryRow("SELECT pubkey, relays FROM users WHERE id=$1", uid).Scan(&user.Pubkey, &user.Relays) - if err != nil { - l.Error().Msgf("unable to get user info for uid %v: %s", uid, err.Error()) - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) - return - } - - // get all names associated with the pubkey - names := []string{} - err = DB.Select(&names, "SELECT nip05s.name FROM nip05s JOIN users ON nip05s.owner_id = users.id WHERE nip05s.owner_id = $1", uid) - if err != nil { - l.Error().Msgf("unable to get nip05 names for uid %v: %s", uid, err.Error()) - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) - return - } - - // map of names - n := make(map[string]string) - for _, name := range names { - n[name] = user.Pubkey - } - - // map of relays - s := make(map[string][]string) - if user.Relays != nil { - if strings.Contains(*user.Relays, ",") { - s[user.Pubkey] = strings.Split(*user.Relays, ",") - } else { - s[user.Pubkey] = []string{*user.Relays} - } - } - - ret := nostrWellKnown{ - Names: n, - Relays: s, - } - - j, err := json.Marshal(ret) - if err != nil { - l.Error().Msg(err.Error()) - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) - return - } - - l.Debug().Msgf("returning nip05 for %s@%s", name, domain) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - w.Write(j) - return -} - -func NewRelay(version string) *khatru.Relay { - // relay configuration - relay = khatru.NewRelay() - relay.Info.Name = config.GetConfig().RelayName - relay.Info.PubKey = config.GetConfig().RelayPubkey - relay.Info.Description = config.GetConfig().RelayDescription - relay.Info.Icon = config.GetConfig().RelayIcon - relay.Info.Contact = config.GetConfig().RelayContact - relay.Info.Version = version - relay.Info.Software = "https://git.devvul.com/asara/well-goknown" - // contact lists - relay.Info.SupportedNIPs = append(relay.Info.SupportedNIPs, 2) - // dms - relay.Info.SupportedNIPs = append(relay.Info.SupportedNIPs, 4) - - relay.OnConnect = append(relay.OnConnect, func(ctx context.Context) { - khatru.RequestAuth(ctx) - }) - - // storage - relay.StoreEvent = append(relay.StoreEvent, RelayDb.SaveEvent) - relay.QueryEvents = append(relay.QueryEvents, RelayDb.QueryEvents) - relay.CountEvents = append(relay.CountEvents, RelayDb.CountEvents) - relay.DeleteEvent = append(relay.DeleteEvent, RelayDb.DeleteEvent) - - // apply policies - policies.ApplySaneDefaults(relay) - - relay.RejectEvent = append( - relay.RejectEvent, - RejectUnregisteredNpubs, - policies.RejectEventsWithBase64Media, - policies.ValidateKind, - ) - - relay.RejectFilter = append( - relay.RejectFilter, - policies.RejectKind04Snoopers, - ) - return relay -} - -func RejectUnregisteredNpubs(ctx context.Context, event *nostr.Event) (reject bool, msg string) { - l := gologger.Get(config.GetConfig().LogLevel).With().Str("context", "nostr-reject-unregistered").Logger() - authenticatedUser := khatru.GetAuthed(ctx) - - if authenticatedUser == "" { - return true, fmt.Sprintf("auth-required: user (%s) not authed", event.PubKey) - } - - // reject nip-04 messages to users who aren't registered - if event.Kind == 4 { - receiver := event.Tags.GetFirst([]string{"p"}).Value() - var rid int - err := DB.QueryRow("SELECT id FROM users WHERE pubkey=$1", receiver).Scan(&rid) - if err != nil { - rid = -1 - } - - var sid int - err = DB.QueryRow("SELECT id FROM users WHERE pubkey=$1", event.PubKey).Scan(&sid) - if err != nil { - sid = -1 - } - - if rid != -1 || sid != -1 { - l.Debug().Msgf("pubkeys %s or %s not found to be registered", receiver, event.PubKey) - return true, fmt.Sprintf("nobody in this nip04 message is registered to the relay") - } - return false, "" - } - - // check if user is registered - var uid int - err := DB.QueryRow("SELECT id FROM users WHERE pubkey=$1", event.PubKey).Scan(&uid) - if err != nil { - l.Debug().Msgf("kind: %v, pubkey: %s, error: %s", event.Kind, event.PubKey, err.Error()) - return true, fmt.Sprintf("pubkey %s is not registered to any users", event.PubKey) - } - return false, "" -} diff --git a/nostr/relay.go b/nostr/relay.go new file mode 100644 index 0000000..1c6019f --- /dev/null +++ b/nostr/relay.go @@ -0,0 +1,121 @@ +package nostr + +import ( + "context" + "fmt" + + "git.devvul.com/asara/gologger" + "git.devvul.com/asara/well-goknown/config" + "github.com/fiatjaf/eventstore/postgresql" + "github.com/fiatjaf/khatru" + "github.com/fiatjaf/khatru/policies" + "github.com/jmoiron/sqlx" + "github.com/nbd-wtf/go-nostr" +) + +var ( + DB *sqlx.DB + RelayDb postgresql.PostgresBackend + relay *khatru.Relay +) + +type nostrUser struct { + Pubkey string `db:"pubkey"` + Relays *string `db:"relays,omitempty"` +} + +type nostrWellKnown struct { + Names map[string]string `json:"names"` + Relays map[string][]string `json:"relays,omitempty"` + NIP46 map[string][]string `json:"nip46,omitempty"` +} + +func NewRelay(version string) *khatru.Relay { + // relay configuration + relay = khatru.NewRelay() + relay.Info.Name = config.GetConfig().RelayName + relay.Info.PubKey = config.GetConfig().RelayPubkey + relay.Info.Description = config.GetConfig().RelayDescription + relay.Info.Icon = config.GetConfig().RelayIcon + relay.Info.Contact = config.GetConfig().RelayContact + relay.Info.Version = version + relay.Info.Software = "https://git.devvul.com/asara/well-goknown" + // contact lists + relay.Info.SupportedNIPs = []int{ + 1, // basic protocol + 2, // contact lists + 4, // encrypted DMs + 11, // relay info + 42, // auth + 70, // protected events + } + + relay.OnConnect = append(relay.OnConnect, func(ctx context.Context) { + khatru.RequestAuth(ctx) + }) + + // storage + relay.StoreEvent = append(relay.StoreEvent, RelayDb.SaveEvent) + relay.QueryEvents = append(relay.QueryEvents, RelayDb.QueryEvents) + relay.CountEvents = append(relay.CountEvents, RelayDb.CountEvents) + relay.DeleteEvent = append(relay.DeleteEvent, RelayDb.DeleteEvent) + + // apply policies + relay.RejectEvent = append( + relay.RejectEvent, + RejectUnregisteredNpubs, + policies.ValidateKind, + ) + + relay.RejectFilter = append( + relay.RejectFilter, + policies.RejectKind04Snoopers, + ) + return relay +} + +func RejectUnregisteredNpubs(ctx context.Context, event *nostr.Event) (reject bool, msg string) { + l := gologger.Get(config.GetConfig().LogLevel).With().Str("context", "nostr-reject-unregistered").Logger() + + // always allow auth messages + if event.Kind == 22242 { + return false, "" + } + + authenticatedUser := khatru.GetAuthed(ctx) + if authenticatedUser == "" { + l.Debug().Msgf("pubkey not authed: %s", event.PubKey) + return true, fmt.Sprintf("auth-required: interacting with this relay requires authentication") + } + + // reject nip-04 messages to users who aren't registered + if event.Kind == 4 { + receiver := event.Tags.GetFirst([]string{"p"}).Value() + var rid int + err := DB.QueryRow("SELECT id FROM users WHERE pubkey=$1", receiver).Scan(&rid) + if err != nil { + rid = -1 + } + + var sid int + err = DB.QueryRow("SELECT id FROM users WHERE pubkey=$1", authenticatedUser).Scan(&sid) + if err != nil { + sid = -1 + } + + if rid != -1 && sid != -1 { + l.Debug().Msgf("pubkeys %s or %s not found to be registered", receiver, event.PubKey) + return true, fmt.Sprintf("restricted: nobody in this nip04 message is registered to the relay") + } + return false, "" + } + + // check if user is registered + var uid int + err := DB.QueryRow("SELECT id FROM users WHERE pubkey=$1", authenticatedUser).Scan(&uid) + if err != nil { + l.Debug().Msgf("kind: %v, pubkey: %s, error: %s", event.Kind, event.PubKey, err.Error()) + return true, fmt.Sprintf("restricted: pubkey %s is not registered to any users", authenticatedUser) + } + return false, "" +} diff --git a/nostr/well-known.go b/nostr/well-known.go new file mode 100644 index 0000000..a3fa9de --- /dev/null +++ b/nostr/well-known.go @@ -0,0 +1,98 @@ +package nostr + +import ( + "encoding/json" + "net" + "net/http" + "strings" + + "git.devvul.com/asara/gologger" + "git.devvul.com/asara/well-goknown/config" +) + +func GetNostrAddr(w http.ResponseWriter, r *http.Request) { + l := gologger.Get(config.GetConfig().LogLevel).With().Str("context", "nostr").Logger() + + // get query string for username + r.ParseForm() + name := strings.ToLower(r.FormValue("name")) + if name == "_" { + name = "" + } + + // normalize domain + domain, _, err := net.SplitHostPort(r.Host) + if err != nil { + domain = r.Host + } + + // get owner id for nip05 request + var uid int + err = DB.QueryRow("SELECT owner_id FROM nip05s WHERE name=$1 AND domain=$2", name, domain).Scan(&uid) + if err != nil { + l.Debug().Msgf("user (%s@%s) doesn't exist: %s", name, domain, err.Error()) + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + + // get the pubkey and relays associated with the nip05 + user := nostrUser{} + err = DB.QueryRow("SELECT pubkey, relays FROM users WHERE id=$1", uid).Scan(&user.Pubkey, &user.Relays) + if err != nil { + l.Debug().Msgf("unable to get user info for uid %v: %s", uid, err.Error()) + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + + // get all names associated with the pubkey + names := []string{} + err = DB.Select(&names, "SELECT nip05s.name FROM nip05s JOIN users ON nip05s.owner_id = users.id WHERE nip05s.owner_id = $1", uid) + if err != nil { + l.Debug().Msgf("unable to get nip05 names for uid %v: %s", uid, err.Error()) + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + + // map of names + n := make(map[string]string) + for _, name := range names { + if name == "" { + n["_"] = user.Pubkey + } else { + n[name] = user.Pubkey + } + } + + // map of relays + s := make(map[string][]string) + if user.Relays != nil { + if strings.Contains(*user.Relays, ",") { + s[user.Pubkey] = strings.Split(*user.Relays, ",") + } else { + s[user.Pubkey] = []string{*user.Relays} + } + } + + // map of nip46 + t := make(map[string][]string) + t[user.Pubkey] = []string{"wss://relay.devvul.com"} + + ret := nostrWellKnown{ + Names: n, + Relays: s, + NIP46: t, + } + + j, err := json.Marshal(ret) + if err != nil { + l.Error().Msgf("unable to marshal nip05 response: %s", err.Error()) + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + + l.Debug().Msgf("returning nip05 for %s@%s: %s", name, domain, user.Pubkey) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(j) + return +}