diff --git a/compose.yaml b/compose.yaml index a832251..ee984c2 100644 --- a/compose.yaml +++ b/compose.yaml @@ -23,8 +23,8 @@ services: - POSTGRES_DB=khatru-relay - POSTGRES_PASSWORD=postgres - POSTGRES_USER=postgres - # expose: - # - 5432 + ports: + - 127.0.0.1:5432:5432 healthcheck: test: [ "CMD", "pg_isready" ] interval: 10s diff --git a/go.mod b/go.mod index c23afe4..7e16dd2 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.24.4 require ( github.com/fiatjaf/eventstore v0.16.2 github.com/fiatjaf/khatru v0.19.1 + github.com/lib/pq v1.10.9 + github.com/nbd-wtf/go-nostr v0.52.3 ) require ( @@ -26,11 +28,9 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect - github.com/lib/pq v1.10.9 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/nbd-wtf/go-nostr v0.52.3 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/rs/cors v1.11.1 // indirect github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect diff --git a/main.go b/main.go index 1ae5d11..5ef2d93 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,19 @@ package main import ( + "context" + "database/sql" "fmt" "net/http" "os" + "time" "github.com/fiatjaf/eventstore/postgresql" "github.com/fiatjaf/khatru" + "github.com/fiatjaf/khatru/policies" + _ "github.com/lib/pq" + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip86" ) func getEnv(key, fallback string) string { @@ -16,6 +23,34 @@ func getEnv(key, fallback string) string { return fallback } +func initManagementDB(db *sql.DB) error { + // create allowed_pubkeys table + _, err := db.Exec(` + CREATE TABLE IF NOT EXISTS allowed_pubkeys ( + pubkey TEXT PRIMARY KEY, + reason TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW() + ) + `) + if err != nil { + return fmt.Errorf("failed to create allowed_pubkeys table: %w", err) + } + + // create banned_pubkeys table + _, err = db.Exec(` + CREATE TABLE IF NOT EXISTS banned_pubkeys ( + pubkey TEXT PRIMARY KEY, + reason TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW() + ) + `) + if err != nil { + return fmt.Errorf("failed to create banned_pubkeys table: %w", err) + } + + return nil +} + func main() { // create the relay instance relay := khatru.NewRelay() @@ -37,6 +72,36 @@ func main() { relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent) relay.ReplaceEvent = append(relay.ReplaceEvent, db.ReplaceEvent) + relay.RejectEvent = append(relay.RejectEvent, policies.ValidateKind) + + // setup management database (second connection for NIP-86) + managementDB, err := sql.Open("postgres", getEnv("DATABASE_URL", "postgresql://postgres:postgres@db:5432/khatru-relay?sslmode=disable")) + if err != nil { + panic(err) + } + defer managementDB.Close() + + // initialize management tables + if err := initManagementDB(managementDB); err != nil { + panic(err) + } + + relay.RejectEvent = append(relay.RejectEvent, + func(ctx context.Context, event *nostr.Event) (reject bool, msg string) { + var reason string + row := managementDB.QueryRowContext(ctx, `SELECT reason FROM banned_pubkeys WHERE pubkey = $1`, event.PubKey) + switch err := row.Scan(&reason); err { + case sql.ErrNoRows: + return false, "" + case nil: + return true, fmt.Sprintf("pubkey %s banned: %s", event.PubKey, reason) + default: + // on unexpected DB errors, do not reject the event solely because of the failure + return false, "" + } + }, + ) + // // there are many other configurable things you can set // relay.RejectEvent = append(relay.RejectEvent, // // built-in policies @@ -69,6 +134,71 @@ func main() { // }, // ) + // management endpoints + relay.ManagementAPI.RejectAPICall = append(relay.ManagementAPI.RejectAPICall, + func(ctx context.Context, mp nip86.MethodParams) (reject bool, msg string) { + user := khatru.GetAuthed(ctx) + ownerPubKey := getEnv("RELAY_PUBKEY", "480ec1a7516406090dc042ddf67780ef30f26f3a864e83b417c053a5a611c838") + if user != ownerPubKey { + return true, "auth-required: only relay owner can access management API" + } + return false, "" + }) + + relay.ManagementAPI.AllowPubKey = func(ctx context.Context, pubkey string, reason string) error { + _, err := managementDB.Exec(` + INSERT INTO allowed_pubkeys (pubkey, reason, created_at) + VALUES ($1, $2, $3) + ON CONFLICT (pubkey) DO UPDATE SET reason = $2, created_at = $3 + `, pubkey, reason, time.Now()) + return err + } + + relay.ManagementAPI.BanPubKey = func(ctx context.Context, pubkey string, reason string) error { + _, err := managementDB.Exec(` + INSERT INTO banned_pubkeys (pubkey, reason, created_at) + VALUES ($1, $2, $3) + ON CONFLICT (pubkey) DO UPDATE SET reason = $2, created_at = $3 + `, pubkey, reason, time.Now()) + return err + } + + relay.ManagementAPI.ListAllowedPubKeys = func(ctx context.Context) ([]nip86.PubKeyReason, error) { + rows, err := managementDB.Query(`SELECT pubkey, reason FROM allowed_pubkeys ORDER BY created_at DESC`) + if err != nil { + return nil, err + } + defer rows.Close() + + var result []nip86.PubKeyReason + for rows.Next() { + var pk nip86.PubKeyReason + if err := rows.Scan(&pk.PubKey, &pk.Reason); err != nil { + return nil, err + } + result = append(result, pk) + } + return result, rows.Err() + } + + relay.ManagementAPI.ListBannedPubKeys = func(ctx context.Context) ([]nip86.PubKeyReason, error) { + rows, err := managementDB.Query(`SELECT pubkey, reason FROM banned_pubkeys ORDER BY created_at DESC`) + if err != nil { + return nil, err + } + defer rows.Close() + + var result []nip86.PubKeyReason + for rows.Next() { + var pk nip86.PubKeyReason + if err := rows.Scan(&pk.PubKey, &pk.Reason); err != nil { + return nil, err + } + result = append(result, pk) + } + return result, rows.Err() + } + mux := relay.Router() // set up other http handlers mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {