fix: nostr auth #5
3 changed files with 219 additions and 204 deletions
204
nostr/nostr.go
204
nostr/nostr.go
|
@ -1,204 +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"`
|
||||
NIP46 map[string][]string `json:"nip46,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"))
|
||||
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.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 {
|
||||
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().Msg(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
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
// always allow auth messages
|
||||
if event.Kind == 22242 {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// 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("restricted: pubkey %s is not registered to any users", event.PubKey)
|
||||
}
|
||||
return false, ""
|
||||
}
|
121
nostr/relay.go
Normal file
121
nostr/relay.go
Normal file
|
@ -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, ""
|
||||
}
|
98
nostr/well-known.go
Normal file
98
nostr/well-known.go
Normal file
|
@ -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
|
||||
}
|
Loading…
Reference in a new issue