basic server relay code.

sse-post-key-prefixes
fiatjaf 2020-11-07 18:02:53 -03:00
commit 6158017db0
7 zmienionych plików z 309 dodań i 0 usunięć

99
relay/handlers.go 100644
Wyświetl plik

@ -0,0 +1,99 @@
package main
import (
"database/sql"
"encoding/json"
"errors"
"net/http"
"time"
)
type ErrorResponse struct {
Error error `json:"error"`
}
func queryUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
keys := r.URL.Query()["keys"]
found := make(map[string]int, len(keys))
for _, key := range keys {
var exists bool
err := db.Get(&exists, `SELECT true FROM event WHERE pubkey = $1`, key)
if err != nil {
w.WriteHeader(500)
log.Warn().Err(err).Str("key", key).Msg("failed to check existence")
return
}
if exists {
found[key] = 1
}
}
json.NewEncoder(w).Encode(found)
}
func fetchUserUpdates(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
key := r.URL.Query().Get("key")
var lastUpdates []Event
err := db.Select(&lastUpdates, `
SELECT *
FROM event
WHERE pubkey = $1
ORDER BY time DESC
LIMIT 25
`, key)
if err == sql.ErrNoRows {
lastUpdates = make([]Event, 0)
} else if err != nil {
w.WriteHeader(500)
log.Warn().Err(err).Str("key", key).Msg("failed to fetch updates")
return
}
json.NewEncoder(w).Encode(lastUpdates)
}
func saveUpdate(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
var evt Event
err := json.NewDecoder(r.Body).Decode(&evt)
if err != nil {
w.WriteHeader(400)
return
}
// safety checks
now := time.Now().UTC().Unix()
if uint32(now-3600) > evt.Time || uint32(now+3600) < evt.Time {
w.WriteHeader(400)
return
}
// check serialization and signature
if ok, err := evt.CheckSignature(); err != nil {
w.WriteHeader(400)
json.NewEncoder(w).Encode(ErrorResponse{err})
return
} else if !ok {
w.WriteHeader(400)
json.NewEncoder(w).Encode(ErrorResponse{errors.New("invalid signature")})
return
}
// insert
_, err = db.Exec(`
INSERT INTO event (pubkey, time, kind, content, signature)
VALUES ($1, $2, $3, $4, $5)
`, evt.Pubkey, evt.Time, evt.Kind, evt.Content, evt.Signature)
if err != nil {
w.WriteHeader(500)
log.Warn().Err(err).Str("pubkey", evt.Pubkey).Msg("failed to save")
return
}
w.WriteHeader(201)
}

62
relay/main.go 100644
Wyświetl plik

@ -0,0 +1,62 @@
package main
import (
"io/ioutil"
"net/http"
"os"
"time"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
"github.com/kelseyhightower/envconfig"
"github.com/rs/cors"
"github.com/rs/zerolog"
)
type Settings struct {
Host string `envconfig:"HOST" default:"0.0.0.0"`
Port string `envconfig:"PORT" default:"7447"`
QLDatabase string `envconfig:"QL_DATABASE"`
PostgresDatabase string `envconfig:"POSTGRESQL_DATABASE"`
SQLiteDatabase string `envconfig:"SQLITE_DATABASE"`
}
var s Settings
var err error
var db *sqlx.DB
var router = mux.NewRouter()
var log = zerolog.New(os.Stderr).Output(zerolog.ConsoleWriter{Out: os.Stderr})
func main() {
err = envconfig.Process("", &s)
if err != nil {
log.Fatal().Err(err).Msg("couldn't process envconfig")
}
db, err = initDB()
if err != nil {
log.Fatal().Err(err).Msg("failed to open database")
}
err = db.Ping()
if err != nil {
log.Fatal().Err(err).Msg("failed to connect to database")
}
// create tables, ignore errors
b, _ := ioutil.ReadFile("schema.sql")
_, err = db.Exec(string(b))
log.Print(err)
router.Path("/query_users").Methods("GET").HandlerFunc(queryUsers)
router.Path("/fetch_user_updates").Methods("GET").HandlerFunc(fetchUserUpdates)
router.Path("/save_update").Methods("POST").HandlerFunc(saveUpdate)
srv := &http.Server{
Handler: cors.Default().Handler(router),
Addr: s.Host + ":" + s.Port,
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
log.Debug().Str("addr", srv.Addr).Msg("listening")
srv.ListenAndServe()
}

9
relay/schema.sql 100644
Wyświetl plik

@ -0,0 +1,9 @@
CREATE TABLE event (
pubkey text NOT NULL,
time integer NOT NULL,
kind integer NOT NULL,
content text NOT NULL,
signature text NOT NULL
)
CREATE INDEX pubkeytime ON event (pubkey, time);

Wyświetl plik

@ -0,0 +1,12 @@
// +build postgres
package main
import (
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)
func initDB() (*sqlx.DB, error) {
return sqlx.Connect("postgres", s.PostgresDatabase)
}

Wyświetl plik

@ -0,0 +1,12 @@
// +build !postgres !sqlite
package main
import (
_ "github.com/cznic/ql/driver"
"github.com/jmoiron/sqlx"
)
func initDB() (*sqlx.DB, error) {
return sqlx.Connect("ql2", s.QLDatabase)
}

Wyświetl plik

@ -0,0 +1,12 @@
// +build sqlite
package main
import (
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
)
func initDB() (*sqlx.DB, error) {
return sqlx.Connect("sqlite3", s.SQLITE_DATABASE)
}

103
relay/types.go 100644
Wyświetl plik

@ -0,0 +1,103 @@
package main
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/btcsuite/btcd/btcec"
)
const (
EventSetMetadata uint8 = 0
EventTextNote uint8 = 1
EventDelete uint8 = 2
EventRecommendServer uint8 = 3
)
type Event struct {
Pubkey string `db:"pubkey"`
Time uint32 `db:"time"`
Kind uint8 `db:"kind"`
// - set_metadata
// - text_note
// - delete
Content string `db:"content"`
Signature string `db:"signature"`
}
// Serialize outputs a byte array that can be hashed/signed to identify/authenticate
// this event. An error will be returned if anything is malformed.
func (evt *Event) Serialize() ([]byte, error) {
b := bytes.Buffer{}
// version: 0
b.Write([]byte{0})
// pubkey
pubkeyb, err := hex.DecodeString(evt.Pubkey)
if err != nil {
return nil, err
}
pubkey, err := btcec.ParsePubKey(pubkeyb, btcec.S256())
if err != nil {
return nil, fmt.Errorf("error parsing pubkey: %w", err)
}
if evt.Pubkey != hex.EncodeToString(pubkey.SerializeCompressed()) {
return nil, fmt.Errorf("pubkey is not serialized in compressed format")
}
if _, err = b.Write(pubkeyb); err != nil {
return nil, err
}
// time
var timeb [4]byte
binary.BigEndian.PutUint32(timeb[:], evt.Time)
if _, err := b.Write(timeb[:]); err != nil {
return nil, err
}
// kind
var kindb [1]byte
kindb[0] = evt.Kind
if _, err := b.Write(kindb[:]); err != nil {
return nil, err
}
// content
if _, err = b.Write([]byte(evt.Content)); err != nil {
return nil, err
}
return b.Bytes(), nil
}
// CheckSignature checks if the signature is valid for the serialized event.
// It will call Serialize() and return an error if that raises an error or if
// the signature itself is invalid.
func (evt Event) CheckSignature() (bool, error) {
serialized, err := evt.Serialize()
if err != nil {
return false, fmt.Errorf("serialization error: %w", err)
}
// validity of these is checked by Serialize()
pubkeyb, _ := hex.DecodeString(evt.Pubkey)
pubkey, _ := btcec.ParsePubKey(pubkeyb, btcec.S256())
bsig, err := hex.DecodeString(evt.Signature)
if err != nil {
return false, fmt.Errorf("signature is invalid hex: %w", err)
}
signature, err := btcec.ParseDERSignature(bsig, btcec.S256())
if err != nil {
return false, fmt.Errorf("failed to parse DER signature: %w", err)
}
hash := sha256.Sum256(serialized)
return signature.Verify(hash[:], pubkey), nil
}