kopia lustrzana https://github.com/nostr-protocol/nostr
basic server relay code.
commit
6158017db0
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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);
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
Ładowanie…
Reference in New Issue