basic event and relaypool.

This commit is contained in:
fiatjaf 2021-01-31 11:05:09 -03:00
commit 32c21eb652
5 changed files with 481 additions and 0 deletions

150
event/event.go Normal file
View File

@ -0,0 +1,150 @@
package event
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"math/big"
"github.com/fiatjaf/bip340"
)
const (
KindSetMetadata uint8 = 0
KindTextNote uint8 = 1
KindRecommendServer uint8 = 2
KindContactList uint8 = 3
)
type Event struct {
ID string `json:"id"` // it's the hash of the serialized event
PubKey string `json:"pubkey"`
CreatedAt uint32 `json:"created_at"`
Kind uint8 `json:"kind"`
Tags Tags `json:"tags"`
Content string `json:"content"`
Sig string `json:"sig"`
}
type Tags []Tag
func (t Tags) Scan(src interface{}) error {
var jtags []byte = make([]byte, 0)
switch v := src.(type) {
case []byte:
jtags = v
case string:
jtags = []byte(v)
default:
return errors.New("couldn't scan tags, it's not a json string")
}
json.Unmarshal(jtags, t)
return nil
}
type Tag []interface{}
// Serialize outputs a byte array that can be hashed/signed to identify/authenticate
func (evt *Event) Serialize() []byte {
// the serialization process is just putting everything into a JSON array
// so the order is kept
arr := make([]interface{}, 6)
// version: 0
arr[0] = 0
// pubkey
arr[1] = evt.PubKey
// created_at
arr[2] = int64(evt.CreatedAt)
// kind
arr[3] = int64(evt.Kind)
// tags
if evt.Tags != nil {
arr[4] = evt.Tags
} else {
arr[4] = make([]bool, 0)
}
// content
arr[5] = evt.Content
serialized, _ := json.Marshal(arr)
return serialized
}
// CheckSignature checks if the signature is valid for the id
// (which is a hash of the serialized event content).
// returns an error if the signature itself is invalid.
func (evt Event) CheckSignature() (bool, error) {
// read and check pubkey
pubkeyb, err := hex.DecodeString(evt.PubKey)
if err != nil {
return false, err
}
if len(pubkeyb) != 32 {
return false, fmt.Errorf("pubkey must be 32 bytes, not %d", len(pubkeyb))
}
// check tags
for _, tag := range evt.Tags {
for _, item := range tag {
switch item.(type) {
case string, int64, float64, int, bool:
// fine
default:
// not fine
return false, fmt.Errorf("tag contains an invalid value %v", item)
}
}
}
sig, err := hex.DecodeString(evt.Sig)
if err != nil {
return false, fmt.Errorf("signature is invalid hex: %w", err)
}
if len(sig) != 64 {
return false, fmt.Errorf("signature must be 64 bytes, not %d", len(sig))
}
var p [32]byte
copy(p[:], pubkeyb)
var s [64]byte
copy(s[:], sig)
h := sha256.Sum256(evt.Serialize())
return bip340.Verify(p, h, s)
}
// Sign signs an event with a given privateKey
func (evt Event) Sign(privateKey string) error {
h := sha256.Sum256(evt.Serialize())
s, _ := new(big.Int).SetString(privateKey, 16)
if s == nil {
return errors.New("invalid private key " + privateKey)
}
aux := make([]byte, 32)
rand.Read(aux)
sig, err := bip340.Sign(s, h, aux)
if err != nil {
return err
}
evt.Sig = hex.EncodeToString(sig[:])
return nil
}

8
go.mod Normal file
View File

@ -0,0 +1,8 @@
module github.com/fiatjaf/go-nostr
go 1.15
require (
github.com/fiatjaf/bip340 v1.0.0
github.com/gorilla/websocket v1.4.2
)

48
go.sum Normal file
View File

@ -0,0 +1,48 @@
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/btcsuite/btcd v0.0.0-20190109040709-5bda5314ca95 h1:bmv+LE3sbjb/M06u2DBi92imeKj7KnCUBOvyZYqI8d8=
github.com/btcsuite/btcd v0.0.0-20190109040709-5bda5314ca95/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcutil v0.0.0-20190112041146-bf1e1be93589/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fiatjaf/bip340 v1.0.0 h1:mpwbm+0KC9BXB/7/pnac4e4N1TiuppyEVXxtVAXj75k=
github.com/fiatjaf/bip340 v1.0.0/go.mod h1:MxAz+5FQUTW4OT2gnCBC6Our486wmqf72ykZIrh7+is=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

243
relaypool/relaypool.go Normal file
View File

@ -0,0 +1,243 @@
package relaypool
import (
"encoding/json"
"errors"
"log"
"github.com/fiatjaf/go-nostr/event"
nostrutils "github.com/fiatjaf/go-nostr/utils"
"github.com/gorilla/websocket"
)
type RelayPool struct {
SecretKey *string
Relays map[string]Policy
websockets map[string]*websocket.Conn
Events chan *EventMessage
Notices chan *NoticeMessage
SubscribedKeys []string
SubscribedEvents []string
}
type Policy struct {
SimplePolicy
ReadSpecific map[string]SimplePolicy
}
type SimplePolicy struct {
Read bool
Write bool
}
type EventMessage struct {
Event event.Event
Context byte
Relay string
}
func (em *EventMessage) UnmarshalJSON(b []byte) error {
var temp []json.RawMessage
if err := json.Unmarshal(b, temp); err != nil {
return err
}
if len(temp) < 2 {
return errors.New("message is not an array of 2 or more")
}
if err := json.Unmarshal(temp[0], em.Event); err != nil {
return err
}
var context string
if err := json.Unmarshal(temp[1], &context); err != nil {
return err
}
em.Context = context[0]
return nil
}
type NoticeMessage struct {
Message string
Relay string
}
func (nm *NoticeMessage) UnmarshalJSON(b []byte) error {
var temp []json.RawMessage
if err := json.Unmarshal(b, temp); err != nil {
return err
}
if len(temp) < 2 {
return errors.New("message is not an array of 2 or more")
}
var tag string
if err := json.Unmarshal(temp[0], &tag); err != nil {
return err
}
if tag != "notice" {
return errors.New("tag is not 'notice'")
}
if err := json.Unmarshal(temp[1], &nm.Message); err != nil {
return err
}
return nil
}
// New creates a new RelayPool with no relays in it
func New() *RelayPool {
return &RelayPool{
Relays: make(map[string]Policy),
websockets: make(map[string]*websocket.Conn),
Events: make(chan *EventMessage),
Notices: make(chan *NoticeMessage),
}
}
// Add adds a new relay to the pool, if policy is nil, it will be a simple
// read+write policy.
func (r *RelayPool) Add(url string, policy *Policy) {
if policy == nil {
policy = &Policy{SimplePolicy: SimplePolicy{Read: true, Write: true}}
}
nm := nostrutils.NormalizeURL(url)
if nm == "" {
return
}
r.Relays[nm] = *policy
conn, _, err := websocket.DefaultDialer.Dial(nostrutils.WebsocketURL(url), nil)
if err != nil {
return
}
defer r.Remove(nm)
done := make(chan struct{})
r.websockets[nm] = conn
go func() {
defer close(done)
for {
typ, message, err := conn.ReadMessage()
if err != nil {
log.Println("read error: ", err)
return
}
if typ == websocket.PingMessage {
conn.WriteMessage(websocket.PongMessage, nil)
}
if typ != websocket.TextMessage || len(message) == 0 || message[0] != '[' {
continue
}
var noticeMessage *NoticeMessage
var eventMessage *EventMessage
err = json.Unmarshal(message, eventMessage)
if err == nil {
eventMessage.Relay = nm
r.Events <- eventMessage
} else {
err = json.Unmarshal(message, noticeMessage)
if err == nil {
noticeMessage.Relay = nm
r.Notices <- noticeMessage
} else {
continue
}
}
}
}()
}
// Remove removes a relay from the pool.
func (r *RelayPool) Remove(url string) {
nm := nostrutils.NormalizeURL(url)
if conn, ok := r.websockets[nm]; ok {
conn.Close()
}
delete(r.Relays, nm)
delete(r.websockets, nm)
}
func (r *RelayPool) SubKey(key string) {
for _, conn := range r.websockets {
conn.WriteMessage(websocket.TextMessage, []byte("sub-key:"+key))
}
}
func (r *RelayPool) UnsubKey(key string) {
for _, conn := range r.websockets {
conn.WriteMessage(websocket.TextMessage, []byte("unsub-key:"+key))
}
}
func (r *RelayPool) SubEvent(id string) {
for _, conn := range r.websockets {
conn.WriteMessage(websocket.TextMessage, []byte("sub-event:"+id))
}
}
func (r *RelayPool) ReqFeed(opts map[string]interface{}) {
var sopts string
if opts == nil {
sopts = "{}"
} else {
jopts, _ := json.Marshal(opts)
sopts = string(jopts)
}
for _, conn := range r.websockets {
conn.WriteMessage(websocket.TextMessage, []byte("req-feed:"+sopts))
}
}
func (r *RelayPool) ReqEvent(id string, opts map[string]interface{}) {
if opts == nil {
opts = make(map[string]interface{})
}
opts["id"] = id
jopts, _ := json.Marshal(opts)
sopts := string(jopts)
for _, conn := range r.websockets {
conn.WriteMessage(websocket.TextMessage, []byte("req-event:"+sopts))
}
}
func (r *RelayPool) ReqKey(key string, opts map[string]interface{}) {
if opts == nil {
opts = make(map[string]interface{})
}
opts["key"] = key
jopts, _ := json.Marshal(opts)
sopts := string(jopts)
for _, conn := range r.websockets {
conn.WriteMessage(websocket.TextMessage, []byte("req-key:"+sopts))
}
}
func (r *RelayPool) PublishEvent(evt *event.Event) error {
if r.SecretKey == nil && evt.Sig == "" {
return errors.New("PublishEvent needs either a signed event to publish or to have been configured with a .SecretKey.")
}
if evt.Sig == "" {
evt.Sign(*r.SecretKey)
}
jevt, _ := json.Marshal(evt)
for _, conn := range r.websockets {
conn.WriteMessage(websocket.TextMessage, jevt)
}
return nil
}

32
utils/utils.go Normal file
View File

@ -0,0 +1,32 @@
package nostrutils
import (
"net/url"
"strings"
)
func NormalizeURL(u string) string {
if !strings.HasPrefix(u, "http") && !strings.HasPrefix(u, "ws") {
u = "ws://" + u
}
p, err := url.Parse(u)
if err != nil {
return ""
}
if strings.HasSuffix(p.RawPath, "/") {
p.RawPath = p.RawPath[0 : len(p.RawPath)-1]
}
if strings.HasSuffix(p.RawPath, "/ws") {
p.RawPath = p.RawPath[0 : len(p.RawPath)-3]
}
return p.String()
}
func WebsocketURL(u string) string {
p, _ := url.Parse(NormalizeURL(u))
p.RawPath += "/ws"
return p.String()
}