add closedrelay: like basic, but only allows authorized pubkeys to post.

This commit is contained in:
fiatjaf
2022-05-02 16:55:39 -03:00
parent 42fee9cb1d
commit c075684e65
7 changed files with 328 additions and 0 deletions

1
closedrelay/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
closedrelay

2
closedrelay/Makefile Normal file
View File

@@ -0,0 +1,2 @@
closedrelay: $(shell find .. -name "*.go")
go build -ldflags="-s -w" -o ./closedrelay

24
closedrelay/README Normal file
View File

@@ -0,0 +1,24 @@
closed relay
============
- a basic relay implementation based on relayer.
- uses postgres, which I think must be over version 12 since it uses generated columns.
- only accepts events from specific pubkeys defined via the environment variable `AUTHORIZED_PUBKEYS` (comma-separated).
running
-------
grab a binary from the releases page and run it with the environment variable POSTGRESQL_DATABASE set to some postgres url:
POSTGRESQL_DATABASE=postgres://name:pass@localhost:5432/dbname ./closedrelay
it also accepts a HOST and a PORT environment variables.
compiling
---------
if you know Go you already know this:
go install github.com/fiatjaf/relayer/closedrelay
or something like that.

43
closedrelay/main.go Normal file
View File

@@ -0,0 +1,43 @@
package main
import (
"fmt"
"github.com/fiatjaf/relayer"
"github.com/jmoiron/sqlx"
"github.com/jmoiron/sqlx/reflectx"
"github.com/kelseyhightower/envconfig"
)
type ClosedRelay struct {
PostgresDatabase string `envconfig:"POSTGRESQL_DATABASE"`
AuthorizedPubkeys []string `envconfig:"AUTHORIZED_PUBKEYS"`
DB *sqlx.DB
}
func (b *ClosedRelay) Name() string {
return "ClosedRelay"
}
func (b *ClosedRelay) Init() error {
err := envconfig.Process("", b)
if err != nil {
return fmt.Errorf("couldn't process envconfig: %w", err)
}
if db, err := initDB(b.PostgresDatabase); err != nil {
return fmt.Errorf("failed to open database: %w", err)
} else {
db.Mapper = reflectx.NewMapperFunc("json", sqlx.NameMapper)
b.DB = db
}
return nil
}
func main() {
var b ClosedRelay
relayer.Start(&b)
}

42
closedrelay/postgresql.go Normal file
View File

@@ -0,0 +1,42 @@
package main
import (
"github.com/fiatjaf/relayer"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)
func initDB(dburl string) (*sqlx.DB, error) {
db, err := sqlx.Connect("postgres", dburl)
if err != nil {
return nil, err
}
_, err = db.Exec(`
CREATE FUNCTION tags_to_tagvalues(jsonb) RETURNS text[]
AS 'SELECT array_agg(t->>1) FROM (SELECT jsonb_array_elements($1) AS t)s;'
LANGUAGE SQL
IMMUTABLE
RETURNS NULL ON NULL INPUT;
CREATE TABLE IF NOT EXISTS event (
id text NOT NULL,
pubkey text NOT NULL,
created_at integer NOT NULL,
kind integer NOT NULL,
tags jsonb NOT NULL,
content text NOT NULL,
sig text NOT NULL,
tagvalues text[] GENERATED ALWAYS AS (tags_to_tagvalues(tags)) STORED
);
CREATE UNIQUE INDEX IF NOT EXISTS ididx ON event USING btree (id text_pattern_ops);
CREATE INDEX IF NOT EXISTS pubkeyprefix ON event USING btree (pubkey text_pattern_ops);
CREATE INDEX IF NOT EXISTS timeidx ON event (created_at);
CREATE INDEX IF NOT EXISTS kindidx ON event (kind);
CREATE INDEX IF NOT EXISTS arbitrarytagvalues ON event USING gin (tagvalues);
`)
relayer.Log.Print(err)
return db, nil
}

158
closedrelay/query.go Normal file
View File

@@ -0,0 +1,158 @@
package main
import (
"database/sql"
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/fiatjaf/go-nostr"
"github.com/rs/zerolog/log"
)
func (b *ClosedRelay) QueryEvents(filter *nostr.Filter) (events []nostr.Event, err error) {
var conditions []string
var params []interface{}
if filter == nil {
err = errors.New("filter cannot be null")
return
}
if filter.IDs != nil {
if len(filter.IDs) > 500 {
// too many ids, fail everything
return
}
likeids := make([]string, 0, len(filter.IDs))
for _, id := range filter.IDs {
// to prevent sql attack here we will check if
// these ids are valid 32byte hex
parsed, err := hex.DecodeString(id)
if err != nil || len(parsed) <= 32 {
continue
}
likeids = append(likeids, fmt.Sprintf("id LIKE '%x%%'", parsed))
}
if len(likeids) == 0 {
// ids being [] mean you won't get anything
return
}
conditions = append(conditions, "("+strings.Join(likeids, " OR ")+")")
}
if filter.Authors != nil {
if len(filter.Authors) > 500 {
// too many authors, fail everything
return
}
likekeys := make([]string, 0, len(filter.Authors))
for _, key := range filter.Authors {
// to prevent sql attack here we will check if
// these keys are valid 32byte hex
parsed, err := hex.DecodeString(key)
if err != nil || len(parsed) != 32 {
continue
}
likekeys = append(likekeys, fmt.Sprintf("pubkey LIKE '%x%%'", parsed))
}
if len(likekeys) == 0 {
// authors being [] mean you won't get anything
return
}
conditions = append(conditions, "("+strings.Join(likekeys, " OR ")+")")
}
if filter.Kinds != nil {
if len(filter.Kinds) > 10 {
// too many kinds, fail everything
return
}
if len(filter.Kinds) == 0 {
// kinds being [] mean you won't get anything
return
}
// no sql injection issues since these are ints
inkinds := make([]string, len(filter.Kinds))
for i, kind := range filter.Kinds {
inkinds[i] = strconv.Itoa(kind)
}
conditions = append(conditions, `kind IN (`+strings.Join(inkinds, ",")+`)`)
}
tagQuery := make([]string, 0, 1)
for _, values := range filter.Tags {
if len(values) == 0 {
// any tag set to [] is wrong
return
}
// add these tags to the query
tagQuery = append(tagQuery, values...)
if len(tagQuery) > 10 {
// too many tags, fail everything
return
}
}
if len(tagQuery) > 0 {
arrayBuild := make([]string, len(tagQuery))
for i, tagValue := range tagQuery {
arrayBuild[i] = "?"
params = append(params, tagValue)
}
// we use a very bad implementation in which we only check the tag values and
// ignore the tag names
conditions = append(conditions,
"tagvalues && ARRAY["+strings.Join(arrayBuild, ",")+"]")
}
if filter.Since != nil {
conditions = append(conditions, "created_at > ?")
params = append(params, filter.Since.Unix())
}
if filter.Until != nil {
conditions = append(conditions, "created_at < ?")
params = append(params, filter.Until.Unix())
}
if len(conditions) == 0 {
// fallback
conditions = append(conditions, "true")
}
query := b.DB.Rebind(`SELECT
id, pubkey, created_at, kind, tags, content, sig
FROM event WHERE ` +
strings.Join(conditions, " AND ") +
" ORDER BY created_at LIMIT 100")
rows, err := b.DB.Query(query, params...)
if err != nil && err != sql.ErrNoRows {
log.Warn().Err(err).Interface("filter", filter).Str("query", query).
Msg("failed to fetch events")
return nil, fmt.Errorf("failed to fetch events: %w", err)
}
for rows.Next() {
var evt nostr.Event
var timestamp int64
err := rows.Scan(&evt.ID, &evt.PubKey, &timestamp,
&evt.Kind, &evt.Tags, &evt.Content, &evt.Sig)
if err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
evt.CreatedAt = time.Unix(timestamp, 0)
events = append(events, evt)
}
return events, nil
}

58
closedrelay/save.go Normal file
View File

@@ -0,0 +1,58 @@
package main
import (
"encoding/json"
"fmt"
"strings"
"github.com/fiatjaf/go-nostr"
)
func (b *ClosedRelay) SaveEvent(evt *nostr.Event) error {
// disallow anything from non-authorized pubkeys
for _, pubkey := range b.AuthorizedPubkeys {
if pubkey == evt.PubKey {
goto save
}
}
return fmt.Errorf("event from '%s' not allowed here", evt.PubKey)
save:
// react to different kinds of events
switch evt.Kind {
case nostr.KindSetMetadata:
// delete past set_metadata events from this user
b.DB.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = 0`, evt.PubKey)
case nostr.KindRecommendServer:
// delete past recommend_server events equal to this one
b.DB.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = 2 AND content = $2`,
evt.PubKey, evt.Content)
case nostr.KindContactList:
// delete past contact lists from this same pubkey
b.DB.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = 3`, evt.PubKey)
default:
// delete all but the 10 most recent ones
b.DB.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = $2 AND created_at < (
SELECT created_at FROM event WHERE pubkey = $1
ORDER BY created_at DESC OFFSET 10 LIMIT 1
)`,
evt.PubKey, evt.Kind)
}
// insert
tagsj, _ := json.Marshal(evt.Tags)
_, err := b.DB.Exec(`
INSERT INTO event (id, pubkey, created_at, kind, tags, content, sig)
VALUES ($1, $2, $3, $4, $5, $6, $7)
`, evt.ID, evt.PubKey, evt.CreatedAt.Unix(), evt.Kind, tagsj, evt.Content, evt.Sig)
if err != nil {
if strings.Index(err.Error(), "UNIQUE") != -1 {
// already exists
return nil
}
return fmt.Errorf("failed to save event %s: %w", evt.ID, err)
}
return nil
}