Skip to content

Commit 6158017

Browse files
author
fiatjaf
committed
basic server relay code.
0 parents  commit 6158017

File tree

7 files changed

+309
-0
lines changed

7 files changed

+309
-0
lines changed

relay/handlers.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package main
2+
3+
import (
4+
"database/sql"
5+
"encoding/json"
6+
"errors"
7+
"net/http"
8+
"time"
9+
)
10+
11+
type ErrorResponse struct {
12+
Error error `json:"error"`
13+
}
14+
15+
func queryUsers(w http.ResponseWriter, r *http.Request) {
16+
w.Header().Set("content-type", "application/json")
17+
18+
keys := r.URL.Query()["keys"]
19+
found := make(map[string]int, len(keys))
20+
for _, key := range keys {
21+
var exists bool
22+
err := db.Get(&exists, `SELECT true FROM event WHERE pubkey = $1`, key)
23+
if err != nil {
24+
w.WriteHeader(500)
25+
log.Warn().Err(err).Str("key", key).Msg("failed to check existence")
26+
return
27+
}
28+
if exists {
29+
found[key] = 1
30+
}
31+
}
32+
json.NewEncoder(w).Encode(found)
33+
}
34+
35+
func fetchUserUpdates(w http.ResponseWriter, r *http.Request) {
36+
w.Header().Set("content-type", "application/json")
37+
38+
key := r.URL.Query().Get("key")
39+
40+
var lastUpdates []Event
41+
err := db.Select(&lastUpdates, `
42+
SELECT *
43+
FROM event
44+
WHERE pubkey = $1
45+
ORDER BY time DESC
46+
LIMIT 25
47+
`, key)
48+
if err == sql.ErrNoRows {
49+
lastUpdates = make([]Event, 0)
50+
} else if err != nil {
51+
w.WriteHeader(500)
52+
log.Warn().Err(err).Str("key", key).Msg("failed to fetch updates")
53+
return
54+
}
55+
56+
json.NewEncoder(w).Encode(lastUpdates)
57+
}
58+
59+
func saveUpdate(w http.ResponseWriter, r *http.Request) {
60+
w.Header().Set("content-type", "application/json")
61+
62+
var evt Event
63+
err := json.NewDecoder(r.Body).Decode(&evt)
64+
if err != nil {
65+
w.WriteHeader(400)
66+
return
67+
}
68+
69+
// safety checks
70+
now := time.Now().UTC().Unix()
71+
if uint32(now-3600) > evt.Time || uint32(now+3600) < evt.Time {
72+
w.WriteHeader(400)
73+
return
74+
}
75+
76+
// check serialization and signature
77+
if ok, err := evt.CheckSignature(); err != nil {
78+
w.WriteHeader(400)
79+
json.NewEncoder(w).Encode(ErrorResponse{err})
80+
return
81+
} else if !ok {
82+
w.WriteHeader(400)
83+
json.NewEncoder(w).Encode(ErrorResponse{errors.New("invalid signature")})
84+
return
85+
}
86+
87+
// insert
88+
_, err = db.Exec(`
89+
INSERT INTO event (pubkey, time, kind, content, signature)
90+
VALUES ($1, $2, $3, $4, $5)
91+
`, evt.Pubkey, evt.Time, evt.Kind, evt.Content, evt.Signature)
92+
if err != nil {
93+
w.WriteHeader(500)
94+
log.Warn().Err(err).Str("pubkey", evt.Pubkey).Msg("failed to save")
95+
return
96+
}
97+
98+
w.WriteHeader(201)
99+
}

relay/main.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package main
2+
3+
import (
4+
"io/ioutil"
5+
"net/http"
6+
"os"
7+
"time"
8+
9+
"github.com/gorilla/mux"
10+
"github.com/jmoiron/sqlx"
11+
"github.com/kelseyhightower/envconfig"
12+
"github.com/rs/cors"
13+
"github.com/rs/zerolog"
14+
)
15+
16+
type Settings struct {
17+
Host string `envconfig:"HOST" default:"0.0.0.0"`
18+
Port string `envconfig:"PORT" default:"7447"`
19+
QLDatabase string `envconfig:"QL_DATABASE"`
20+
PostgresDatabase string `envconfig:"POSTGRESQL_DATABASE"`
21+
SQLiteDatabase string `envconfig:"SQLITE_DATABASE"`
22+
}
23+
24+
var s Settings
25+
var err error
26+
var db *sqlx.DB
27+
var router = mux.NewRouter()
28+
var log = zerolog.New(os.Stderr).Output(zerolog.ConsoleWriter{Out: os.Stderr})
29+
30+
func main() {
31+
err = envconfig.Process("", &s)
32+
if err != nil {
33+
log.Fatal().Err(err).Msg("couldn't process envconfig")
34+
}
35+
36+
db, err = initDB()
37+
if err != nil {
38+
log.Fatal().Err(err).Msg("failed to open database")
39+
}
40+
err = db.Ping()
41+
if err != nil {
42+
log.Fatal().Err(err).Msg("failed to connect to database")
43+
}
44+
45+
// create tables, ignore errors
46+
b, _ := ioutil.ReadFile("schema.sql")
47+
_, err = db.Exec(string(b))
48+
log.Print(err)
49+
50+
router.Path("/query_users").Methods("GET").HandlerFunc(queryUsers)
51+
router.Path("/fetch_user_updates").Methods("GET").HandlerFunc(fetchUserUpdates)
52+
router.Path("/save_update").Methods("POST").HandlerFunc(saveUpdate)
53+
54+
srv := &http.Server{
55+
Handler: cors.Default().Handler(router),
56+
Addr: s.Host + ":" + s.Port,
57+
WriteTimeout: 15 * time.Second,
58+
ReadTimeout: 15 * time.Second,
59+
}
60+
log.Debug().Str("addr", srv.Addr).Msg("listening")
61+
srv.ListenAndServe()
62+
}

relay/schema.sql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
CREATE TABLE event (
2+
pubkey text NOT NULL,
3+
time integer NOT NULL,
4+
kind integer NOT NULL,
5+
content text NOT NULL,
6+
signature text NOT NULL
7+
)
8+
9+
CREATE INDEX pubkeytime ON event (pubkey, time);

relay/settings_postgresql.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// +build postgres
2+
3+
package main
4+
5+
import (
6+
"github.com/jmoiron/sqlx"
7+
_ "github.com/lib/pq"
8+
)
9+
10+
func initDB() (*sqlx.DB, error) {
11+
return sqlx.Connect("postgres", s.PostgresDatabase)
12+
}

relay/settings_ql.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// +build !postgres !sqlite
2+
3+
package main
4+
5+
import (
6+
_ "github.com/cznic/ql/driver"
7+
"github.com/jmoiron/sqlx"
8+
)
9+
10+
func initDB() (*sqlx.DB, error) {
11+
return sqlx.Connect("ql2", s.QLDatabase)
12+
}

relay/settings_sqlite.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// +build sqlite
2+
3+
package main
4+
5+
import (
6+
"github.com/jmoiron/sqlx"
7+
_ "github.com/mattn/go-sqlite3"
8+
)
9+
10+
func initDB() (*sqlx.DB, error) {
11+
return sqlx.Connect("sqlite3", s.SQLITE_DATABASE)
12+
}

relay/types.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"crypto/sha256"
6+
"encoding/binary"
7+
"encoding/hex"
8+
"fmt"
9+
10+
"github.com/btcsuite/btcd/btcec"
11+
)
12+
13+
const (
14+
EventSetMetadata uint8 = 0
15+
EventTextNote uint8 = 1
16+
EventDelete uint8 = 2
17+
EventRecommendServer uint8 = 3
18+
)
19+
20+
type Event struct {
21+
Pubkey string `db:"pubkey"`
22+
Time uint32 `db:"time"`
23+
24+
Kind uint8 `db:"kind"`
25+
// - set_metadata
26+
// - text_note
27+
// - delete
28+
29+
Content string `db:"content"`
30+
Signature string `db:"signature"`
31+
}
32+
33+
// Serialize outputs a byte array that can be hashed/signed to identify/authenticate
34+
// this event. An error will be returned if anything is malformed.
35+
func (evt *Event) Serialize() ([]byte, error) {
36+
b := bytes.Buffer{}
37+
38+
// version: 0
39+
b.Write([]byte{0})
40+
41+
// pubkey
42+
pubkeyb, err := hex.DecodeString(evt.Pubkey)
43+
if err != nil {
44+
return nil, err
45+
}
46+
pubkey, err := btcec.ParsePubKey(pubkeyb, btcec.S256())
47+
if err != nil {
48+
return nil, fmt.Errorf("error parsing pubkey: %w", err)
49+
}
50+
if evt.Pubkey != hex.EncodeToString(pubkey.SerializeCompressed()) {
51+
return nil, fmt.Errorf("pubkey is not serialized in compressed format")
52+
}
53+
if _, err = b.Write(pubkeyb); err != nil {
54+
return nil, err
55+
}
56+
57+
// time
58+
var timeb [4]byte
59+
binary.BigEndian.PutUint32(timeb[:], evt.Time)
60+
if _, err := b.Write(timeb[:]); err != nil {
61+
return nil, err
62+
}
63+
64+
// kind
65+
var kindb [1]byte
66+
kindb[0] = evt.Kind
67+
if _, err := b.Write(kindb[:]); err != nil {
68+
return nil, err
69+
}
70+
71+
// content
72+
if _, err = b.Write([]byte(evt.Content)); err != nil {
73+
return nil, err
74+
}
75+
76+
return b.Bytes(), nil
77+
}
78+
79+
// CheckSignature checks if the signature is valid for the serialized event.
80+
// It will call Serialize() and return an error if that raises an error or if
81+
// the signature itself is invalid.
82+
func (evt Event) CheckSignature() (bool, error) {
83+
serialized, err := evt.Serialize()
84+
if err != nil {
85+
return false, fmt.Errorf("serialization error: %w", err)
86+
}
87+
88+
// validity of these is checked by Serialize()
89+
pubkeyb, _ := hex.DecodeString(evt.Pubkey)
90+
pubkey, _ := btcec.ParsePubKey(pubkeyb, btcec.S256())
91+
92+
bsig, err := hex.DecodeString(evt.Signature)
93+
if err != nil {
94+
return false, fmt.Errorf("signature is invalid hex: %w", err)
95+
}
96+
signature, err := btcec.ParseDERSignature(bsig, btcec.S256())
97+
if err != nil {
98+
return false, fmt.Errorf("failed to parse DER signature: %w", err)
99+
}
100+
101+
hash := sha256.Sum256(serialized)
102+
return signature.Verify(hash[:], pubkey), nil
103+
}

0 commit comments

Comments
 (0)