Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
0544159d4b
|
|||
|
65e1dd6183
|
|||
|
0e83a56025
|
|||
|
93d6871488
|
|||
|
681cdb3a64
|
|||
|
901b4ff16a
|
|||
|
6d34664cf8
|
|||
|
dac6a30625
|
|||
|
5959d5dc7e
|
|||
|
1d07875652
|
|||
|
8ec0c49ecd
|
|||
| 525df97679 | |||
| e2ad580c65 | |||
| ba287ee644 | |||
| c391b9db46 | |||
| 59f246d304 | |||
| eaed2294bc | |||
| ae0d4f5b68 | |||
|
4ee09ada17
|
|||
|
e91c591a6f
|
|||
|
2323545d4b
|
|||
|
1d18425677
|
|||
|
fc68bcf3cb
|
|||
|
affd6c1ebc
|
|||
|
6e103c454d
|
|||
|
db3f98b8cb
|
|||
|
43404d6a07
|
|||
|
49bdf3f5d7
|
|||
|
a2449e24ae
|
|||
|
5c129e078e
|
|||
|
b28acc0c29
|
|||
|
71b699c5c5
|
|||
|
8164330f29
|
|||
|
1226b1f534
|
|||
|
3aa56ebe66
|
9
.gitignore
vendored
9
.gitignore
vendored
@@ -64,7 +64,7 @@ node_modules/**
|
||||
!.gitmodules
|
||||
!*.txt
|
||||
!*.sum
|
||||
!version
|
||||
!pkg/version
|
||||
!*.service
|
||||
!*.benc
|
||||
!*.png
|
||||
@@ -84,13 +84,13 @@ node_modules/**
|
||||
!*.xml
|
||||
!.name
|
||||
!.gitignore
|
||||
|
||||
!version
|
||||
# ...even if they are in subdirectories
|
||||
!*/
|
||||
/blocklist.json
|
||||
/gui/gui/main.wasm
|
||||
/gui/gui/index.html
|
||||
database/testrealy
|
||||
pkg/database/testrealy
|
||||
/.idea/workspace.xml
|
||||
/.idea/dictionaries/project.xml
|
||||
/.idea/shelf/Add_tombstone_handling__enhance_event_ID_logic__update_imports.xml
|
||||
@@ -99,3 +99,6 @@ database/testrealy
|
||||
/.idea/modules.xml
|
||||
/.idea/orly.dev.iml
|
||||
/.idea/vcs.xml
|
||||
/.idea/codeStyles/codeStyleConfig.xml
|
||||
/.idea/material_theme_project_new.xml
|
||||
/.idea/orly.iml
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
// Package config provides a go-simpler.org/env configuration table and helpers
|
||||
// for working with the list of key/value lists stored in .env files.
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"orly.dev/utils/chk"
|
||||
env2 "orly.dev/utils/env"
|
||||
"orly.dev/utils/log"
|
||||
"orly.dev/utils/lol"
|
||||
"orly.dev/version"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"go-simpler.org/env"
|
||||
|
||||
"orly.dev/utils/apputil"
|
||||
)
|
||||
|
||||
// C is the configuration for realy relay. These are read from the environment
|
||||
// if present, or if a .env file is found in ~/.config/realy/ that is read
|
||||
// instead and overrides anything else.
|
||||
type C struct {
|
||||
AppName string `env:"ORLY_APP_NAME" default:"orly"`
|
||||
Config string `env:"ORLY_CONFIG_DIR" usage:"location for configuration file, which has the name '.env' to make it harder to delete, and is a standard environment KEY=value<newline>... style"`
|
||||
State string `env:"ORLY_STATE_DATA_DIR" usage:"storage location for state data affected by dynamic interactive interfaces"`
|
||||
DataDir string `env:"ORLY_DATA_DIR" usage:"storage location for the ratel event store"`
|
||||
Listen string `env:"ORLY_LISTEN" default:"0.0.0.0" usage:"network listen address"`
|
||||
DNS string `env:"ORLY_DNS" usage:"external DNS name that points at the relay"`
|
||||
Port int `env:"ORLY_PORT" default:"3334" usage:"port to listen on"`
|
||||
LogLevel string `env:"ORLY_LOG_LEVEL" default:"info" usage:"debug level: fatal error warn info debug trace"`
|
||||
DbLogLevel string `env:"ORLY_DB_LOG_LEVEL" default:"info" usage:"debug level: fatal error warn info debug trace"`
|
||||
Pprof bool `env:"ORLY_PPROF" default:"false" usage:"enable pprof on 127.0.0.1:6060"`
|
||||
}
|
||||
|
||||
// New creates a new config.C.
|
||||
func New() (cfg *C, err error) {
|
||||
cfg = &C{}
|
||||
if err = env.Load(cfg, &env.Options{SliceSep: ","}); chk.T(err) {
|
||||
return
|
||||
}
|
||||
if cfg.Config == "" {
|
||||
cfg.Config = filepath.Join(xdg.ConfigHome, cfg.AppName)
|
||||
}
|
||||
if cfg.DataDir == "" {
|
||||
cfg.DataDir = filepath.Join(xdg.DataHome, cfg.AppName)
|
||||
}
|
||||
envPath := filepath.Join(cfg.Config, ".env")
|
||||
if apputil.FileExists(envPath) {
|
||||
var e env2.Env
|
||||
if e, err = env2.GetEnv(envPath); chk.T(err) {
|
||||
return
|
||||
}
|
||||
if err = env.Load(
|
||||
cfg, &env.Options{SliceSep: ",", Source: e},
|
||||
); chk.E(err) {
|
||||
return
|
||||
}
|
||||
lol.SetLogLevel(cfg.LogLevel)
|
||||
log.I.F("loaded configuration from %s", envPath)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// HelpRequested returns true if any of the common types of help invocation are
|
||||
// found as the first command line parameter/flag.
|
||||
func HelpRequested() (help bool) {
|
||||
if len(os.Args) > 1 {
|
||||
switch strings.ToLower(os.Args[1]) {
|
||||
case "help", "-h", "--h", "-help", "--help", "?":
|
||||
help = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetEnv processes os.Args to detect a request for printing the current
|
||||
// settings as a list of environment variable key/values.
|
||||
func GetEnv() (requested bool) {
|
||||
if len(os.Args) > 1 {
|
||||
switch strings.ToLower(os.Args[1]) {
|
||||
case "env":
|
||||
requested = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// KV is a key/value pair.
|
||||
type KV struct{ Key, Value string }
|
||||
|
||||
// KVSlice is a collection of key/value pairs.
|
||||
type KVSlice []KV
|
||||
|
||||
func (kv KVSlice) Len() int { return len(kv) }
|
||||
func (kv KVSlice) Less(i, j int) bool { return kv[i].Key < kv[j].Key }
|
||||
func (kv KVSlice) Swap(i, j int) { kv[i], kv[j] = kv[j], kv[i] }
|
||||
|
||||
// Compose merges two KVSlice together, replacing the values of earlier keys
|
||||
// with same named KV items later in the slice (enabling compositing two
|
||||
// together as a .env, as well as them being composed as structs.
|
||||
func (kv KVSlice) Compose(kv2 KVSlice) (out KVSlice) {
|
||||
// duplicate the initial KVSlice
|
||||
for _, p := range kv {
|
||||
out = append(out, p)
|
||||
}
|
||||
out:
|
||||
for i, p := range kv2 {
|
||||
for j, q := range out {
|
||||
// if the key is repeated, replace the value
|
||||
if p.Key == q.Key {
|
||||
out[j].Value = kv2[i].Value
|
||||
continue out
|
||||
}
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// EnvKV turns a struct with `env` keys (used with go-simpler/env) into a
|
||||
// standard formatted environment variable key/value pair list, one per line.
|
||||
// Note you must dereference a pointer type to use this. This allows the
|
||||
// composition of the config in this file with an extended form with a
|
||||
// customized variant of realy to produce correct environment variables both
|
||||
// read and write.
|
||||
func EnvKV(cfg any) (m KVSlice) {
|
||||
t := reflect.TypeOf(cfg)
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
k := t.Field(i).Tag.Get("env")
|
||||
v := reflect.ValueOf(cfg).Field(i).Interface()
|
||||
var val string
|
||||
switch v.(type) {
|
||||
case string:
|
||||
val = v.(string)
|
||||
case int, bool, time.Duration:
|
||||
val = fmt.Sprint(v)
|
||||
case []string:
|
||||
arr := v.([]string)
|
||||
if len(arr) > 0 {
|
||||
val = strings.Join(arr, ",")
|
||||
}
|
||||
}
|
||||
// this can happen with embedded structs
|
||||
if k == "" {
|
||||
continue
|
||||
}
|
||||
m = append(m, KV{k, val})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// PrintEnv renders the key/values of a config.C to a provided io.Writer.
|
||||
func PrintEnv(cfg *C, printer io.Writer) {
|
||||
kvs := EnvKV(*cfg)
|
||||
sort.Sort(kvs)
|
||||
for _, v := range kvs {
|
||||
_, _ = fmt.Fprintf(printer, "%s=%s\n", v.Key, v.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintHelp outputs a help text listing the configuration options and default
|
||||
// values to a provided io.Writer (usually os.Stderr or os.Stdout).
|
||||
func PrintHelp(cfg *C, printer io.Writer) {
|
||||
_, _ = fmt.Fprintf(
|
||||
printer,
|
||||
"%s %s\n\n", cfg.AppName, version.V,
|
||||
)
|
||||
|
||||
_, _ = fmt.Fprintf(
|
||||
printer,
|
||||
"Environment variables that configure %s:\n\n", cfg.AppName,
|
||||
)
|
||||
|
||||
env.Usage(cfg, printer, &env.Options{SliceSep: ","})
|
||||
_, _ = fmt.Fprintf(
|
||||
printer,
|
||||
"\nCLI parameter 'help' also prints this information\n"+
|
||||
"\n.env file found at the path %s will be automatically "+
|
||||
"loaded for configuration.\nset these two variables for a custom load path,"+
|
||||
" this file will be created on first startup.\nenvironment overrides it and "+
|
||||
"you can also edit the file to set configuration options\n\n"+
|
||||
"use the parameter 'env' to print out the current configuration to the terminal\n\n"+
|
||||
"set the environment using\n\n\t%s env > %s/.env\n", os.Args[0],
|
||||
cfg.Config,
|
||||
cfg.Config,
|
||||
)
|
||||
|
||||
fmt.Fprintf(printer, "\ncurrent configuration:\n\n")
|
||||
PrintEnv(cfg, printer)
|
||||
fmt.Fprintln(printer)
|
||||
return
|
||||
}
|
||||
81
app/main.go
81
app/main.go
@@ -1,81 +0,0 @@
|
||||
// Package app implements the realy nostr relay with a simple follow/mute list authentication scheme and the new HTTP REST based protocol.
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"orly.dev/app/config"
|
||||
"orly.dev/encoders/event"
|
||||
"orly.dev/encoders/filter"
|
||||
"orly.dev/encoders/filters"
|
||||
"orly.dev/interfaces/store"
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
type List map[string]struct{}
|
||||
|
||||
type Relay struct {
|
||||
sync.Mutex
|
||||
*config.C
|
||||
Store store.I
|
||||
}
|
||||
|
||||
func (r *Relay) Name() string { return r.C.AppName }
|
||||
|
||||
func (r *Relay) Storage() store.I { return r.Store }
|
||||
|
||||
func (r *Relay) Init() (err error) {
|
||||
// for _, src := range r.C.Owners {
|
||||
// if len(src) < 1 {
|
||||
// continue
|
||||
// }
|
||||
// dst := make([]byte, len(src)/2)
|
||||
// if _, err = hex.DecBytes(dst, []byte(src)); chk.E(err) {
|
||||
// if dst, err = bech32encoding.NpubToBytes([]byte(src)); chk.E(err) {
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
// r.owners = append(r.owners, dst)
|
||||
// }
|
||||
// if len(r.owners) > 0 {
|
||||
// log.F.C(func() string {
|
||||
// ownerIds := make([]string, len(r.owners))
|
||||
// for i, npub := range r.owners {
|
||||
// ownerIds[i] = hex.Enc(npub)
|
||||
// }
|
||||
// owners := strings.Join(ownerIds, ",")
|
||||
// return fmt.Sprintf("owners %s", owners)
|
||||
// })
|
||||
// r.ZeroLists()
|
||||
// r.CheckOwnerLists(context.Bg())
|
||||
// }
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Relay) AcceptEvent(
|
||||
c context.T, evt *event.E, hr *http.Request,
|
||||
origin string, authedPubkey []byte,
|
||||
) (accept bool, notice string, afterSave func()) {
|
||||
accept = true
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Relay) AcceptFilter(
|
||||
c context.T, hr *http.Request, f *filter.S,
|
||||
authedPubkey []byte,
|
||||
) (allowed *filter.S, ok bool, modified bool) {
|
||||
allowed = f
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Relay) AcceptReq(
|
||||
c context.T, hr *http.Request, id []byte,
|
||||
ff *filters.T, authedPubkey []byte,
|
||||
) (allowed *filters.T, ok bool, modified bool) {
|
||||
|
||||
allowed = ff
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package realy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"orly.dev/interfaces/relay"
|
||||
"orly.dev/utils/normalize"
|
||||
"strings"
|
||||
|
||||
"orly.dev/encoders/event"
|
||||
"orly.dev/interfaces/store"
|
||||
"orly.dev/protocol/socketapi"
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
func (s *Server) addEvent(
|
||||
c context.T, rl relay.I, ev *event.E,
|
||||
hr *http.Request, origin string,
|
||||
authedPubkey []byte,
|
||||
) (accepted bool, message []byte) {
|
||||
|
||||
if ev == nil {
|
||||
return false, normalize.Invalid.F("empty event")
|
||||
}
|
||||
// sto := rl.Storage()
|
||||
// advancedSaver, _ := sto.(relay.AdvancedSaver)
|
||||
// don't allow storing event with protected marker as per nip-70 with auth enabled.
|
||||
// if (s.authRequired || !s.publicReadable) && ev.Tags.ContainsProtectedMarker() {
|
||||
// if len(authedPubkey) == 0 || !bytes.Equal(ev.Pubkey, authedPubkey) {
|
||||
// return false,
|
||||
// []byte(fmt.Sprintf("event with relay marker tag '-' (nip-70 protected event) "+
|
||||
// "may only be published by matching npub: %0x is not %0x",
|
||||
// authedPubkey, ev.Pubkey))
|
||||
// }
|
||||
// }
|
||||
if ev.Kind.IsEphemeral() {
|
||||
} else {
|
||||
// if advancedSaver != nil {
|
||||
// advancedSaver.BeforeSave(c, ev)
|
||||
// }
|
||||
if saveErr := s.Publish(c, ev); saveErr != nil {
|
||||
if errors.Is(saveErr, store.ErrDupEvent) {
|
||||
return false, []byte(saveErr.Error())
|
||||
}
|
||||
errmsg := saveErr.Error()
|
||||
if socketapi.NIP20prefixmatcher.MatchString(errmsg) {
|
||||
if strings.Contains(errmsg, "tombstone") {
|
||||
return false, normalize.Error.F("event was deleted, not storing it again")
|
||||
}
|
||||
if strings.HasPrefix(errmsg, string(normalize.Blocked)) {
|
||||
return false, []byte(errmsg)
|
||||
}
|
||||
return false, []byte(errmsg)
|
||||
} else {
|
||||
return false, []byte(errmsg)
|
||||
}
|
||||
}
|
||||
// if advancedSaver != nil {
|
||||
// advancedSaver.AfterSave(ev)
|
||||
// }
|
||||
}
|
||||
// var authRequired bool
|
||||
// if ar, ok := rl.(relay.Authenticator); ok {
|
||||
// authRequired = ar.AuthRequired()
|
||||
// }
|
||||
// notify subscribers
|
||||
s.listeners.Deliver(ev)
|
||||
accepted = true
|
||||
return
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package realy
|
||||
|
||||
import (
|
||||
"orly.dev/utils/log"
|
||||
)
|
||||
|
||||
func (s *Server) disconnect() {
|
||||
for client := range s.clients {
|
||||
log.I.F("closing client %s", client.RemoteAddr())
|
||||
client.Close()
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package realy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"orly.dev/interfaces/relay"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/log"
|
||||
"orly.dev/version"
|
||||
"sort"
|
||||
|
||||
"orly.dev/protocol/relayinfo"
|
||||
)
|
||||
|
||||
func (s *Server) handleRelayInfo(w http.ResponseWriter, r *http.Request) {
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
log.I.Ln("handling relay information document")
|
||||
var info *relayinfo.T
|
||||
if informationer, ok := s.relay.(relay.Informationer); ok {
|
||||
info = informationer.GetNIP11InformationDocument()
|
||||
} else {
|
||||
supportedNIPs := relayinfo.GetList(
|
||||
relayinfo.BasicProtocol,
|
||||
relayinfo.EncryptedDirectMessage,
|
||||
relayinfo.EventDeletion,
|
||||
relayinfo.RelayInformationDocument,
|
||||
relayinfo.GenericTagQueries,
|
||||
relayinfo.NostrMarketplace,
|
||||
relayinfo.EventTreatment,
|
||||
relayinfo.CommandResults,
|
||||
relayinfo.ParameterizedReplaceableEvents,
|
||||
relayinfo.ExpirationTimestamp,
|
||||
relayinfo.ProtectedEvents,
|
||||
relayinfo.RelayListMetadata,
|
||||
)
|
||||
sort.Sort(supportedNIPs)
|
||||
log.T.Ln("supported NIPs", supportedNIPs)
|
||||
info = &relayinfo.T{
|
||||
Name: s.relay.Name(),
|
||||
Description: version.Description,
|
||||
Nips: supportedNIPs, Software: version.URL,
|
||||
Version: version.V,
|
||||
Limitation: relayinfo.Limits{},
|
||||
Icon: "https://cdn.satellite.earth/ac9778868fbf23b63c47c769a74e163377e6ea94d3f0f31711931663d035c4f6.png",
|
||||
}
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(info); chk.E(err) {
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GenerateDescription(text string, scopes []string) string {
|
||||
if len(scopes) == 0 {
|
||||
return text
|
||||
}
|
||||
result := make([]string, 0)
|
||||
for _, value := range scopes {
|
||||
result = append(result, "`"+value+"`")
|
||||
}
|
||||
return text + "<br/><br/>**Scopes**<br/>" + strings.Join(result, ", ")
|
||||
}
|
||||
|
||||
func GetRemoteFromReq(r *http.Request) (rr string) {
|
||||
// reverse proxy should populate this field so we see the remote not the proxy
|
||||
rem := r.Header.Get("X-Forwarded-For")
|
||||
if rem == "" {
|
||||
rr = r.RemoteAddr
|
||||
} else {
|
||||
splitted := strings.Split(rem, " ")
|
||||
if len(splitted) == 1 {
|
||||
rr = splitted[0]
|
||||
}
|
||||
if len(splitted) == 2 {
|
||||
rr = splitted[1]
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"orly.dev/app/realy/publish"
|
||||
"orly.dev/encoders/event"
|
||||
"orly.dev/interfaces/relay"
|
||||
"orly.dev/interfaces/store"
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
type Server interface {
|
||||
AddEvent(
|
||||
c context.T, rl relay.I, ev *event.E, hr *http.Request,
|
||||
origin string, authedPubkey []byte,
|
||||
) (
|
||||
accepted bool,
|
||||
message []byte,
|
||||
)
|
||||
Context() context.T
|
||||
Disconnect()
|
||||
Publisher() *publish.S
|
||||
Publish(c context.T, evt *event.E) (err error)
|
||||
Relay() relay.I
|
||||
Shutdown()
|
||||
Storage() store.I
|
||||
// Options() *options.T
|
||||
// AcceptEvent(
|
||||
// c context.T, ev *event.E, hr *http.Request, origin string,
|
||||
// authedPubkey []byte) (accept bool, notice string, afterSave func())
|
||||
// AdminAuth(r *http.Request,
|
||||
// tolerance ...time.Duration) (authed bool, pubkey []byte)
|
||||
// AuthRequired() bool
|
||||
// Configuration() store.Configuration
|
||||
// Owners() [][]byte
|
||||
// PublicReadable() bool
|
||||
// SetConfiguration(*store.Configuration)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package realy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"orly.dev/app/realy/interfaces"
|
||||
"orly.dev/app/realy/publish"
|
||||
"orly.dev/encoders/event"
|
||||
"orly.dev/interfaces/relay"
|
||||
"orly.dev/interfaces/store"
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
func (s *Server) Storage() store.I { return s.relay.Storage() }
|
||||
|
||||
func (s *Server) Relay() relay.I { return s.relay }
|
||||
|
||||
func (s *Server) Disconnect() { s.disconnect() }
|
||||
|
||||
func (s *Server) AddEvent(
|
||||
c context.T, rl relay.I, ev *event.E, hr *http.Request, origin string,
|
||||
authedPubkey []byte,
|
||||
) (accepted bool, message []byte) {
|
||||
|
||||
return s.addEvent(c, rl, ev, hr, origin, authedPubkey)
|
||||
}
|
||||
|
||||
func (s *Server) Publisher() *publish.S { return s.listeners }
|
||||
|
||||
func (s *Server) Context() context.T { return s.Ctx }
|
||||
|
||||
var _ interfaces.Server = &Server{}
|
||||
@@ -1,154 +0,0 @@
|
||||
package realy
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"orly.dev/app/realy/helpers"
|
||||
"orly.dev/app/realy/options"
|
||||
"orly.dev/app/realy/publish"
|
||||
"orly.dev/interfaces/relay"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/log"
|
||||
realy_lol "orly.dev/version"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/fasthttp/websocket"
|
||||
"github.com/rs/cors"
|
||||
|
||||
"orly.dev/interfaces/signer"
|
||||
"orly.dev/protocol/openapi"
|
||||
"orly.dev/protocol/socketapi"
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Ctx context.T
|
||||
Cancel context.F
|
||||
options *options.T
|
||||
relay relay.I
|
||||
clientsMu sync.Mutex
|
||||
clients map[*websocket.Conn]struct{}
|
||||
Addr string
|
||||
mux *openapi.ServeMux
|
||||
httpServer *http.Server
|
||||
// authRequired bool
|
||||
// publicReadable bool
|
||||
// maxLimit int
|
||||
// admins []signer.I
|
||||
// owners [][]byte
|
||||
listeners *publish.S
|
||||
huma.API
|
||||
// ConfigurationMx sync.Mutex
|
||||
// configuration *store.Configuration
|
||||
}
|
||||
|
||||
type ServerParams struct {
|
||||
Ctx context.T
|
||||
Cancel context.F
|
||||
Rl relay.I
|
||||
DbPath string
|
||||
MaxLimit int
|
||||
Admins []signer.I
|
||||
Owners [][]byte
|
||||
PublicReadable bool
|
||||
}
|
||||
|
||||
func NewServer(sp *ServerParams, opts ...options.O) (s *Server, err error) {
|
||||
op := options.Default()
|
||||
for _, opt := range opts {
|
||||
opt(op)
|
||||
}
|
||||
if storage := sp.Rl.Storage(); storage != nil {
|
||||
if err = storage.Init(sp.DbPath); chk.T(err) {
|
||||
return nil, fmt.Errorf("storage init: %w", err)
|
||||
}
|
||||
}
|
||||
serveMux := openapi.NewServeMux()
|
||||
s = &Server{
|
||||
Ctx: sp.Ctx,
|
||||
Cancel: sp.Cancel,
|
||||
relay: sp.Rl,
|
||||
clients: make(map[*websocket.Conn]struct{}),
|
||||
mux: serveMux,
|
||||
options: op,
|
||||
listeners: publish.New(socketapi.New(), openapi.New()),
|
||||
API: openapi.NewHuma(
|
||||
serveMux, sp.Rl.Name(), realy_lol.V,
|
||||
realy_lol.Description,
|
||||
),
|
||||
}
|
||||
// register the http API operations
|
||||
huma.AutoRegister(s.API, openapi.NewOperations(s))
|
||||
go func() {
|
||||
if err := s.relay.Init(); chk.E(err) {
|
||||
s.Shutdown()
|
||||
}
|
||||
}()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// ServeHTTP implements the relay's http handler.
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// standard nostr protocol only governs the "root" path of the relay and
|
||||
// websockets
|
||||
if r.URL.Path == "/" && r.Header.Get("Accept") == "application/nostr+json" {
|
||||
s.handleRelayInfo(w, r)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/" && r.Header.Get("Upgrade") == "websocket" {
|
||||
s.handleWebsocket(w, r)
|
||||
return
|
||||
}
|
||||
log.I.F(
|
||||
"http request: %s from %s", r.URL.String(), helpers.GetRemoteFromReq(r),
|
||||
)
|
||||
s.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Start up the relay.
|
||||
func (s *Server) Start(host string, port int, started ...chan bool) error {
|
||||
addr := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
log.I.F("starting relay listener at %s", addr)
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.httpServer = &http.Server{
|
||||
Handler: cors.Default().Handler(s),
|
||||
Addr: addr,
|
||||
ReadHeaderTimeout: 7 * time.Second,
|
||||
IdleTimeout: 28 * time.Second,
|
||||
}
|
||||
for _, startedC := range started {
|
||||
close(startedC)
|
||||
}
|
||||
if err = s.httpServer.Serve(ln); errors.Is(err, http.ErrServerClosed) {
|
||||
} else if err != nil {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown the relay.
|
||||
func (s *Server) Shutdown() {
|
||||
log.I.Ln("shutting down relay")
|
||||
s.Cancel()
|
||||
log.W.Ln("closing event store")
|
||||
chk.E(s.relay.Storage().Close())
|
||||
log.W.Ln("shutting down relay listener")
|
||||
chk.E(s.httpServer.Shutdown(s.Ctx))
|
||||
if f, ok := s.relay.(relay.ShutdownAware); ok {
|
||||
f.OnShutdown(s.Ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Router returns the servemux that handles paths on the HTTP server of the
|
||||
// relay.
|
||||
func (s *Server) Router() *http.ServeMux {
|
||||
return s.mux.ServeMux
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"orly.dev/utils/log"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
func MonitorResources(c context.T) {
|
||||
tick := time.NewTicker(time.Minute * 15)
|
||||
log.I.Ln("running process", os.Args[0], os.Getpid())
|
||||
// memStats := &runtime.MemStats{}
|
||||
for {
|
||||
select {
|
||||
case <-c.Done():
|
||||
log.D.Ln("shutting down resource monitor")
|
||||
return
|
||||
case <-tick.C:
|
||||
// runtime.ReadMemStats(memStats)
|
||||
log.D.Ln(
|
||||
"# goroutines", runtime.NumGoroutine(), "# cgo calls",
|
||||
runtime.NumCgoCall(),
|
||||
)
|
||||
// log.D.S(memStats)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// Package cmd contains the executable applications of the realy suite.
|
||||
package cmd
|
||||
@@ -14,8 +14,14 @@ import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/log"
|
||||
"orly.dev/cmd/lerproxy/buf"
|
||||
"orly.dev/cmd/lerproxy/hsts"
|
||||
"orly.dev/cmd/lerproxy/reverse"
|
||||
"orly.dev/cmd/lerproxy/tcpkeepalive"
|
||||
"orly.dev/cmd/lerproxy/util"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
"orly.dev/pkg/utils/context"
|
||||
"orly.dev/pkg/utils/log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
@@ -27,13 +33,6 @@ import (
|
||||
"github.com/alexflint/go-arg"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"orly.dev/cmd/lerproxy/buf"
|
||||
"orly.dev/cmd/lerproxy/hsts"
|
||||
"orly.dev/cmd/lerproxy/reverse"
|
||||
"orly.dev/cmd/lerproxy/tcpkeepalive"
|
||||
"orly.dev/cmd/lerproxy/util"
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
type runArgs struct {
|
||||
|
||||
@@ -6,9 +6,8 @@ import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"orly.dev/utils/log"
|
||||
|
||||
"orly.dev/cmd/lerproxy/util"
|
||||
"orly.dev/pkg/utils/log"
|
||||
)
|
||||
|
||||
// NewSingleHostReverseProxy is a copy of httputil.NewSingleHostReverseProxy
|
||||
|
||||
@@ -4,10 +4,9 @@ package tcpkeepalive
|
||||
|
||||
import (
|
||||
"net"
|
||||
"orly.dev/utils/chk"
|
||||
"time"
|
||||
|
||||
"orly.dev/cmd/lerproxy/timeout"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Period can be changed prior to opening a Listener to alter its'
|
||||
|
||||
@@ -4,7 +4,7 @@ package timeout
|
||||
|
||||
import (
|
||||
"net"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
||||
@@ -3,16 +3,15 @@ package main
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"orly.dev/crypto/p256k"
|
||||
"orly.dev/encoders/bech32encoding"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/errorf"
|
||||
"orly.dev/utils/log"
|
||||
"orly.dev/pkg/crypto/p256k"
|
||||
"orly.dev/pkg/encoders/bech32encoding"
|
||||
"orly.dev/pkg/interfaces/signer"
|
||||
"orly.dev/pkg/protocol/httpauth"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
"orly.dev/pkg/utils/errorf"
|
||||
"orly.dev/pkg/utils/log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"orly.dev/interfaces/signer"
|
||||
"orly.dev/protocol/httpauth"
|
||||
)
|
||||
|
||||
const secEnv = "NOSTR_SECRET_KEY"
|
||||
|
||||
@@ -8,18 +8,17 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"orly.dev/crypto/p256k"
|
||||
"orly.dev/crypto/sha256"
|
||||
"orly.dev/encoders/bech32encoding"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/errorf"
|
||||
"orly.dev/utils/log"
|
||||
realy_lol "orly.dev/version"
|
||||
"orly.dev/pkg/crypto/p256k"
|
||||
"orly.dev/pkg/crypto/sha256"
|
||||
"orly.dev/pkg/encoders/bech32encoding"
|
||||
"orly.dev/pkg/encoders/hex"
|
||||
"orly.dev/pkg/interfaces/signer"
|
||||
"orly.dev/pkg/protocol/httpauth"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
"orly.dev/pkg/utils/errorf"
|
||||
"orly.dev/pkg/utils/log"
|
||||
realy_lol "orly.dev/pkg/version"
|
||||
"os"
|
||||
|
||||
"orly.dev/encoders/hex"
|
||||
"orly.dev/interfaces/signer"
|
||||
"orly.dev/protocol/httpauth"
|
||||
)
|
||||
|
||||
const secEnv = "NOSTR_SECRET_KEY"
|
||||
|
||||
@@ -6,13 +6,15 @@ import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"orly.dev/crypto/ec/bech32"
|
||||
"orly.dev/crypto/ec/schnorr"
|
||||
secp256k2 "orly.dev/crypto/ec/secp256k1"
|
||||
"orly.dev/encoders/bech32encoding"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/interrupt"
|
||||
"orly.dev/utils/log"
|
||||
"orly.dev/pkg/crypto/ec/bech32"
|
||||
"orly.dev/pkg/crypto/ec/schnorr"
|
||||
"orly.dev/pkg/crypto/ec/secp256k1"
|
||||
"orly.dev/pkg/encoders/bech32encoding"
|
||||
"orly.dev/pkg/utils/atomic"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
"orly.dev/pkg/utils/interrupt"
|
||||
"orly.dev/pkg/utils/log"
|
||||
"orly.dev/pkg/utils/qu"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
@@ -20,9 +22,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/alexflint/go-arg"
|
||||
|
||||
"orly.dev/utils/atomic"
|
||||
"orly.dev/utils/qu"
|
||||
)
|
||||
|
||||
var prefix = append(bech32encoding.PubHRP, '1')
|
||||
@@ -34,9 +33,9 @@ const (
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
sec *secp256k2.SecretKey
|
||||
sec *secp256k1.SecretKey
|
||||
npub []byte
|
||||
pub *secp256k2.PublicKey
|
||||
pub *secp256k1.PublicKey
|
||||
}
|
||||
|
||||
var args struct {
|
||||
@@ -219,11 +218,11 @@ out:
|
||||
// GenKeyPair creates a fresh new key pair using the entropy source used by
|
||||
// crypto/rand (ie, /dev/random on posix systems).
|
||||
func GenKeyPair() (
|
||||
sec *secp256k2.SecretKey,
|
||||
pub *secp256k2.PublicKey, err error,
|
||||
sec *secp256k1.SecretKey,
|
||||
pub *secp256k1.PublicKey, err error,
|
||||
) {
|
||||
|
||||
sec, err = secp256k2.GenerateSecretKey()
|
||||
sec, err = secp256k1.GenerateSecretKey()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error generating key: %s", err)
|
||||
return
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package base58_test
|
||||
|
||||
import (
|
||||
"orly.dev/utils/lol"
|
||||
)
|
||||
|
||||
var (
|
||||
log, chk, errorf = lol.Main.Log, lol.Main.Check, lol.Main.Errorf
|
||||
)
|
||||
59
docs/doc-comments-prompt.txt
Normal file
59
docs/doc-comments-prompt.txt
Normal file
@@ -0,0 +1,59 @@
|
||||
Always start documentation comments with the symbol name verbatim, and then use this to start a sentence summarizing the symbol's function
|
||||
|
||||
For documentation comments on functions and methods:
|
||||
|
||||
- Write a general description in one or two sentences at the top
|
||||
|
||||
- use the format `# Header` for headings of sections.
|
||||
|
||||
- Follow by a description of the parameters and then return values, with a series of bullet points describing each item, each with an empty line in between.
|
||||
|
||||
- Last, describe the expected behaviour of the function or method, keep this with one space apart from the comment start token
|
||||
|
||||
For documentation on types, variables and comments, write 1-2 sentences describing how the item is used.
|
||||
|
||||
For documentation on package, summarise in up to 3 sentences the functions and purpose of the package
|
||||
|
||||
Do not use markdown ** or __ or any similar things in initial words of a bullet point, instead use standard godoc style # prefix for header sections
|
||||
|
||||
ALWAYS separate each bullet point with an empty line, and ALWAYS indent them three spaces after the //
|
||||
|
||||
NEVER put a colon after the first word of the first line of a document comment
|
||||
|
||||
Use British English spelling and Oxford commas
|
||||
|
||||
Always break lines before 80 columns, and flow under bullet points two columns right of the bullet point hyphen.
|
||||
|
||||
Do not write a section for parameters or return values when there is none
|
||||
|
||||
In the `# Expected behavior` section always add an empty line after this title before the description, and don't indent this section as this makes it appear as preformatted monospace.
|
||||
|
||||
A good typical example:
|
||||
|
||||
// NewServer initializes and returns a new Server instance based on the provided
|
||||
// ServerParams and optional settings. It sets up storage, initializes the
|
||||
// relay, and configures necessary components for server operation.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - sp (*ServerParams): The configuration parameters for initializing the
|
||||
// server.
|
||||
//
|
||||
// - opts (...options.O): Optional settings that modify the server's behavior.
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - s (*Server): The newly created Server instance.
|
||||
//
|
||||
// - err (error): An error if any step fails during initialization.
|
||||
//
|
||||
// # Expected Behaviour
|
||||
//
|
||||
// - Initializes storage with the provided database path.
|
||||
//
|
||||
// - Configures the server's options using the default settings and applies any
|
||||
// optional settings provided.
|
||||
//
|
||||
// - Sets up a ServeMux for handling HTTP requests.
|
||||
//
|
||||
// - Initializes the relay, starting its operation in a separate goroutine.
|
||||
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
@@ -1,7 +0,0 @@
|
||||
package listener
|
||||
|
||||
type I interface {
|
||||
Write(p []byte) (n int, err error)
|
||||
Close() error
|
||||
Remote() string
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package publisher
|
||||
|
||||
import (
|
||||
"orly.dev/encoders/event"
|
||||
"orly.dev/interfaces/typer"
|
||||
)
|
||||
|
||||
type I interface {
|
||||
typer.T
|
||||
Deliver(ev *event.E)
|
||||
Receive(msg typer.T)
|
||||
}
|
||||
|
||||
type Publishers []I
|
||||
@@ -1,19 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"orly.dev/encoders/event"
|
||||
"orly.dev/interfaces/store"
|
||||
"orly.dev/utils/context"
|
||||
)
|
||||
|
||||
type I interface {
|
||||
Context() context.T
|
||||
HandleRelayInfo(
|
||||
w http.ResponseWriter, r *http.Request,
|
||||
)
|
||||
Storage() store.I
|
||||
AddEvent(
|
||||
c context.T, ev *event.E, hr *http.Request, remote string,
|
||||
) (accepted bool, message []byte)
|
||||
}
|
||||
37
main.go
37
main.go
@@ -5,26 +5,25 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pkg/profile"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"orly.dev/app/realy"
|
||||
"orly.dev/app/realy/options"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/interrupt"
|
||||
"orly.dev/utils/log"
|
||||
realy_lol "orly.dev/version"
|
||||
"os"
|
||||
|
||||
"orly.dev/app"
|
||||
"orly.dev/app/config"
|
||||
"orly.dev/database"
|
||||
"orly.dev/utils/context"
|
||||
"orly.dev/utils/lol"
|
||||
"github.com/pkg/profile"
|
||||
app2 "orly.dev/pkg/app"
|
||||
"orly.dev/pkg/app/config"
|
||||
"orly.dev/pkg/app/relay"
|
||||
"orly.dev/pkg/app/relay/options"
|
||||
"orly.dev/pkg/database"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
"orly.dev/pkg/utils/context"
|
||||
"orly.dev/pkg/utils/interrupt"
|
||||
"orly.dev/pkg/utils/log"
|
||||
"orly.dev/pkg/utils/lol"
|
||||
"orly.dev/pkg/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.I.F("starting realy %s", realy_lol.V)
|
||||
var err error
|
||||
var cfg *config.C
|
||||
if cfg, err = config.New(); chk.T(err) {
|
||||
@@ -34,6 +33,7 @@ func main() {
|
||||
config.PrintHelp(cfg, os.Stderr)
|
||||
os.Exit(0)
|
||||
}
|
||||
log.I.F("starting %s %s", cfg.AppName, version.V)
|
||||
if config.GetEnv() {
|
||||
config.PrintEnv(cfg, os.Stdout)
|
||||
os.Exit(0)
|
||||
@@ -55,18 +55,19 @@ func main() {
|
||||
if chk.E(err) {
|
||||
os.Exit(1)
|
||||
}
|
||||
r := &app.Relay{C: cfg, Store: storage}
|
||||
go app.MonitorResources(c)
|
||||
var server *realy.Server
|
||||
serverParams := &realy.ServerParams{
|
||||
r := &app2.Relay{C: cfg, Store: storage}
|
||||
go app2.MonitorResources(c)
|
||||
var server *relay.Server
|
||||
serverParams := &relay.ServerParams{
|
||||
Ctx: c,
|
||||
Cancel: cancel,
|
||||
Rl: r,
|
||||
DbPath: cfg.DataDir,
|
||||
MaxLimit: 512, // Default max limit for events
|
||||
C: cfg,
|
||||
}
|
||||
var opts []options.O
|
||||
if server, err = realy.NewServer(serverParams, opts...); chk.E(err) {
|
||||
if server, err = relay.NewServer(serverParams, opts...); chk.E(err) {
|
||||
os.Exit(1)
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
292
pkg/app/config/config.go
Normal file
292
pkg/app/config/config.go
Normal file
@@ -0,0 +1,292 @@
|
||||
// Package config provides a go-simpler.org/env configuration table and helpers
|
||||
// for working with the list of key/value lists stored in .env files.
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"orly.dev/pkg/utils/apputil"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
env2 "orly.dev/pkg/utils/env"
|
||||
"orly.dev/pkg/utils/log"
|
||||
"orly.dev/pkg/utils/lol"
|
||||
"orly.dev/pkg/version"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"go-simpler.org/env"
|
||||
)
|
||||
|
||||
// C holds application configuration settings loaded from environment variables
|
||||
// and default values. It defines parameters for app behaviour, storage
|
||||
// locations, logging, and network settings used across the relay service.
|
||||
type C struct {
|
||||
AppName string `env:"ORLY_APP_NAME" default:"orly"`
|
||||
Config string `env:"ORLY_CONFIG_DIR" usage:"location for configuration file, which has the name '.env' to make it harder to delete, and is a standard environment KEY=value<newline>... style" default:"~/.config/orly"`
|
||||
State string `env:"ORLY_STATE_DATA_DIR" usage:"storage location for state data affected by dynamic interactive interfaces" default:"~/.local/state/orly"`
|
||||
DataDir string `env:"ORLY_DATA_DIR" usage:"storage location for the event store" default:"~/.local/cache/orly"`
|
||||
Listen string `env:"ORLY_LISTEN" default:"0.0.0.0" usage:"network listen address"`
|
||||
Port int `env:"ORLY_PORT" default:"3334" usage:"port to listen on"`
|
||||
LogLevel string `env:"ORLY_LOG_LEVEL" default:"info" usage:"debug level: fatal error warn info debug trace"`
|
||||
DbLogLevel string `env:"ORLY_DB_LOG_LEVEL" default:"info" usage:"debug level: fatal error warn info debug trace"`
|
||||
Pprof bool `env:"ORLY_PPROF" default:"false" usage:"enable pprof on 127.0.0.1:6060"`
|
||||
AuthRequired bool `env:"ORLY_AUTH_REQUIRED" default:"false" usage:"require authentication for all requests"`
|
||||
PublicReadable bool `env:"ORLY_PUBLIC_READABLE" default:"true" usage:"allow public read access to regardless of whether the client is authed"`
|
||||
SpiderSeeds []string `env:"ORLY_SPIDER_SEEDS" usage:"seeds to use for the spider (relays that are looked up initially to find owner relay lists) (comma separated)" default:"wss://relay.nostr.band/,wss://relay.damus.io/,wss://nostr.wine/,wss://nostr.land/,wss://theforest.nostr1.com/"`
|
||||
Owners []string `env:"ORLY_OWNERS" usage:"list of users whose follow lists designate whitelisted users who can publish events, and who can read if public readable is false (comma separated)"`
|
||||
Private bool `env:"ORLY_PRIVATE" usage:"do not spider for user metadata because the relay is private and this would leak relay memberships" default:"false"`
|
||||
}
|
||||
|
||||
// New creates and initializes a new configuration object for the relay
|
||||
// application
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - cfg: A pointer to the initialized configuration struct containing default
|
||||
// or environment-provided values
|
||||
//
|
||||
// - err: An error object that is non-nil if any operation during
|
||||
// initialization fails
|
||||
//
|
||||
// # Expected Behaviour:
|
||||
//
|
||||
// Initializes a new configuration instance by loading environment variables and
|
||||
// checking for a .env file in the default configuration directory. Sets logging
|
||||
// levels based on configuration values and returns the populated configuration
|
||||
// or an error if any step fails
|
||||
func New() (cfg *C, err error) {
|
||||
cfg = &C{}
|
||||
if err = env.Load(cfg, &env.Options{SliceSep: ","}); chk.T(err) {
|
||||
return
|
||||
}
|
||||
if cfg.Config == "" || strings.Contains(cfg.State, "~") {
|
||||
cfg.Config = filepath.Join(xdg.ConfigHome, cfg.AppName)
|
||||
}
|
||||
if cfg.DataDir == "" || strings.Contains(cfg.State, "~") {
|
||||
cfg.DataDir = filepath.Join(xdg.DataHome, cfg.AppName)
|
||||
}
|
||||
if cfg.State == "" || strings.Contains(cfg.State, "~") {
|
||||
cfg.State = filepath.Join(xdg.StateHome, cfg.AppName)
|
||||
}
|
||||
envPath := filepath.Join(cfg.Config, ".env")
|
||||
if apputil.FileExists(envPath) {
|
||||
var e env2.Env
|
||||
if e, err = env2.GetEnv(envPath); chk.T(err) {
|
||||
return
|
||||
}
|
||||
if err = env.Load(
|
||||
cfg, &env.Options{SliceSep: ",", Source: e},
|
||||
); chk.E(err) {
|
||||
return
|
||||
}
|
||||
lol.SetLogLevel(cfg.LogLevel)
|
||||
log.I.F("loaded configuration from %s", envPath)
|
||||
}
|
||||
log.I.S(cfg)
|
||||
return
|
||||
}
|
||||
|
||||
// HelpRequested determines if the command line arguments indicate a request for help
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - help: A boolean value indicating true if a help flag was detected in the
|
||||
// command line arguments, false otherwise
|
||||
//
|
||||
// # Expected Behaviour
|
||||
//
|
||||
// The function checks the first command line argument for common help flags and
|
||||
// returns true if any of them are present. Returns false if no help flag is found
|
||||
func HelpRequested() (help bool) {
|
||||
if len(os.Args) > 1 {
|
||||
switch strings.ToLower(os.Args[1]) {
|
||||
case "help", "-h", "--h", "-help", "--help", "?":
|
||||
help = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetEnv checks if the first command line argument is "env" and returns
|
||||
// whether the environment configuration should be printed.
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - requested: A boolean indicating true if the 'env' argument was
|
||||
// provided, false otherwise.
|
||||
//
|
||||
// # Expected Behaviour
|
||||
//
|
||||
// The function returns true when the first command line argument is "env"
|
||||
// (case-insensitive), signalling that the environment configuration should be
|
||||
// printed. Otherwise, it returns false.
|
||||
func GetEnv() (requested bool) {
|
||||
if len(os.Args) > 1 {
|
||||
switch strings.ToLower(os.Args[1]) {
|
||||
case "env":
|
||||
requested = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// KV is a key/value pair.
|
||||
type KV struct{ Key, Value string }
|
||||
|
||||
// KVSlice is a sortable slice of key/value pairs, designed for managing
|
||||
// configuration data and enabling operations like merging and sorting based on
|
||||
// keys.
|
||||
type KVSlice []KV
|
||||
|
||||
func (kv KVSlice) Len() int { return len(kv) }
|
||||
func (kv KVSlice) Less(i, j int) bool { return kv[i].Key < kv[j].Key }
|
||||
func (kv KVSlice) Swap(i, j int) { kv[i], kv[j] = kv[j], kv[i] }
|
||||
|
||||
// Compose merges two KVSlice instances into a new slice where key-value pairs
|
||||
// from the second slice override any duplicate keys from the first slice.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - kv2: The second KVSlice whose entries will be merged with the receiver.
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - out: A new KVSlice containing all entries from both slices, with keys
|
||||
// from kv2 taking precedence over keys from the receiver.
|
||||
//
|
||||
// # Expected Behaviour
|
||||
//
|
||||
// The method returns a new KVSlice that combines the contents of the receiver
|
||||
// and kv2. If any key exists in both slices, the value from kv2 is used. The
|
||||
// resulting slice remains sorted by keys as per the KVSlice implementation.
|
||||
func (kv KVSlice) Compose(kv2 KVSlice) (out KVSlice) {
|
||||
// duplicate the initial KVSlice
|
||||
for _, p := range kv {
|
||||
out = append(out, p)
|
||||
}
|
||||
out:
|
||||
for i, p := range kv2 {
|
||||
for j, q := range out {
|
||||
// if the key is repeated, replace the value
|
||||
if p.Key == q.Key {
|
||||
out[j].Value = kv2[i].Value
|
||||
continue out
|
||||
}
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// EnvKV generates key/value pairs from a configuration object's struct tags
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - cfg: A configuration object whose struct fields are processed for env tags
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - m: A KVSlice containing key/value pairs derived from the config's env tags
|
||||
//
|
||||
// # Expected Behaviour
|
||||
//
|
||||
// Processes each field of the config object, extracting values tagged with
|
||||
// "env" and converting them to strings. Skips fields without an "env" tag.
|
||||
// Handles various value types including strings, integers, booleans, durations,
|
||||
// and string slices by joining elements with commas.
|
||||
func EnvKV(cfg any) (m KVSlice) {
|
||||
t := reflect.TypeOf(cfg)
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
k := t.Field(i).Tag.Get("env")
|
||||
v := reflect.ValueOf(cfg).Field(i).Interface()
|
||||
var val string
|
||||
switch v.(type) {
|
||||
case string:
|
||||
val = v.(string)
|
||||
case int, bool, time.Duration:
|
||||
val = fmt.Sprint(v)
|
||||
case []string:
|
||||
arr := v.([]string)
|
||||
if len(arr) > 0 {
|
||||
val = strings.Join(arr, ",")
|
||||
}
|
||||
}
|
||||
// this can happen with embedded structs
|
||||
if k == "" {
|
||||
continue
|
||||
}
|
||||
m = append(m, KV{k, val})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// PrintEnv outputs sorted environment key/value pairs from a configuration object
|
||||
// to the provided writer
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - cfg: Pointer to the configuration object containing env tags
|
||||
//
|
||||
// - printer: Destination for the output, typically an io.Writer implementation
|
||||
//
|
||||
// # Expected Behaviour
|
||||
//
|
||||
// Outputs each environment variable derived from the config's struct tags in
|
||||
// sorted order, formatted as "key=value\n" to the specified writer
|
||||
func PrintEnv(cfg *C, printer io.Writer) {
|
||||
kvs := EnvKV(*cfg)
|
||||
sort.Sort(kvs)
|
||||
for _, v := range kvs {
|
||||
_, _ = fmt.Fprintf(printer, "%s=%s\n", v.Key, v.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintHelp prints help information including application version, environment
|
||||
// variable configuration, and details about .env file handling to the provided
|
||||
// writer
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - cfg: Configuration object containing app name and config directory path
|
||||
//
|
||||
// - printer: Output destination for the help text
|
||||
//
|
||||
// # Expected Behaviour
|
||||
//
|
||||
// Prints application name and version followed by environment variable
|
||||
// configuration details, explains .env file behaviour including automatic
|
||||
// loading and custom path options, and displays current configuration values
|
||||
// using PrintEnv. Outputs all information to the specified writer
|
||||
func PrintHelp(cfg *C, printer io.Writer) {
|
||||
_, _ = fmt.Fprintf(
|
||||
printer,
|
||||
"%s %s\n\n", cfg.AppName, version.V,
|
||||
)
|
||||
_, _ = fmt.Fprintf(
|
||||
printer,
|
||||
"Environment variables that configure %s:\n\n", cfg.AppName,
|
||||
)
|
||||
env.Usage(cfg, printer, &env.Options{SliceSep: ","})
|
||||
_, _ = fmt.Fprintf(
|
||||
printer,
|
||||
"\nCLI parameter 'help' also prints this information\n"+
|
||||
"\n.env file found at the path %s will be automatically "+
|
||||
"loaded for configuration.\nset these two variables for a custom load path,"+
|
||||
" this file will be created on first startup.\nenvironment overrides it and "+
|
||||
"you can also edit the file to set configuration options\n\n"+
|
||||
"use the parameter 'env' to print out the current configuration to the terminal\n\n"+
|
||||
"set the environment using\n\n\t%s env > %s/.env\n",
|
||||
cfg.Config,
|
||||
os.Args[0],
|
||||
cfg.Config,
|
||||
)
|
||||
fmt.Fprintf(printer, "\ncurrent configuration:\n\n")
|
||||
PrintEnv(cfg, printer)
|
||||
fmt.Fprintln(printer)
|
||||
return
|
||||
}
|
||||
170
pkg/app/main.go
Normal file
170
pkg/app/main.go
Normal file
@@ -0,0 +1,170 @@
|
||||
// Package app implements the orly nostr relay.
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"orly.dev/pkg/app/config"
|
||||
"orly.dev/pkg/encoders/event"
|
||||
"orly.dev/pkg/encoders/filter"
|
||||
"orly.dev/pkg/encoders/filters"
|
||||
"orly.dev/pkg/interfaces/store"
|
||||
"orly.dev/pkg/utils/context"
|
||||
)
|
||||
|
||||
// List represents a set-like structure using a map with empty struct values.
|
||||
type List map[string]struct{}
|
||||
|
||||
// Relay is a struct that represents a relay for Nostr events. It contains a
|
||||
// configuration and a persistence layer for storing the events. The Relay
|
||||
// type implements various methods to handle event acceptance, filtering,
|
||||
// and storage.
|
||||
type Relay struct {
|
||||
sync.Mutex
|
||||
*config.C
|
||||
Store store.I
|
||||
}
|
||||
|
||||
// Name returns the name of the application represented by this relay.
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - string: the name of the application.
|
||||
//
|
||||
// # Expected behaviour
|
||||
//
|
||||
// This function simply returns the AppName field from the configuration.
|
||||
func (r *Relay) Name() string { return r.C.AppName }
|
||||
|
||||
// Storage represents a persistence layer for Nostr events handled by a relay.
|
||||
func (r *Relay) Storage() store.I { return r.Store }
|
||||
|
||||
// Init initializes and sets up the relay for Nostr events.
|
||||
//
|
||||
// #Return Values
|
||||
//
|
||||
// - err: an error if any issues occurred during initialization.
|
||||
//
|
||||
// #Expected behaviour
|
||||
//
|
||||
// This function is responsible for setting up the relay, configuring it,
|
||||
// and initializing the necessary components to handle Nostr events.
|
||||
func (r *Relay) Init() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AcceptEvent checks an event and determines whether the event should be
|
||||
// accepted and if the client has the authority to submit it.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - c - a context.T for signalling if the task has been canceled.
|
||||
//
|
||||
// - evt - an *event.E that is being evaluated.
|
||||
//
|
||||
// - hr - an *http.Request containing the information about the current
|
||||
// connection.
|
||||
//
|
||||
// - origin - the address of the client.
|
||||
//
|
||||
// - authedPubkey - the public key, if authed, of the client for this
|
||||
// connection.
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - accept - returns true if the event is accepted.
|
||||
//
|
||||
// - notice - if it is not accepted, a message in the form of
|
||||
// `machine-readable-prefix: reason for error/blocked/rate-limited/etc`
|
||||
//
|
||||
// - afterSave - a closure to run after the event has been stored.
|
||||
//
|
||||
// # Expected behaviour
|
||||
//
|
||||
// This function checks whether the client has permission to store the event,
|
||||
// and if they don't, returns false and some kind of error message. If they do,
|
||||
// the event is forwarded to the database to be stored and indexed.
|
||||
func (r *Relay) AcceptEvent(
|
||||
c context.T, evt *event.E, hr *http.Request,
|
||||
origin string, authedPubkey []byte,
|
||||
) (accept bool, notice string, afterSave func()) {
|
||||
accept = true
|
||||
return
|
||||
}
|
||||
|
||||
// AcceptFilter checks if a filter is allowed based on authentication status and
|
||||
// relay policies
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - c: Context for task cancellation.
|
||||
//
|
||||
// - hr: HTTP request containing connection information.
|
||||
//
|
||||
// - f: Filter to evaluate for acceptance.
|
||||
//
|
||||
// - authedPubkey: Public key of authenticated client, if applicable.
|
||||
//
|
||||
// # Return values
|
||||
//
|
||||
// - allowed: The filter if permitted; may be modified during processing.
|
||||
//
|
||||
// - ok: Boolean indicating whether the filter is accepted.
|
||||
//
|
||||
// - modified: Boolean indicating whether the filter was altered during
|
||||
// evaluation.
|
||||
//
|
||||
// # Expected behaviour
|
||||
//
|
||||
// The method evaluates whether the provided filter should be allowed based on
|
||||
// authentication status and relay-specific rules. If permitted, returns the
|
||||
// filter (possibly modified) and true for ok; otherwise returns nil or false
|
||||
// for ok accordingly.
|
||||
func (r *Relay) AcceptFilter(
|
||||
c context.T, hr *http.Request, f *filter.S,
|
||||
authedPubkey []byte,
|
||||
) (allowed *filter.S, ok bool, modified bool) {
|
||||
allowed = f
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
// AcceptReq evaluates whether the provided filters are allowed based on
|
||||
// authentication status and relay policies for an incoming HTTP request.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - c: Context for task cancellation.
|
||||
//
|
||||
// - hr: HTTP request containing connection information.
|
||||
//
|
||||
// - id: Identifier associated with the request.
|
||||
//
|
||||
// - ff: Filters to evaluate for acceptance.
|
||||
//
|
||||
// - authedPubkey: Public key of authenticated client, if applicable.
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - allowed: The filters if permitted; may be modified during processing.
|
||||
//
|
||||
// - ok: Boolean indicating whether the filters are accepted.
|
||||
//
|
||||
// - modified: Boolean indicating whether the filters were altered during
|
||||
// evaluation.
|
||||
//
|
||||
// # Expected Behaviour:
|
||||
//
|
||||
// The method evaluates whether the provided filters should be allowed based on
|
||||
// authentication status and relay-specific rules. If permitted, returns the
|
||||
// filters (possibly modified) and true for ok; otherwise returns nil or false
|
||||
// for ok accordingly.
|
||||
func (r *Relay) AcceptReq(
|
||||
c context.T, hr *http.Request, id []byte,
|
||||
ff *filters.T, authedPubkey []byte,
|
||||
) (allowed *filters.T, ok bool, modified bool) {
|
||||
allowed = ff
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
58
pkg/app/relay/accept-event.go
Normal file
58
pkg/app/relay/accept-event.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
|
||||
"orly.dev/pkg/encoders/event"
|
||||
"orly.dev/pkg/utils/context"
|
||||
)
|
||||
|
||||
// AcceptEvent determines whether an incoming event should be accepted for
|
||||
// processing based on authentication requirements.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - c: the context of the request
|
||||
//
|
||||
// - ev: pointer to the event structure
|
||||
//
|
||||
// - hr: HTTP request related to the event (if any)
|
||||
//
|
||||
// - authedPubkey: public key of the authenticated user (if any)
|
||||
//
|
||||
// - remote: remote address from where the event was received
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - accept: boolean indicating whether the event should be accepted
|
||||
//
|
||||
// - notice: string providing a message or error notice
|
||||
//
|
||||
// - afterSave: function to execute after saving the event (if applicable)
|
||||
//
|
||||
// # Expected Behaviour:
|
||||
//
|
||||
// - If authentication is required and no public key is provided, reject the
|
||||
// event.
|
||||
//
|
||||
// - Otherwise, accept the event for processing.
|
||||
func (s *Server) AcceptEvent(
|
||||
c context.T, ev *event.E, hr *http.Request, authedPubkey []byte,
|
||||
remote string,
|
||||
) (accept bool, notice string, afterSave func()) {
|
||||
// if auth is required and the user is not authed, reject
|
||||
if s.AuthRequired() && len(authedPubkey) == 0 {
|
||||
return
|
||||
}
|
||||
// check if the authed user is on the lists
|
||||
list := append(s.OwnersFollowed(), s.FollowedFollows()...)
|
||||
for _, u := range list {
|
||||
if bytes.Equal(u, authedPubkey) {
|
||||
accept = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// todo: check if event author is on owners' mute lists or block list
|
||||
return
|
||||
}
|
||||
237
pkg/app/relay/accept-event_test.go
Normal file
237
pkg/app/relay/accept-event_test.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"orly.dev/pkg/app/config"
|
||||
"orly.dev/pkg/encoders/event"
|
||||
"orly.dev/pkg/utils/context"
|
||||
)
|
||||
|
||||
// mockServerForEvent is a simple mock implementation of the Server struct for testing AcceptEvent
|
||||
type mockServerForEvent struct {
|
||||
authRequired bool
|
||||
ownersFollowed [][]byte
|
||||
followedFollows [][]byte
|
||||
}
|
||||
|
||||
func (m *mockServerForEvent) AuthRequired() bool {
|
||||
return m.authRequired
|
||||
}
|
||||
|
||||
func (m *mockServerForEvent) OwnersFollowed() [][]byte {
|
||||
return m.ownersFollowed
|
||||
}
|
||||
|
||||
func (m *mockServerForEvent) FollowedFollows() [][]byte {
|
||||
return m.followedFollows
|
||||
}
|
||||
|
||||
// AcceptEvent implements the Server.AcceptEvent method for testing
|
||||
func (m *mockServerForEvent) AcceptEvent(
|
||||
c context.T, ev *event.E, hr *http.Request, authedPubkey []byte,
|
||||
remote string,
|
||||
) (accept bool, notice string, afterSave func()) {
|
||||
// if auth is required and the user is not authed, reject
|
||||
if m.AuthRequired() && len(authedPubkey) == 0 {
|
||||
return
|
||||
}
|
||||
// check if the authed user is on the lists
|
||||
list := append(m.OwnersFollowed(), m.FollowedFollows()...)
|
||||
for _, u := range list {
|
||||
if bytes.Equal(u, authedPubkey) {
|
||||
accept = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestAcceptEvent(t *testing.T) {
|
||||
// Create a context and HTTP request for testing
|
||||
ctx := context.Bg()
|
||||
req, _ := http.NewRequest("GET", "http://example.com", nil)
|
||||
|
||||
// Create a test event
|
||||
testEvent := &event.E{}
|
||||
|
||||
// Test cases
|
||||
tests := []struct {
|
||||
name string
|
||||
server *mockServerForEvent
|
||||
authedPubkey []byte
|
||||
expectedAccept bool
|
||||
}{
|
||||
{
|
||||
name: "Auth required, no pubkey",
|
||||
server: &mockServerForEvent{
|
||||
authRequired: true,
|
||||
},
|
||||
authedPubkey: nil,
|
||||
expectedAccept: false,
|
||||
},
|
||||
{
|
||||
name: "Auth required, with pubkey, not on lists",
|
||||
server: &mockServerForEvent{
|
||||
authRequired: true,
|
||||
ownersFollowed: [][]byte{
|
||||
[]byte("followed1"),
|
||||
[]byte("followed2"),
|
||||
},
|
||||
followedFollows: [][]byte{
|
||||
[]byte("follow1"),
|
||||
[]byte("follow2"),
|
||||
},
|
||||
},
|
||||
authedPubkey: []byte("test-pubkey"),
|
||||
expectedAccept: false,
|
||||
},
|
||||
{
|
||||
name: "Auth required, with pubkey, on owners followed list",
|
||||
server: &mockServerForEvent{
|
||||
authRequired: true,
|
||||
ownersFollowed: [][]byte{
|
||||
[]byte("followed1"),
|
||||
[]byte("test-pubkey"),
|
||||
[]byte("followed2"),
|
||||
},
|
||||
followedFollows: [][]byte{
|
||||
[]byte("follow1"),
|
||||
[]byte("follow2"),
|
||||
},
|
||||
},
|
||||
authedPubkey: []byte("test-pubkey"),
|
||||
expectedAccept: true,
|
||||
},
|
||||
{
|
||||
name: "Auth required, with pubkey, on followed follows list",
|
||||
server: &mockServerForEvent{
|
||||
authRequired: true,
|
||||
ownersFollowed: [][]byte{
|
||||
[]byte("followed1"),
|
||||
[]byte("followed2"),
|
||||
},
|
||||
followedFollows: [][]byte{
|
||||
[]byte("follow1"),
|
||||
[]byte("test-pubkey"),
|
||||
[]byte("follow2"),
|
||||
},
|
||||
},
|
||||
authedPubkey: []byte("test-pubkey"),
|
||||
expectedAccept: true,
|
||||
},
|
||||
{
|
||||
name: "Auth not required, no pubkey, not on lists",
|
||||
server: &mockServerForEvent{
|
||||
authRequired: false,
|
||||
ownersFollowed: [][]byte{
|
||||
[]byte("followed1"),
|
||||
[]byte("followed2"),
|
||||
},
|
||||
followedFollows: [][]byte{
|
||||
[]byte("follow1"),
|
||||
[]byte("follow2"),
|
||||
},
|
||||
},
|
||||
authedPubkey: nil,
|
||||
expectedAccept: false,
|
||||
},
|
||||
{
|
||||
name: "Auth not required, with pubkey, on lists",
|
||||
server: &mockServerForEvent{
|
||||
authRequired: false,
|
||||
ownersFollowed: [][]byte{
|
||||
[]byte("followed1"),
|
||||
[]byte("test-pubkey"),
|
||||
[]byte("followed2"),
|
||||
},
|
||||
followedFollows: [][]byte{
|
||||
[]byte("follow1"),
|
||||
[]byte("follow2"),
|
||||
},
|
||||
},
|
||||
authedPubkey: []byte("test-pubkey"),
|
||||
expectedAccept: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Run tests
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Use the mock server's AcceptEvent method
|
||||
accept, notice, afterSave := tt.server.AcceptEvent(ctx, testEvent, req, tt.authedPubkey, "127.0.0.1")
|
||||
|
||||
// Check if the acceptance status matches the expected value
|
||||
if accept != tt.expectedAccept {
|
||||
t.Errorf("AcceptEvent() accept = %v, want %v", accept, tt.expectedAccept)
|
||||
}
|
||||
|
||||
// Notice should be empty in the current implementation
|
||||
if notice != "" {
|
||||
t.Errorf("AcceptEvent() notice = %v, want empty string", notice)
|
||||
}
|
||||
|
||||
// afterSave should be nil in the current implementation
|
||||
if afterSave != nil {
|
||||
t.Error("AcceptEvent() afterSave is not nil, but should be nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAcceptEventWithRealServer tests the AcceptEvent function with a real Server instance
|
||||
func TestAcceptEventWithRealServer(t *testing.T) {
|
||||
// Create a context and HTTP request for testing
|
||||
ctx := context.Bg()
|
||||
req, _ := http.NewRequest("GET", "http://example.com", nil)
|
||||
|
||||
// Create a test event
|
||||
testEvent := &event.E{}
|
||||
|
||||
// Create a Server instance with configuration
|
||||
s := &Server{
|
||||
C: &config.C{
|
||||
AuthRequired: true,
|
||||
},
|
||||
Lists: new(Lists),
|
||||
}
|
||||
|
||||
// Test with no authenticated pubkey
|
||||
accept, notice, afterSave := s.AcceptEvent(ctx, testEvent, req, nil, "127.0.0.1")
|
||||
if accept {
|
||||
t.Error("AcceptEvent() accept = true, want false")
|
||||
}
|
||||
if notice != "" {
|
||||
t.Errorf("AcceptEvent() notice = %v, want empty string", notice)
|
||||
}
|
||||
if afterSave != nil {
|
||||
t.Error("AcceptEvent() afterSave is not nil, but should be nil")
|
||||
}
|
||||
|
||||
// Test with authenticated pubkey but not on any list
|
||||
accept, notice, afterSave = s.AcceptEvent(ctx, testEvent, req, []byte("test-pubkey"), "127.0.0.1")
|
||||
if accept {
|
||||
t.Error("AcceptEvent() accept = true, want false")
|
||||
}
|
||||
|
||||
// Add the pubkey to the owners followed list
|
||||
s.SetOwnersFollowed([][]byte{[]byte("test-pubkey")})
|
||||
|
||||
// Test with authenticated pubkey on the owners followed list
|
||||
accept, notice, afterSave = s.AcceptEvent(ctx, testEvent, req, []byte("test-pubkey"), "127.0.0.1")
|
||||
if !accept {
|
||||
t.Error("AcceptEvent() accept = false, want true")
|
||||
}
|
||||
|
||||
// Clear the owners followed list and add the pubkey to the followed follows list
|
||||
s.SetOwnersFollowed(nil)
|
||||
s.SetFollowedFollows([][]byte{[]byte("test-pubkey")})
|
||||
|
||||
// Test with authenticated pubkey on the followed follows list
|
||||
accept, notice, afterSave = s.AcceptEvent(ctx, testEvent, req, []byte("test-pubkey"), "127.0.0.1")
|
||||
if !accept {
|
||||
t.Error("AcceptEvent() accept = false, want true")
|
||||
}
|
||||
}
|
||||
50
pkg/app/relay/accept-req.go
Normal file
50
pkg/app/relay/accept-req.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"orly.dev/pkg/encoders/filters"
|
||||
"orly.dev/pkg/utils/context"
|
||||
)
|
||||
|
||||
// AcceptReq determines whether a request should be accepted based on
|
||||
// authentication and public readability settings.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - c: context for the request handling
|
||||
//
|
||||
// - hr: HTTP request received
|
||||
//
|
||||
// - f: filters to apply
|
||||
//
|
||||
// - authedPubkey: authenticated public key (if any)
|
||||
//
|
||||
// - remote: remote address of the request
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - allowed: filters that are allowed after processing
|
||||
//
|
||||
// - accept: boolean indicating whether the request should be accepted
|
||||
//
|
||||
// - modified: boolean indicating if the request has been modified during
|
||||
// processing
|
||||
//
|
||||
// # Expected Behaviour:
|
||||
//
|
||||
// - If authentication is required and there's no authenticated public key,
|
||||
// reject the request.
|
||||
//
|
||||
// - Otherwise, accept the request.
|
||||
func (s *Server) AcceptReq(
|
||||
c context.T, hr *http.Request, ff *filters.T,
|
||||
authedPubkey []byte, remote string,
|
||||
) (allowed *filters.T, accept bool, modified bool) {
|
||||
// if auth is required, and not public readable, reject
|
||||
if s.AuthRequired() && len(authedPubkey) == 0 && !s.PublicReadable() {
|
||||
return
|
||||
}
|
||||
allowed = ff
|
||||
accept = true
|
||||
return
|
||||
}
|
||||
210
pkg/app/relay/accept-req_test.go
Normal file
210
pkg/app/relay/accept-req_test.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"orly.dev/pkg/app/config"
|
||||
"orly.dev/pkg/encoders/filters"
|
||||
"orly.dev/pkg/utils/context"
|
||||
)
|
||||
|
||||
// mockServer is a simple mock implementation of the Server struct for testing
|
||||
type mockServer struct {
|
||||
authRequired bool
|
||||
publicReadable bool
|
||||
ownersPubkeys [][]byte
|
||||
}
|
||||
|
||||
func (m *mockServer) AuthRequired() bool {
|
||||
return m.authRequired || m.LenOwnersPubkeys() > 0
|
||||
}
|
||||
|
||||
func (m *mockServer) PublicReadable() bool {
|
||||
return m.publicReadable
|
||||
}
|
||||
|
||||
func (m *mockServer) LenOwnersPubkeys() int {
|
||||
return len(m.ownersPubkeys)
|
||||
}
|
||||
|
||||
func (m *mockServer) OwnersFollowed() [][]byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockServer) FollowedFollows() [][]byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AcceptReq implements the Server.AcceptReq method for testing
|
||||
func (m *mockServer) AcceptReq(
|
||||
c context.T, hr *http.Request, ff *filters.T,
|
||||
authedPubkey []byte, remote string,
|
||||
) (allowed *filters.T, accept bool, modified bool) {
|
||||
// if auth is required, and not public readable, reject
|
||||
if m.AuthRequired() && len(authedPubkey) == 0 && !m.PublicReadable() {
|
||||
return
|
||||
}
|
||||
allowed = ff
|
||||
accept = true
|
||||
return
|
||||
}
|
||||
|
||||
func TestAcceptReq(t *testing.T) {
|
||||
// Create a context and HTTP request for testing
|
||||
ctx := context.Bg()
|
||||
req, _ := http.NewRequest("GET", "http://example.com", nil)
|
||||
|
||||
// Create test filters
|
||||
testFilters := filters.New()
|
||||
|
||||
// Test cases
|
||||
tests := []struct {
|
||||
name string
|
||||
server *mockServer
|
||||
authedPubkey []byte
|
||||
expectedAccept bool
|
||||
}{
|
||||
{
|
||||
name: "Auth required, no pubkey, not public readable",
|
||||
server: &mockServer{
|
||||
authRequired: true,
|
||||
publicReadable: false,
|
||||
},
|
||||
authedPubkey: nil,
|
||||
expectedAccept: false,
|
||||
},
|
||||
{
|
||||
name: "Auth required, no pubkey, public readable",
|
||||
server: &mockServer{
|
||||
authRequired: true,
|
||||
publicReadable: true,
|
||||
},
|
||||
authedPubkey: nil,
|
||||
expectedAccept: true,
|
||||
},
|
||||
{
|
||||
name: "Auth required, with pubkey",
|
||||
server: &mockServer{
|
||||
authRequired: true,
|
||||
publicReadable: false,
|
||||
},
|
||||
authedPubkey: []byte("test-pubkey"),
|
||||
expectedAccept: true,
|
||||
},
|
||||
{
|
||||
name: "Auth not required",
|
||||
server: &mockServer{
|
||||
authRequired: false,
|
||||
publicReadable: false,
|
||||
},
|
||||
authedPubkey: nil,
|
||||
expectedAccept: true,
|
||||
},
|
||||
{
|
||||
name: "Auth required due to owner pubkeys, no pubkey, not public readable",
|
||||
server: &mockServer{
|
||||
authRequired: false,
|
||||
publicReadable: false,
|
||||
ownersPubkeys: [][]byte{[]byte("owner1")},
|
||||
},
|
||||
authedPubkey: nil,
|
||||
expectedAccept: false,
|
||||
},
|
||||
{
|
||||
name: "Auth required due to owner pubkeys, no pubkey, public readable",
|
||||
server: &mockServer{
|
||||
authRequired: false,
|
||||
publicReadable: true,
|
||||
ownersPubkeys: [][]byte{[]byte("owner1")},
|
||||
},
|
||||
authedPubkey: nil,
|
||||
expectedAccept: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Run tests
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Use the mock server's AcceptReq method
|
||||
allowed, accept, modified := tt.server.AcceptReq(ctx, req, testFilters, tt.authedPubkey, "127.0.0.1")
|
||||
|
||||
// Check if the acceptance status matches the expected value
|
||||
if accept != tt.expectedAccept {
|
||||
t.Errorf("AcceptReq() accept = %v, want %v", accept, tt.expectedAccept)
|
||||
}
|
||||
|
||||
// If the request should be accepted, check that the filters are returned
|
||||
if tt.expectedAccept {
|
||||
if allowed == nil {
|
||||
t.Error("AcceptReq() allowed is nil, but request was accepted")
|
||||
}
|
||||
} else {
|
||||
if allowed != nil {
|
||||
t.Error("AcceptReq() allowed is not nil, but request was rejected")
|
||||
}
|
||||
}
|
||||
|
||||
// Modified should be false as the current implementation doesn't modify filters
|
||||
if modified {
|
||||
t.Error("AcceptReq() modified = true, want false")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAcceptReqWithRealServer tests the AcceptReq function with a real Server instance
|
||||
func TestAcceptReqWithRealServer(t *testing.T) {
|
||||
// Create a context and HTTP request for testing
|
||||
ctx := context.Bg()
|
||||
req, _ := http.NewRequest("GET", "http://example.com", nil)
|
||||
|
||||
// Create test filters
|
||||
testFilters := filters.New()
|
||||
|
||||
// Create a Server instance with configuration
|
||||
s := &Server{
|
||||
C: &config.C{
|
||||
AuthRequired: true,
|
||||
PublicReadable: false,
|
||||
},
|
||||
Lists: new(Lists),
|
||||
}
|
||||
|
||||
// Test with no authenticated pubkey
|
||||
allowed, accept, modified := s.AcceptReq(ctx, req, testFilters, nil, "127.0.0.1")
|
||||
if accept {
|
||||
t.Error("AcceptReq() accept = true, want false")
|
||||
}
|
||||
if allowed != nil {
|
||||
t.Error("AcceptReq() allowed is not nil, but request was rejected")
|
||||
}
|
||||
if modified {
|
||||
t.Error("AcceptReq() modified = true, want false")
|
||||
}
|
||||
|
||||
// Test with authenticated pubkey
|
||||
allowed, accept, modified = s.AcceptReq(ctx, req, testFilters, []byte("test-pubkey"), "127.0.0.1")
|
||||
if !accept {
|
||||
t.Error("AcceptReq() accept = false, want true")
|
||||
}
|
||||
if allowed != testFilters {
|
||||
t.Error("AcceptReq() allowed is not the same as input filters")
|
||||
}
|
||||
if modified {
|
||||
t.Error("AcceptReq() modified = true, want false")
|
||||
}
|
||||
|
||||
// Test with public readable
|
||||
s.C.PublicReadable = true
|
||||
allowed, accept, modified = s.AcceptReq(ctx, req, testFilters, nil, "127.0.0.1")
|
||||
if !accept {
|
||||
t.Error("AcceptReq() accept = false, want true")
|
||||
}
|
||||
if allowed != testFilters {
|
||||
t.Error("AcceptReq() allowed is not the same as input filters")
|
||||
}
|
||||
if modified {
|
||||
t.Error("AcceptReq() modified = true, want false")
|
||||
}
|
||||
}
|
||||
85
pkg/app/relay/addEvent.go
Normal file
85
pkg/app/relay/addEvent.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"orly.dev/pkg/encoders/event"
|
||||
"orly.dev/pkg/interfaces/relay"
|
||||
"orly.dev/pkg/interfaces/store"
|
||||
"orly.dev/pkg/protocol/socketapi"
|
||||
"orly.dev/pkg/utils/context"
|
||||
"orly.dev/pkg/utils/normalize"
|
||||
)
|
||||
|
||||
// AddEvent processes an incoming event, saves it if valid, and delivers it to
|
||||
// subscribers.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - c: context for request handling
|
||||
//
|
||||
// - rl: relay interface
|
||||
//
|
||||
// - ev: the event to be added
|
||||
//
|
||||
// - hr: HTTP request related to the event (if any)
|
||||
//
|
||||
// - origin: origin of the event (if any)
|
||||
//
|
||||
// - authedPubkey: public key of the authenticated user (if any)
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - accepted: true if the event was successfully processed, false otherwise
|
||||
//
|
||||
// - message: additional information or error message related to the
|
||||
// processing
|
||||
//
|
||||
// # Expected Behaviour:
|
||||
//
|
||||
// - Validates the incoming event.
|
||||
//
|
||||
// - Saves the event using the Publish method if it is not ephemeral.
|
||||
//
|
||||
// - Handles duplicate events by returning an appropriate error message.
|
||||
//
|
||||
// - Delivers the event to subscribers via the listeners' Deliver method.
|
||||
//
|
||||
// - Returns a boolean indicating whether the event was accepted and any
|
||||
// relevant message.
|
||||
func (s *Server) AddEvent(
|
||||
c context.T, rl relay.I, ev *event.E,
|
||||
hr *http.Request, origin string,
|
||||
authedPubkey []byte,
|
||||
) (accepted bool, message []byte) {
|
||||
|
||||
if ev == nil {
|
||||
return false, normalize.Invalid.F("empty event")
|
||||
}
|
||||
if ev.Kind.IsEphemeral() {
|
||||
} else {
|
||||
if saveErr := s.Publish(c, ev); saveErr != nil {
|
||||
if errors.Is(saveErr, store.ErrDupEvent) {
|
||||
return false, []byte(saveErr.Error())
|
||||
}
|
||||
errmsg := saveErr.Error()
|
||||
if socketapi.NIP20prefixmatcher.MatchString(errmsg) {
|
||||
if strings.Contains(errmsg, "tombstone") {
|
||||
return false, normalize.Error.F("event was deleted, not storing it again")
|
||||
}
|
||||
if strings.HasPrefix(errmsg, string(normalize.Blocked)) {
|
||||
return false, []byte(errmsg)
|
||||
}
|
||||
return false, []byte(errmsg)
|
||||
} else {
|
||||
return false, []byte(errmsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
// notify subscribers
|
||||
s.listeners.Deliver(ev)
|
||||
accepted = true
|
||||
return
|
||||
}
|
||||
71
pkg/app/relay/auth.go
Normal file
71
pkg/app/relay/auth.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"orly.dev/pkg/utils/chk"
|
||||
"orly.dev/pkg/utils/log"
|
||||
"orly.dev/pkg/utils/lol"
|
||||
)
|
||||
|
||||
// ServiceURL constructs the service URL based on the incoming HTTP request. It
|
||||
// checks for authentication requirements and determines the protocol (ws or
|
||||
// wss) based on headers like X-Forwarded-Host, X-Forwarded-Proto, and the host
|
||||
// itself.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - req: A pointer to an http.Request object representing the incoming request.
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - st: A string representing the constructed service URL.
|
||||
//
|
||||
// # Expected Behaviour:
|
||||
//
|
||||
// - Checks if authentication is required.
|
||||
//
|
||||
// - Retrieves the host from X-Forwarded-Host or falls back to req.Host.
|
||||
//
|
||||
// - Determines the protocol (ws or wss) based on various conditions including
|
||||
// headers and host details.
|
||||
//
|
||||
// - Returns the constructed URL string.
|
||||
func (s *Server) ServiceURL(req *http.Request) (st string) {
|
||||
lol.Tracer("ServiceURL")
|
||||
defer func() { lol.Tracer("end ServiceURL", st) }()
|
||||
if !s.AuthRequired() {
|
||||
log.T.F("auth not required")
|
||||
return
|
||||
}
|
||||
host := req.Header.Get("X-Forwarded-Host")
|
||||
if host == "" {
|
||||
host = req.Host
|
||||
}
|
||||
proto := req.Header.Get("X-Forwarded-Proto")
|
||||
if proto == "" {
|
||||
if host == "localhost" {
|
||||
proto = "ws"
|
||||
} else if strings.Contains(host, ":") {
|
||||
// has a port number
|
||||
proto = "ws"
|
||||
} else if _, err := strconv.Atoi(
|
||||
strings.ReplaceAll(
|
||||
host, ".",
|
||||
"",
|
||||
),
|
||||
); chk.E(err) {
|
||||
// it's a naked IP
|
||||
proto = "ws"
|
||||
} else {
|
||||
proto = "wss"
|
||||
}
|
||||
} else if proto == "https" {
|
||||
proto = "wss"
|
||||
} else if proto == "http" {
|
||||
proto = "ws"
|
||||
}
|
||||
return proto + "://" + host
|
||||
}
|
||||
65
pkg/app/relay/handleRelayinfo.go
Normal file
65
pkg/app/relay/handleRelayinfo.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"orly.dev/pkg/interfaces/relay"
|
||||
"orly.dev/pkg/protocol/relayinfo"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
"orly.dev/pkg/utils/log"
|
||||
"orly.dev/pkg/version"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// HandleRelayInfo generates and returns a relay information document in JSON
|
||||
// format based on the server's configuration and supported NIPs.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - w: HTTP response writer used to send the generated document.
|
||||
//
|
||||
// - r: HTTP request object containing incoming client request data.
|
||||
//
|
||||
// # Expected Behaviour
|
||||
//
|
||||
// The function constructs a relay information document using either the
|
||||
// Informer interface implementation or predefined server configuration. It
|
||||
// returns this document as a JSON response to the client.
|
||||
func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) {
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
log.I.Ln("handling relay information document")
|
||||
var info *relayinfo.T
|
||||
if informationer, ok := s.relay.(relay.Informer); ok {
|
||||
info = informationer.GetNIP11InformationDocument()
|
||||
} else {
|
||||
supportedNIPs := relayinfo.GetList(
|
||||
relayinfo.BasicProtocol,
|
||||
relayinfo.Authentication,
|
||||
// relayinfo.EncryptedDirectMessage,
|
||||
relayinfo.EventDeletion,
|
||||
relayinfo.RelayInformationDocument,
|
||||
relayinfo.GenericTagQueries,
|
||||
// relayinfo.NostrMarketplace,
|
||||
relayinfo.EventTreatment,
|
||||
// relayinfo.CommandResults,
|
||||
relayinfo.ParameterizedReplaceableEvents,
|
||||
// relayinfo.ExpirationTimestamp,
|
||||
// relayinfo.ProtectedEvents,
|
||||
// relayinfo.RelayListMetadata,
|
||||
)
|
||||
sort.Sort(supportedNIPs)
|
||||
log.T.Ln("supported NIPs", supportedNIPs)
|
||||
info = &relayinfo.T{
|
||||
Name: s.relay.Name(),
|
||||
Description: version.Description,
|
||||
Nips: supportedNIPs, Software: version.URL,
|
||||
Version: version.V,
|
||||
Limitation: relayinfo.Limits{
|
||||
AuthRequired: s.C.AuthRequired,
|
||||
},
|
||||
Icon: "https://cdn.satellite.earth/ac9778868fbf23b63c47c769a74e163377e6ea94d3f0f31711931663d035c4f6.png",
|
||||
}
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(info); chk.E(err) {
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
package realy
|
||||
package relay
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"orly.dev/protocol/socketapi"
|
||||
"orly.dev/pkg/protocol/socketapi"
|
||||
)
|
||||
|
||||
func (s *Server) handleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
a := &socketapi.A{Server: s}
|
||||
a := &socketapi.A{I: s}
|
||||
a.Serve(w, r, s)
|
||||
}
|
||||
101
pkg/app/relay/helpers/helpers.go
Normal file
101
pkg/app/relay/helpers/helpers.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GenerateDescription generates a detailed description containing the provided
|
||||
// text and an optional list of scopes.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - text: A string representing the base description.
|
||||
//
|
||||
// - scopes: A slice of strings indicating scopes to be included in the
|
||||
// description.
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - A string combining the base description and a formatted list of
|
||||
// scopes, if provided.
|
||||
//
|
||||
// # Expected behaviour
|
||||
//
|
||||
// The function appends a formatted list of scopes to the base description if
|
||||
// any scopes are provided. If no scopes are provided, it returns the base
|
||||
// description unchanged. The formatted list of scopes includes each scope
|
||||
// surrounded by backticks and separated by commas.
|
||||
func GenerateDescription(text string, scopes []string) string {
|
||||
if len(scopes) == 0 {
|
||||
return text
|
||||
}
|
||||
result := make([]string, 0)
|
||||
for _, value := range scopes {
|
||||
result = append(result, "`"+value+"`")
|
||||
}
|
||||
return text + "<br/><br/>**Scopes**<br/>" + strings.Join(result, ", ")
|
||||
}
|
||||
|
||||
// GetRemoteFromReq retrieves the originating IP address of the client from
|
||||
// an HTTP request, considering standard and non-standard proxy headers.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - r: The HTTP request object containing details of the client and
|
||||
// routing information.
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - rr: A string value representing the IP address of the originating
|
||||
// remote client.
|
||||
//
|
||||
// # Expected behaviour
|
||||
//
|
||||
// The function first checks for the standardized "Forwarded" header (RFC 7239)
|
||||
// to identify the original client IP. If that isn't available, it falls back to
|
||||
// the "X-Forwarded-For" header. If both headers are absent, it defaults to
|
||||
// using the request's RemoteAddr.
|
||||
//
|
||||
// For the "Forwarded" header, it extracts the client IP from the "for"
|
||||
// parameter. For the "X-Forwarded-For" header, if it contains one IP, it
|
||||
// returns that. If it contains two IPs, it returns the second.
|
||||
func GetRemoteFromReq(r *http.Request) (rr string) {
|
||||
// First check for the standardized Forwarded header (RFC 7239)
|
||||
forwarded := r.Header.Get("Forwarded")
|
||||
if forwarded != "" {
|
||||
// Parse the Forwarded header which can contain multiple parameters
|
||||
//
|
||||
// Format:
|
||||
//
|
||||
// Forwarded: by=<identifier>;for=<identifier>;host=<host>;proto=<http|https>
|
||||
parts := strings.Split(forwarded, ";")
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if strings.HasPrefix(part, "for=") {
|
||||
// Extract the client IP from the "for" parameter
|
||||
forValue := strings.TrimPrefix(part, "for=")
|
||||
// Remove quotes if present
|
||||
forValue = strings.Trim(forValue, "\"")
|
||||
// Handle IPv6 addresses which are enclosed in square brackets
|
||||
forValue = strings.Trim(forValue, "[]")
|
||||
return forValue
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the Forwarded header is not available or doesn't contain "for"
|
||||
// parameter, fall back to X-Forwarded-For
|
||||
rem := r.Header.Get("X-Forwarded-For")
|
||||
if rem == "" {
|
||||
rr = r.RemoteAddr
|
||||
} else {
|
||||
splitted := strings.Split(rem, " ")
|
||||
if len(splitted) == 1 {
|
||||
rr = splitted[0]
|
||||
}
|
||||
if len(splitted) == 2 {
|
||||
rr = splitted[1]
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
108
pkg/app/relay/lists.go
Normal file
108
pkg/app/relay/lists.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Lists manages lists of pubkeys, followed users, follows, and muted users with
|
||||
// concurrency safety via a mutex.
|
||||
//
|
||||
// This list is designed primarily for owner-follow-list in mind, but with an
|
||||
// explicit allowlist/blocklist set up, ownersFollowed corresponds to the
|
||||
// allowed users, and ownersMuted corresponds to the blocked users, and all
|
||||
// filtering logic will work the same way.
|
||||
//
|
||||
// Currently, there is no explicit purpose for the followedFollows list being
|
||||
// separate from the ownersFollowed list, but there could be reasons for this
|
||||
// distinction, such as rate limiting applying to the former and not the latter.
|
||||
type Lists struct {
|
||||
sync.Mutex
|
||||
ownersPubkeys [][]byte
|
||||
ownersFollowed [][]byte
|
||||
followedFollows [][]byte
|
||||
ownersMuted [][]byte
|
||||
}
|
||||
|
||||
func (l *Lists) LenOwnersPubkeys() (ll int) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
ll = len(l.ownersPubkeys)
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Lists) OwnersPubkeys() (pks [][]byte) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
pks = append(pks, l.ownersPubkeys...)
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Lists) SetOwnersPubkeys(pks [][]byte) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
l.ownersPubkeys = pks
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Lists) LenOwnersFollowed() (ll int) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
ll = len(l.ownersFollowed)
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Lists) OwnersFollowed() (pks [][]byte) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
pks = append(pks, l.ownersFollowed...)
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Lists) SetOwnersFollowed(pks [][]byte) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
l.ownersFollowed = pks
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Lists) LenFollowedFollows() (ll int) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
ll = len(l.followedFollows)
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Lists) FollowedFollows() (pks [][]byte) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
pks = append(pks, l.followedFollows...)
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Lists) SetFollowedFollows(pks [][]byte) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
l.followedFollows = pks
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Lists) LenOwnersMuted() (ll int) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
ll = len(l.ownersMuted)
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Lists) OwnersMuted() (pks [][]byte) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
pks = append(pks, l.ownersMuted...)
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Lists) SetOwnersMuted(pks [][]byte) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
l.ownersMuted = pks
|
||||
return
|
||||
}
|
||||
217
pkg/app/relay/lists_test.go
Normal file
217
pkg/app/relay/lists_test.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLists_OwnersPubkeys(t *testing.T) {
|
||||
// Create a new Lists instance
|
||||
l := &Lists{}
|
||||
|
||||
// Test with empty list
|
||||
pks := l.OwnersPubkeys()
|
||||
if len(pks) != 0 {
|
||||
t.Errorf("Expected empty list, got %d items", len(pks))
|
||||
}
|
||||
|
||||
// Test with some pubkeys
|
||||
testPubkeys := [][]byte{
|
||||
[]byte("pubkey1"),
|
||||
[]byte("pubkey2"),
|
||||
[]byte("pubkey3"),
|
||||
}
|
||||
|
||||
l.SetOwnersPubkeys(testPubkeys)
|
||||
|
||||
// Verify length
|
||||
if l.LenOwnersPubkeys() != len(testPubkeys) {
|
||||
t.Errorf("Expected length %d, got %d", len(testPubkeys), l.LenOwnersPubkeys())
|
||||
}
|
||||
|
||||
// Verify content
|
||||
pks = l.OwnersPubkeys()
|
||||
if len(pks) != len(testPubkeys) {
|
||||
t.Errorf("Expected %d pubkeys, got %d", len(testPubkeys), len(pks))
|
||||
}
|
||||
|
||||
// Verify each pubkey
|
||||
for i, pk := range pks {
|
||||
if !bytes.Equal(pk, testPubkeys[i]) {
|
||||
t.Errorf("Pubkey at index %d doesn't match: expected %s, got %s",
|
||||
i, testPubkeys[i], pk)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that the returned slice is a copy, not a reference
|
||||
pks[0] = []byte("modified")
|
||||
newPks := l.OwnersPubkeys()
|
||||
if bytes.Equal(pks[0], newPks[0]) {
|
||||
t.Error("Returned slice should be a copy, not a reference")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLists_OwnersFollowed(t *testing.T) {
|
||||
// Create a new Lists instance
|
||||
l := &Lists{}
|
||||
|
||||
// Test with empty list
|
||||
followed := l.OwnersFollowed()
|
||||
if len(followed) != 0 {
|
||||
t.Errorf("Expected empty list, got %d items", len(followed))
|
||||
}
|
||||
|
||||
// Test with some pubkeys
|
||||
testPubkeys := [][]byte{
|
||||
[]byte("followed1"),
|
||||
[]byte("followed2"),
|
||||
[]byte("followed3"),
|
||||
}
|
||||
|
||||
l.SetOwnersFollowed(testPubkeys)
|
||||
|
||||
// Verify length
|
||||
if l.LenOwnersFollowed() != len(testPubkeys) {
|
||||
t.Errorf("Expected length %d, got %d", len(testPubkeys), l.LenOwnersFollowed())
|
||||
}
|
||||
|
||||
// Verify content
|
||||
followed = l.OwnersFollowed()
|
||||
if len(followed) != len(testPubkeys) {
|
||||
t.Errorf("Expected %d followed, got %d", len(testPubkeys), len(followed))
|
||||
}
|
||||
|
||||
// Verify each pubkey
|
||||
for i, pk := range followed {
|
||||
if !bytes.Equal(pk, testPubkeys[i]) {
|
||||
t.Errorf("Followed at index %d doesn't match: expected %s, got %s",
|
||||
i, testPubkeys[i], pk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLists_FollowedFollows(t *testing.T) {
|
||||
// Create a new Lists instance
|
||||
l := &Lists{}
|
||||
|
||||
// Test with empty list
|
||||
follows := l.FollowedFollows()
|
||||
if len(follows) != 0 {
|
||||
t.Errorf("Expected empty list, got %d items", len(follows))
|
||||
}
|
||||
|
||||
// Test with some pubkeys
|
||||
testPubkeys := [][]byte{
|
||||
[]byte("follow1"),
|
||||
[]byte("follow2"),
|
||||
[]byte("follow3"),
|
||||
}
|
||||
|
||||
l.SetFollowedFollows(testPubkeys)
|
||||
|
||||
// Verify length
|
||||
if l.LenFollowedFollows() != len(testPubkeys) {
|
||||
t.Errorf("Expected length %d, got %d", len(testPubkeys), l.LenFollowedFollows())
|
||||
}
|
||||
|
||||
// Verify content
|
||||
follows = l.FollowedFollows()
|
||||
if len(follows) != len(testPubkeys) {
|
||||
t.Errorf("Expected %d follows, got %d", len(testPubkeys), len(follows))
|
||||
}
|
||||
|
||||
// Verify each pubkey
|
||||
for i, pk := range follows {
|
||||
if !bytes.Equal(pk, testPubkeys[i]) {
|
||||
t.Errorf("Follow at index %d doesn't match: expected %s, got %s",
|
||||
i, testPubkeys[i], pk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLists_OwnersMuted(t *testing.T) {
|
||||
// Create a new Lists instance
|
||||
l := &Lists{}
|
||||
|
||||
// Test with empty list
|
||||
muted := l.OwnersMuted()
|
||||
if len(muted) != 0 {
|
||||
t.Errorf("Expected empty list, got %d items", len(muted))
|
||||
}
|
||||
|
||||
// Test with some pubkeys
|
||||
testPubkeys := [][]byte{
|
||||
[]byte("muted1"),
|
||||
[]byte("muted2"),
|
||||
[]byte("muted3"),
|
||||
}
|
||||
|
||||
l.SetOwnersMuted(testPubkeys)
|
||||
|
||||
// Verify length
|
||||
if l.LenOwnersMuted() != len(testPubkeys) {
|
||||
t.Errorf("Expected length %d, got %d", len(testPubkeys), l.LenOwnersMuted())
|
||||
}
|
||||
|
||||
// Verify content
|
||||
muted = l.OwnersMuted()
|
||||
if len(muted) != len(testPubkeys) {
|
||||
t.Errorf("Expected %d muted, got %d", len(testPubkeys), len(muted))
|
||||
}
|
||||
|
||||
// Verify each pubkey
|
||||
for i, pk := range muted {
|
||||
if !bytes.Equal(pk, testPubkeys[i]) {
|
||||
t.Errorf("Muted at index %d doesn't match: expected %s, got %s",
|
||||
i, testPubkeys[i], pk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLists_ConcurrentAccess(t *testing.T) {
|
||||
// Create a new Lists instance
|
||||
l := &Lists{}
|
||||
|
||||
// Test concurrent access to the lists
|
||||
done := make(chan bool)
|
||||
|
||||
// Concurrent reads and writes
|
||||
go func() {
|
||||
for i := 0; i < 100; i++ {
|
||||
l.SetOwnersPubkeys([][]byte{[]byte("pubkey1"), []byte("pubkey2")})
|
||||
l.OwnersPubkeys()
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for i := 0; i < 100; i++ {
|
||||
l.SetOwnersFollowed([][]byte{[]byte("followed1"), []byte("followed2")})
|
||||
l.OwnersFollowed()
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for i := 0; i < 100; i++ {
|
||||
l.SetFollowedFollows([][]byte{[]byte("follow1"), []byte("follow2")})
|
||||
l.FollowedFollows()
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for i := 0; i < 100; i++ {
|
||||
l.SetOwnersMuted([][]byte{[]byte("muted1"), []byte("muted2")})
|
||||
l.OwnersMuted()
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
for i := 0; i < 4; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
// If we got here without deadlocks or panics, the test passes
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
// Package options provides some option configurations for the realy relay.
|
||||
// Package options provides some option configurations for the relay.
|
||||
//
|
||||
// None of this package is actually in use, and the skip event function has not been
|
||||
// implemented. In theory this could be used for something but it currently isn't.
|
||||
// None of this package is actually in use, and the skip event function has not
|
||||
// been implemented. In theory, this could be used for something but it currently
|
||||
// isn't.
|
||||
package options
|
||||
|
||||
import (
|
||||
"orly.dev/encoders/event"
|
||||
"orly.dev/pkg/encoders/event"
|
||||
)
|
||||
|
||||
type SkipEventFunc func(*event.E) bool
|
||||
|
||||
// T is a collection of options.
|
||||
type T struct {
|
||||
// SkipEventFunc is in theory a function to test whether an event should not be sent in
|
||||
// response to a query.
|
||||
// SkipEventFunc is in theory a function to test whether an event should not
|
||||
// be sent in response to a query.
|
||||
SkipEventFunc
|
||||
}
|
||||
|
||||
@@ -25,7 +26,8 @@ func Default() *T {
|
||||
return &T{}
|
||||
}
|
||||
|
||||
// WithSkipEventFunc is an options.T generator that adds a function to skip events.
|
||||
// WithSkipEventFunc is an options.T generator that adds a function to skip
|
||||
// events.
|
||||
func WithSkipEventFunc(skipEventFunc func(*event.E) bool) O {
|
||||
return func(o *T) {
|
||||
o.SkipEventFunc = skipEventFunc
|
||||
@@ -4,8 +4,8 @@
|
||||
package publish
|
||||
|
||||
import (
|
||||
"orly.dev/app/realy/publish/publisher"
|
||||
"orly.dev/encoders/event"
|
||||
"orly.dev/pkg/encoders/event"
|
||||
"orly.dev/pkg/interfaces/publisher"
|
||||
)
|
||||
|
||||
// S is the control structure for the subscription management scheme.
|
||||
23
pkg/app/relay/server-impl.go
Normal file
23
pkg/app/relay/server-impl.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
"orly.dev/pkg/app/relay/publish"
|
||||
"orly.dev/pkg/interfaces/relay"
|
||||
"orly.dev/pkg/interfaces/server"
|
||||
"orly.dev/pkg/interfaces/store"
|
||||
"orly.dev/pkg/utils/context"
|
||||
)
|
||||
|
||||
func (s *Server) Storage() store.I { return s.relay.Storage() }
|
||||
|
||||
func (s *Server) Relay() relay.I { return s.relay }
|
||||
|
||||
func (s *Server) Publisher() *publish.S { return s.listeners }
|
||||
|
||||
func (s *Server) Context() context.T { return s.Ctx }
|
||||
|
||||
func (s *Server) AuthRequired() bool { return s.C.AuthRequired || s.LenOwnersPubkeys() > 0 }
|
||||
|
||||
func (s *Server) PublicReadable() bool { return s.C.PublicReadable }
|
||||
|
||||
var _ server.I = &Server{}
|
||||
@@ -1,34 +1,53 @@
|
||||
package realy
|
||||
package relay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"orly.dev/encoders/tags"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/utils/errorf"
|
||||
"orly.dev/utils/log"
|
||||
"orly.dev/utils/normalize"
|
||||
|
||||
"orly.dev/encoders/event"
|
||||
"orly.dev/encoders/filter"
|
||||
"orly.dev/encoders/kinds"
|
||||
"orly.dev/encoders/tag"
|
||||
"orly.dev/interfaces/store"
|
||||
"orly.dev/utils/context"
|
||||
"orly.dev/pkg/encoders/event"
|
||||
"orly.dev/pkg/encoders/filter"
|
||||
"orly.dev/pkg/encoders/kind"
|
||||
"orly.dev/pkg/encoders/kinds"
|
||||
"orly.dev/pkg/encoders/tag"
|
||||
"orly.dev/pkg/encoders/tags"
|
||||
"orly.dev/pkg/interfaces/store"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
"orly.dev/pkg/utils/context"
|
||||
"orly.dev/pkg/utils/errorf"
|
||||
"orly.dev/pkg/utils/log"
|
||||
"orly.dev/pkg/utils/normalize"
|
||||
)
|
||||
|
||||
// Publish processes and saves an event based on its type and rules.
|
||||
// It handles replaceable, ephemeral, and parameterized replaceable events.
|
||||
// Duplicate or conflicting events are managed before saving the new one.
|
||||
// Publish processes and stores an event in the server's storage. It handles different types of events: ephemeral, replaceable, and parameterized replaceable.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - c (context.Context): The context for the operation.
|
||||
//
|
||||
// - evt (*event.E): The event to be published.
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - err (error): An error if any step fails during the publishing process.
|
||||
//
|
||||
// # Expected Behaviour
|
||||
//
|
||||
// - For ephemeral events, the method doesn't store them and returns
|
||||
// immediately.
|
||||
//
|
||||
// - For replaceable events, it first queries for existing similar events,
|
||||
// deletes older ones, and then stores the new event.
|
||||
//
|
||||
// - For parameterized replaceable events, it performs a similar process but
|
||||
// uses additional tags to identify duplicates.
|
||||
func (s *Server) Publish(c context.T, evt *event.E) (err error) {
|
||||
sto := s.relay.Storage()
|
||||
if evt.Kind.IsEphemeral() {
|
||||
// do not store ephemeral events
|
||||
// don't store ephemeral events
|
||||
return nil
|
||||
|
||||
} else if evt.Kind.IsReplaceable() {
|
||||
// replaceable event, delete before storing
|
||||
// replaceable event, delete old after storing
|
||||
var evs []*event.E
|
||||
f := filter.New()
|
||||
f.Authors = tag.New(evt.Pubkey)
|
||||
@@ -48,14 +67,63 @@ func (s *Server) Publish(c context.T, evt *event.E) (err error) {
|
||||
"maybe replace %s with %s", ev.Serialize(), evt.Serialize(),
|
||||
)
|
||||
if ev.CreatedAt.Int() > evt.CreatedAt.Int() {
|
||||
log.I.S(ev, evt)
|
||||
return errorf.W(string(normalize.Invalid.F("not replacing newer replaceable event")))
|
||||
return errorf.W(
|
||||
string(
|
||||
normalize.Invalid.F(
|
||||
"not replacing newer replaceable event",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
// not deleting these events because some clients are retarded
|
||||
// and the query will pull the new one, but a backup can recover
|
||||
// the data of old ones
|
||||
if ev.Kind.IsDirectoryEvent() {
|
||||
del = false
|
||||
if evt.Kind.Equal(kind.FollowList) {
|
||||
// if the event is from someone on ownersFollowed or
|
||||
// followedFollows, for now add to this list so they're
|
||||
// immediately effective.
|
||||
var isFollowed bool
|
||||
ownersFollowed := s.OwnersFollowed()
|
||||
for _, pk := range ownersFollowed {
|
||||
if bytes.Equal(evt.Pubkey, pk) {
|
||||
isFollowed = true
|
||||
}
|
||||
}
|
||||
if isFollowed {
|
||||
if _, _, err = sto.SaveEvent(
|
||||
c, evt,
|
||||
); err != nil && !errors.Is(
|
||||
err, store.ErrDupEvent,
|
||||
) {
|
||||
return
|
||||
}
|
||||
// we need to trigger the spider with no fetch
|
||||
if err = s.Spider(true); chk.E(err) {
|
||||
err = nil
|
||||
}
|
||||
// event has been saved and lists updated.
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
if evt.Kind.Equal(kind.MuteList) {
|
||||
// check if this is one of the owners, if so, the mute list
|
||||
// should be applied immediately.
|
||||
owners := s.OwnersPubkeys()
|
||||
for _, pk := range owners {
|
||||
if bytes.Equal(evt.Pubkey, pk) {
|
||||
if _, _, err = sto.SaveEvent(
|
||||
c, evt,
|
||||
); err != nil && !errors.Is(
|
||||
err, store.ErrDupEvent,
|
||||
) {
|
||||
return
|
||||
}
|
||||
// we need to trigger the spider with no fetch
|
||||
if err = s.Spider(true); chk.E(err) {
|
||||
err = nil
|
||||
}
|
||||
// event has been saved and lists updated.
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// defer the delete until after the save, further down, has
|
||||
// completed.
|
||||
@@ -74,8 +142,6 @@ func (s *Server) Publish(c context.T, evt *event.E) (err error) {
|
||||
)
|
||||
},
|
||||
)
|
||||
// replaceable events we don't tombstone when replacing,
|
||||
// so if deleted, old versions can be restored
|
||||
if err = sto.DeleteEvent(c, ev.EventId()); chk.E(err) {
|
||||
return
|
||||
}
|
||||
@@ -156,7 +222,7 @@ func (s *Server) Publish(c context.T, evt *event.E) (err error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, _, err = sto.SaveEvent(c, evt); chk.E(err) && !errors.Is(
|
||||
if _, _, err = sto.SaveEvent(c, evt); err != nil && !errors.Is(
|
||||
err, store.ErrDupEvent,
|
||||
) {
|
||||
return
|
||||
272
pkg/app/relay/server.go
Normal file
272
pkg/app/relay/server.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"orly.dev/pkg/app/config"
|
||||
"orly.dev/pkg/app/relay/helpers"
|
||||
"orly.dev/pkg/app/relay/options"
|
||||
"orly.dev/pkg/app/relay/publish"
|
||||
"orly.dev/pkg/interfaces/relay"
|
||||
"orly.dev/pkg/protocol/servemux"
|
||||
"orly.dev/pkg/protocol/socketapi"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
"orly.dev/pkg/utils/context"
|
||||
"orly.dev/pkg/utils/log"
|
||||
|
||||
"github.com/rs/cors"
|
||||
)
|
||||
|
||||
// Server represents the core structure for running a nostr relay. It
|
||||
// encapsulates various components such as context, cancel function, options,
|
||||
// relay interface, address, HTTP server, and configuration settings.
|
||||
type Server struct {
|
||||
Ctx context.T
|
||||
Cancel context.F
|
||||
options *options.T
|
||||
relay relay.I
|
||||
Addr string
|
||||
mux *servemux.S
|
||||
httpServer *http.Server
|
||||
listeners *publish.S
|
||||
*config.C
|
||||
*Lists
|
||||
}
|
||||
|
||||
// ServerParams represents the configuration parameters for initializing a
|
||||
// server. It encapsulates various components such as context, cancel function,
|
||||
// relay interface, database path, maximum limit, and configuration settings.
|
||||
type ServerParams struct {
|
||||
Ctx context.T
|
||||
Cancel context.F
|
||||
Rl relay.I
|
||||
DbPath string
|
||||
MaxLimit int
|
||||
*config.C
|
||||
}
|
||||
|
||||
// NewServer initializes and returns a new Server instance based on the provided
|
||||
// ServerParams and optional settings. It sets up storage, initializes the
|
||||
// relay, and configures necessary components for server operation.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - sp (*ServerParams): The configuration parameters for initializing the
|
||||
// server.
|
||||
//
|
||||
// - opts (...options.O): Optional settings that modify the server's behavior.
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - s (*Server): The newly created Server instance.
|
||||
//
|
||||
// - err (error): An error if any step fails during initialization.
|
||||
//
|
||||
// # Expected Behaviour
|
||||
//
|
||||
// - Initializes storage with the provided database path.
|
||||
//
|
||||
// - Configures the server's options using the default settings and applies any
|
||||
// optional settings provided.
|
||||
//
|
||||
// - Sets up a ServeMux for handling HTTP requests.
|
||||
//
|
||||
// - Initializes the relay, starting its operation in a separate goroutine.
|
||||
func NewServer(sp *ServerParams, opts ...options.O) (s *Server, err error) {
|
||||
op := options.Default()
|
||||
for _, opt := range opts {
|
||||
opt(op)
|
||||
}
|
||||
if storage := sp.Rl.Storage(); storage != nil {
|
||||
if err = storage.Init(sp.DbPath); chk.T(err) {
|
||||
return nil, fmt.Errorf("storage init: %w", err)
|
||||
}
|
||||
}
|
||||
serveMux := servemux.NewServeMux()
|
||||
s = &Server{
|
||||
Ctx: sp.Ctx,
|
||||
Cancel: sp.Cancel,
|
||||
relay: sp.Rl,
|
||||
mux: serveMux,
|
||||
options: op,
|
||||
listeners: publish.New(socketapi.New()),
|
||||
C: sp.C,
|
||||
Lists: new(Lists),
|
||||
}
|
||||
go func() {
|
||||
if err := s.relay.Init(); chk.E(err) {
|
||||
s.Shutdown()
|
||||
}
|
||||
}()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// ServeHTTP handles incoming HTTP requests according to the standard Nostr
|
||||
// protocol. It specifically processes WebSocket upgrades and
|
||||
// "application/nostr+json" Accept headers.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - w (http.ResponseWriter): The response writer for sending responses.
|
||||
//
|
||||
// - r (*http.Request): The request object containing client's details and data.
|
||||
//
|
||||
// # Expected Behaviour
|
||||
//
|
||||
// - Checks if the request URL path is "/".
|
||||
//
|
||||
// - For WebSocket upgrades, calls handleWebsocket method.
|
||||
//
|
||||
// - If "Accept" header is "application/nostr+json", calls HandleRelayInfo
|
||||
// method.
|
||||
//
|
||||
// - Logs the HTTP request details for non-standard requests.
|
||||
//
|
||||
// - For all other paths, delegates to the internal mux's ServeHTTP method.
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// standard nostr protocol only governs the "root" path of the relay and
|
||||
// websockets
|
||||
if r.URL.Path == "/" {
|
||||
if r.Header.Get("Upgrade") == "websocket" {
|
||||
s.handleWebsocket(w, r)
|
||||
return
|
||||
}
|
||||
if r.Header.Get("Accept") == "application/nostr+json" {
|
||||
s.HandleRelayInfo(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
log.I.F(
|
||||
"http request: %s from %s",
|
||||
r.URL.String(), helpers.GetRemoteFromReq(r),
|
||||
)
|
||||
s.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Start initializes the server by setting up a TCP listener and serving HTTP
|
||||
// requests.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - host (string): The hostname or IP address to listen on.
|
||||
//
|
||||
// - port (int): The port number to bind to.
|
||||
//
|
||||
// - started (...chan bool): Optional channels that are closed after the server
|
||||
// starts successfully.
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - err (error): An error if any step fails during the server startup process.
|
||||
//
|
||||
// # Expected Behaviour
|
||||
//
|
||||
// - Joins the host and port into a full address string.
|
||||
//
|
||||
// - Logs the intention to start the relay listener at the specified address.
|
||||
//
|
||||
// - Listens for TCP connections on the specified address.
|
||||
//
|
||||
// - Configures an HTTP server with CORS middleware, sets timeouts, and binds it
|
||||
// to the listener.
|
||||
//
|
||||
// - If any started channels are provided, closes them upon successful startup.
|
||||
//
|
||||
// - Starts serving requests using the configured HTTP server.
|
||||
func (s *Server) Start(
|
||||
host string, port int, started ...chan bool,
|
||||
) (err error) {
|
||||
if len(s.C.Owners) > 0 {
|
||||
// start up spider
|
||||
if err = s.Spider(s.C.Private); chk.E(err) {
|
||||
// there wasn't any owners, or they couldn't be found on the spider
|
||||
// seeds.
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
// start up a spider run to trigger every 30 minutes
|
||||
ticker := time.NewTicker(30 * time.Minute)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err = s.Spider(s.C.Private); chk.E(err) {
|
||||
// there wasn't any owners, or they couldn't be found on the spider
|
||||
// seeds.
|
||||
err = nil
|
||||
}
|
||||
case <-s.Ctx.Done():
|
||||
log.I.F("stopping spider ticker")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
addr := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
log.I.F("starting relay listener at %s", addr)
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.httpServer = &http.Server{
|
||||
Handler: cors.Default().Handler(s),
|
||||
Addr: addr,
|
||||
ReadHeaderTimeout: 7 * time.Second,
|
||||
IdleTimeout: 28 * time.Second,
|
||||
}
|
||||
for _, startedC := range started {
|
||||
close(startedC)
|
||||
}
|
||||
if err = s.httpServer.Serve(ln); errors.Is(err, http.ErrServerClosed) {
|
||||
} else if err != nil {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the server and its components. It ensures that
|
||||
// all resources are properly released.
|
||||
//
|
||||
// # Expected Behaviour
|
||||
//
|
||||
// - Logs shutting down message.
|
||||
//
|
||||
// - Cancels the context to stop ongoing operations.
|
||||
//
|
||||
// - Closes the event store, logging the action and checking for errors.
|
||||
//
|
||||
// - Shuts down the HTTP server, logging the action and checking for errors.
|
||||
//
|
||||
// - If the relay implements ShutdownAware, it calls OnShutdown with the
|
||||
// context.
|
||||
func (s *Server) Shutdown() {
|
||||
log.I.Ln("shutting down relay")
|
||||
s.Cancel()
|
||||
log.W.Ln("closing event store")
|
||||
chk.E(s.relay.Storage().Close())
|
||||
if s.httpServer != nil {
|
||||
log.W.Ln("shutting down relay listener")
|
||||
chk.E(s.httpServer.Shutdown(s.Ctx))
|
||||
}
|
||||
if f, ok := s.relay.(relay.ShutdownAware); ok {
|
||||
f.OnShutdown(s.Ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Router retrieves and returns the HTTP ServeMux associated with the server.
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - router (*http.ServeMux): The ServeMux instance used for routing HTTP
|
||||
// requests.
|
||||
//
|
||||
// # Expected Behaviour
|
||||
//
|
||||
// - Returns the ServeMux that handles incoming HTTP requests to the server.
|
||||
func (s *Server) Router() (router *http.ServeMux) {
|
||||
return s.mux.ServeMux
|
||||
}
|
||||
120
pkg/app/relay/spider-fetch.go
Normal file
120
pkg/app/relay/spider-fetch.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
"orly.dev/pkg/crypto/ec/schnorr"
|
||||
"orly.dev/pkg/encoders/event"
|
||||
"orly.dev/pkg/encoders/filter"
|
||||
"orly.dev/pkg/encoders/hex"
|
||||
"orly.dev/pkg/encoders/kind"
|
||||
"orly.dev/pkg/encoders/kinds"
|
||||
"orly.dev/pkg/encoders/tag"
|
||||
"orly.dev/pkg/protocol/ws"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
"orly.dev/pkg/utils/context"
|
||||
"orly.dev/pkg/utils/log"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func (s *Server) SpiderFetch(
|
||||
k *kind.T, noFetch bool, pubkeys ...[]byte,
|
||||
) (pks [][]byte, err error) {
|
||||
// first search the local database
|
||||
pkList := tag.New(pubkeys...)
|
||||
f := &filter.F{
|
||||
Kinds: kinds.New(k),
|
||||
Authors: pkList,
|
||||
}
|
||||
var evs event.S
|
||||
if evs, err = s.Storage().QueryEvents(s.Ctx, f); chk.E(err) {
|
||||
// none were found, so we need to scan the spiders
|
||||
err = nil
|
||||
}
|
||||
if len(evs) < len(pubkeys) && !noFetch {
|
||||
// we need to search the spider seeds.
|
||||
// Break up pubkeys into batches of 512
|
||||
log.I.F("breaking up %d pubkeys into batches of 512", len(pubkeys))
|
||||
for i := 0; i < len(pubkeys); i += 512 {
|
||||
end := i + 512
|
||||
if end > len(pubkeys) {
|
||||
end = len(pubkeys)
|
||||
}
|
||||
batchPubkeys := pubkeys[i:end]
|
||||
log.I.F(
|
||||
"processing batch %d to %d of %d pubkeys", i, end, len(pubkeys),
|
||||
)
|
||||
batchPkList := tag.New(batchPubkeys...)
|
||||
batchFilter := &filter.F{
|
||||
Kinds: kinds.New(k),
|
||||
Authors: batchPkList,
|
||||
}
|
||||
|
||||
for _, seed := range s.C.SpiderSeeds {
|
||||
select {
|
||||
case <-s.Ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
var evss event.S
|
||||
var cli *ws.Client
|
||||
if cli, err = ws.RelayConnect(context.Bg(), seed); chk.E(err) {
|
||||
err = nil
|
||||
continue
|
||||
}
|
||||
if evss, err = cli.QuerySync(
|
||||
context.Bg(), batchFilter,
|
||||
); chk.E(err) {
|
||||
err = nil
|
||||
continue
|
||||
}
|
||||
for _, ev := range evss {
|
||||
evs = append(evs, ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
// save the events to the database
|
||||
for _, ev := range evs {
|
||||
if _, _, err = s.Storage().SaveEvent(s.Ctx, ev); chk.E(err) {
|
||||
err = nil
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
// deduplicate and take the newest
|
||||
var tmp event.S
|
||||
evMap := make(map[string]event.S)
|
||||
for _, ev := range evs {
|
||||
evMap[ev.PubKeyString()] = append(evMap[ev.PubKeyString()], ev)
|
||||
}
|
||||
for _, evm := range evMap {
|
||||
if len(evm) < 1 {
|
||||
continue
|
||||
}
|
||||
if len(evm) > 1 {
|
||||
sort.Sort(evm)
|
||||
}
|
||||
tmp = append(tmp, evm[0])
|
||||
}
|
||||
evs = tmp
|
||||
// we have all we're going to get now
|
||||
pkMap := make(map[string]struct{})
|
||||
for _, ev := range evs {
|
||||
t := ev.Tags.GetAll(tag.New("p"))
|
||||
for _, tt := range t.ToSliceOfTags() {
|
||||
pkh := tt.Value()
|
||||
if len(pkh) != 2*schnorr.PubKeyBytesLen {
|
||||
continue
|
||||
}
|
||||
pk := make([]byte, schnorr.PubKeyBytesLen)
|
||||
if _, err = hex.DecBytes(pk, pkh); chk.E(err) {
|
||||
err = nil
|
||||
continue
|
||||
}
|
||||
pkMap[string(pk)] = struct{}{}
|
||||
}
|
||||
}
|
||||
for pk := range pkMap {
|
||||
pks = append(pks, []byte(pk))
|
||||
}
|
||||
log.I.F("found %d pks", len(pks))
|
||||
return
|
||||
}
|
||||
133
pkg/app/relay/spider.go
Normal file
133
pkg/app/relay/spider.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"orly.dev/pkg/crypto/ec/bech32"
|
||||
"orly.dev/pkg/encoders/bech32encoding"
|
||||
"orly.dev/pkg/encoders/hex"
|
||||
"orly.dev/pkg/encoders/kind"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
"orly.dev/pkg/utils/log"
|
||||
)
|
||||
|
||||
func (s *Server) Spider(noFetch ...bool) (err error) {
|
||||
var ownersPubkeys [][]byte
|
||||
for _, v := range s.C.Owners {
|
||||
var prf []byte
|
||||
var pk []byte
|
||||
var bits5 []byte
|
||||
if prf, bits5, err = bech32.DecodeNoLimit([]byte(v)); chk.D(err) {
|
||||
// try hex then
|
||||
if _, err = hex.DecBytes(pk, []byte(v)); chk.E(err) {
|
||||
log.W.F(
|
||||
"owner key %s is neither bech32 npub nor hex",
|
||||
v,
|
||||
)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if !bytes.Equal(prf, bech32encoding.NpubHRP) {
|
||||
log.W.F(
|
||||
"owner key %s is neither bech32 npub nor hex",
|
||||
v,
|
||||
)
|
||||
continue
|
||||
}
|
||||
if pk, err = bech32.ConvertBits(bits5, 5, 8, false); chk.E(err) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
// owners themselves are on the OwnersFollowed list as first level
|
||||
ownersPubkeys = append(ownersPubkeys, pk)
|
||||
}
|
||||
if len(ownersPubkeys) == 0 {
|
||||
// there is no OwnersPubkeys, so there is nothing to do.
|
||||
return
|
||||
}
|
||||
dontFetch := false
|
||||
if len(noFetch) > 0 && noFetch[0] {
|
||||
dontFetch = true
|
||||
}
|
||||
log.I.F("getting ownersFollowed")
|
||||
var ownersFollowed [][]byte
|
||||
if ownersFollowed, err = s.SpiderFetch(
|
||||
kind.FollowList, dontFetch, ownersPubkeys...,
|
||||
); chk.E(err) {
|
||||
return
|
||||
}
|
||||
log.I.F("getting followedFollows")
|
||||
var followedFollows [][]byte
|
||||
if followedFollows, err = s.SpiderFetch(
|
||||
kind.FollowList, dontFetch, ownersFollowed...,
|
||||
); chk.E(err) {
|
||||
return
|
||||
}
|
||||
log.I.F("getting ownersMuted")
|
||||
var ownersMuted [][]byte
|
||||
if ownersMuted, err = s.SpiderFetch(
|
||||
kind.MuteList, dontFetch, ownersPubkeys...,
|
||||
); chk.E(err) {
|
||||
return
|
||||
}
|
||||
// remove the ownersFollowed and ownersMuted items from the followedFollows
|
||||
// list
|
||||
filteredFollows := make([][]byte, 0, len(followedFollows))
|
||||
for _, follow := range followedFollows {
|
||||
found := false
|
||||
for _, owner := range ownersFollowed {
|
||||
if bytes.Equal(follow, owner) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, owner := range ownersMuted {
|
||||
if bytes.Equal(follow, owner) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
filteredFollows = append(filteredFollows, follow)
|
||||
}
|
||||
}
|
||||
followedFollows = filteredFollows
|
||||
own := "owner"
|
||||
if len(ownersPubkeys) > 1 {
|
||||
own = "owners"
|
||||
}
|
||||
fol := "pubkey"
|
||||
if len(ownersFollowed) > 1 {
|
||||
fol = "pubkeys"
|
||||
}
|
||||
folfol := "pubkey"
|
||||
if len(followedFollows) > 1 {
|
||||
folfol = "pubkeys"
|
||||
}
|
||||
mut := "pubkey"
|
||||
if len(ownersMuted) > 1 {
|
||||
mut = "pubkeys"
|
||||
}
|
||||
log.T.F(
|
||||
"found %d %s with a total of %d followed %s and %d followed's follows %s, and excluding %d owner muted %s",
|
||||
len(ownersPubkeys), own,
|
||||
len(ownersFollowed), fol,
|
||||
len(followedFollows), folfol,
|
||||
len(ownersMuted), mut,
|
||||
)
|
||||
// add the owners
|
||||
ownersFollowed = append(ownersFollowed, ownersPubkeys...)
|
||||
s.SetOwnersPubkeys(ownersPubkeys)
|
||||
s.SetOwnersFollowed(ownersFollowed)
|
||||
s.SetFollowedFollows(followedFollows)
|
||||
s.SetOwnersMuted(ownersMuted)
|
||||
// lastly, update users profile metadata and relay lists in the background
|
||||
if !dontFetch {
|
||||
go func() {
|
||||
everyone := append(ownersFollowed, followedFollows...)
|
||||
s.SpiderFetch(kind.ProfileMetadata, false, everyone...)
|
||||
s.SpiderFetch(kind.RelayListMetadata, false, everyone...)
|
||||
s.SpiderFetch(kind.DMRelaysList, false, everyone...)
|
||||
}()
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
package realy
|
||||
package relay
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"orly.dev/pkg/encoders/event"
|
||||
"orly.dev/pkg/encoders/eventid"
|
||||
"orly.dev/pkg/encoders/filter"
|
||||
"orly.dev/pkg/interfaces/store"
|
||||
"orly.dev/pkg/utils/context"
|
||||
"orly.dev/pkg/utils/units"
|
||||
"testing"
|
||||
|
||||
"orly.dev/encoders/event"
|
||||
"orly.dev/encoders/eventid"
|
||||
"orly.dev/encoders/filter"
|
||||
"orly.dev/interfaces/store"
|
||||
"orly.dev/utils/context"
|
||||
"orly.dev/utils/units"
|
||||
)
|
||||
|
||||
func startTestRelay(c context.T, t *testing.T, tr *testRelay) *Server {
|
||||
39
pkg/app/resources.go
Normal file
39
pkg/app/resources.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"orly.dev/pkg/utils/context"
|
||||
"orly.dev/pkg/utils/log"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MonitorResources periodically logs resource usage metrics such as the number
|
||||
// of active goroutines and CGO calls at 15-minute intervals, and exits when the
|
||||
// provided context signals cancellation.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - c: Context used to control the lifecycle of the resource monitoring process.
|
||||
//
|
||||
// # Expected behaviour
|
||||
//
|
||||
// The function runs indefinitely, logging metrics every 15 minutes until the
|
||||
// context is cancelled. Upon cancellation, it logs a shutdown message and exits
|
||||
// gracefully without returning any values.
|
||||
func MonitorResources(c context.T) {
|
||||
tick := time.NewTicker(time.Minute * 15)
|
||||
log.I.Ln("running process", os.Args[0], os.Getpid())
|
||||
for {
|
||||
select {
|
||||
case <-c.Done():
|
||||
log.D.Ln("shutting down resource monitor")
|
||||
return
|
||||
case <-tick.C:
|
||||
log.D.Ln(
|
||||
"# goroutines", runtime.NumGoroutine(),
|
||||
"# cgo calls", runtime.NumCgoCall(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ package base58_test
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"orly.dev/crypto/ec/base58"
|
||||
"orly.dev/pkg/crypto/ec/base58"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ package base58_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"orly.dev/crypto/ec/base58"
|
||||
"orly.dev/pkg/crypto/ec/base58"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ package base58
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"orly.dev/crypto/sha256"
|
||||
"orly.dev/pkg/crypto/sha256"
|
||||
)
|
||||
|
||||
// ErrChecksum indicates that the checksum of a check-encoded string does not verify against
|
||||
@@ -5,7 +5,7 @@
|
||||
package base58_test
|
||||
|
||||
import (
|
||||
"orly.dev/crypto/ec/base58"
|
||||
"orly.dev/pkg/crypto/ec/base58"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -6,14 +6,14 @@ package base58_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
base59 "orly.dev/crypto/ec/base58"
|
||||
"orly.dev/pkg/crypto/ec/base58"
|
||||
)
|
||||
|
||||
// This example demonstrates how to decode modified base58 encoded data.
|
||||
func ExampleDecode() {
|
||||
// Decode example modified base58 encoded data.
|
||||
encoded := "25JnwSn7XKfNQ"
|
||||
decoded := base59.Decode(encoded)
|
||||
decoded := base58.Decode(encoded)
|
||||
|
||||
// Show the decoded data.
|
||||
fmt.Println("Decoded Data:", string(decoded))
|
||||
@@ -27,7 +27,7 @@ func ExampleDecode() {
|
||||
func ExampleEncode() {
|
||||
// Encode example data with the modified base58 encoding scheme.
|
||||
data := []byte("Test data")
|
||||
encoded := base59.Encode(data)
|
||||
encoded := base58.Encode(data)
|
||||
|
||||
// Show the encoded data.
|
||||
fmt.Println("Encoded Data:", encoded)
|
||||
@@ -40,7 +40,7 @@ func ExampleEncode() {
|
||||
func ExampleCheckDecode() {
|
||||
// Decode an example Base58Check encoded data.
|
||||
encoded := "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
|
||||
decoded, version, err := base59.CheckDecode(encoded)
|
||||
decoded, version, err := base58.CheckDecode(encoded)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
@@ -60,7 +60,7 @@ func ExampleCheckDecode() {
|
||||
func ExampleCheckEncode() {
|
||||
// Encode example data with the Base58Check encoding scheme.
|
||||
data := []byte("Test data")
|
||||
encoded := base59.CheckEncode(data, 0)
|
||||
encoded := base58.CheckEncode(data, 0)
|
||||
|
||||
// Show the encoded data.
|
||||
fmt.Println("Encoded Data:", encoded)
|
||||
@@ -52,7 +52,7 @@ func TestBech32(t *testing.T) {
|
||||
{
|
||||
"split1cheo2y9e2w",
|
||||
ErrNonCharsetChar('o'),
|
||||
}, // invalid character (o) in data part
|
||||
}, // invalid character (o) in data part
|
||||
{"split1a2y9w", ErrInvalidSeparatorIndex(5)}, // too short data part
|
||||
{
|
||||
"1checkupstagehandshakeupstreamerranterredcaperred2y9e3w",
|
||||
@@ -6,10 +6,9 @@ package btcec
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"orly.dev/crypto/ec/secp256k1"
|
||||
"orly.dev/pkg/crypto/ec/secp256k1"
|
||||
"orly.dev/pkg/encoders/hex"
|
||||
"testing"
|
||||
|
||||
"orly.dev/encoders/hex"
|
||||
)
|
||||
|
||||
// setHex decodes the passed big-endian hex string into the internal field value
|
||||
@@ -20,31 +20,31 @@ package btcec
|
||||
// reverse the transform than to operate in affine coordinates.
|
||||
|
||||
import (
|
||||
secp256k2 "orly.dev/crypto/ec/secp256k1"
|
||||
"orly.dev/pkg/crypto/ec/secp256k1"
|
||||
)
|
||||
|
||||
// KoblitzCurve provides an implementation for secp256k1 that fits the ECC
|
||||
// Curve interface from crypto/elliptic.
|
||||
type KoblitzCurve = secp256k2.KoblitzCurve
|
||||
type KoblitzCurve = secp256k1.KoblitzCurve
|
||||
|
||||
// S256 returns a Curve which implements secp256k1.
|
||||
func S256() *KoblitzCurve {
|
||||
return secp256k2.S256()
|
||||
return secp256k1.S256()
|
||||
}
|
||||
|
||||
// CurveParams contains the parameters for the secp256k1 curve.
|
||||
type CurveParams = secp256k2.CurveParams
|
||||
type CurveParams = secp256k1.CurveParams
|
||||
|
||||
// Params returns the secp256k1 curve parameters for convenience.
|
||||
func Params() *CurveParams {
|
||||
return secp256k2.Params()
|
||||
return secp256k1.Params()
|
||||
}
|
||||
|
||||
// Generator returns the public key at the Generator Point.
|
||||
func Generator() *PublicKey {
|
||||
var (
|
||||
result JacobianPoint
|
||||
k secp256k2.ModNScalar
|
||||
k secp256k1.ModNScalar
|
||||
)
|
||||
k.SetInt(1)
|
||||
ScalarBaseMultNonConst(&k, &result)
|
||||
@@ -2,7 +2,7 @@ package chaincfg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"orly.dev/crypto/ec/wire"
|
||||
"orly.dev/pkg/crypto/ec/wire"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
package chaincfg
|
||||
|
||||
import (
|
||||
"orly.dev/crypto/ec/chainhash"
|
||||
wire2 "orly.dev/crypto/ec/wire"
|
||||
"orly.dev/pkg/crypto/ec/chainhash"
|
||||
"orly.dev/pkg/crypto/ec/wire"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// genesisCoinbaseTx is the coinbase transaction for the genesis blocks for
|
||||
// the main network, regression test network, and test network (version 3).
|
||||
genesisCoinbaseTx = wire2.MsgTx{
|
||||
genesisCoinbaseTx = wire.MsgTx{
|
||||
Version: 1,
|
||||
TxIn: []*wire2.TxIn{
|
||||
TxIn: []*wire.TxIn{
|
||||
{
|
||||
PreviousOutPoint: wire2.OutPoint{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: chainhash.Hash{},
|
||||
Index: 0xffffffff,
|
||||
},
|
||||
@@ -41,7 +41,7 @@ var (
|
||||
Sequence: 0xffffffff,
|
||||
},
|
||||
},
|
||||
TxOut: []*wire2.TxOut{
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 0x12a05f200,
|
||||
PkScript: []byte{
|
||||
@@ -92,8 +92,8 @@ var (
|
||||
// genesisBlock defines
|
||||
// genesisBlock defines the genesis block of the block chain which serves as the
|
||||
// public transaction ledger for the main network.
|
||||
genesisBlock = wire2.MsgBlock{
|
||||
Header: wire2.BlockHeader{
|
||||
genesisBlock = wire.MsgBlock{
|
||||
Header: wire.BlockHeader{
|
||||
Version: 1,
|
||||
PrevBlock: chainhash.Hash{}, // 0000000000000000000000000000000000000000000000000000000000000000
|
||||
MerkleRoot: genesisMerkleRoot, // 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b
|
||||
@@ -104,6 +104,6 @@ var (
|
||||
Bits: 0x1d00ffff, // 486604799 [00000000ffff0000000000000000000000000000000000000000000000000000]
|
||||
Nonce: 0x7c2bac1d, // 2083236893
|
||||
},
|
||||
Transactions: []*wire2.MsgTx{&genesisCoinbaseTx},
|
||||
Transactions: []*wire.MsgTx{&genesisCoinbaseTx},
|
||||
}
|
||||
)
|
||||
@@ -3,8 +3,8 @@ package chaincfg
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"orly.dev/crypto/ec/chainhash"
|
||||
wire2 "orly.dev/crypto/ec/wire"
|
||||
"orly.dev/pkg/crypto/ec/chainhash"
|
||||
"orly.dev/pkg/crypto/ec/wire"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -113,7 +113,7 @@ type Params struct {
|
||||
Name string
|
||||
|
||||
// Net defines the magic bytes used to identify the network.
|
||||
Net wire2.BitcoinNet
|
||||
Net wire.BitcoinNet
|
||||
|
||||
// DefaultPort defines the default peer-to-peer port for the network.
|
||||
DefaultPort string
|
||||
@@ -123,7 +123,7 @@ type Params struct {
|
||||
DNSSeeds []DNSSeed
|
||||
|
||||
// GenesisBlock defines the first block of the chain.
|
||||
GenesisBlock *wire2.MsgBlock
|
||||
GenesisBlock *wire.MsgBlock
|
||||
|
||||
// GenesisHash is the starting block hash.
|
||||
GenesisHash *chainhash.Hash
|
||||
@@ -231,7 +231,7 @@ type Params struct {
|
||||
// MainNetParams defines the network parameters for the main Bitcoin network.
|
||||
var MainNetParams = Params{
|
||||
Name: "mainnet",
|
||||
Net: wire2.MainNet,
|
||||
Net: wire.MainNet,
|
||||
DefaultPort: "8333",
|
||||
DNSSeeds: []DNSSeed{
|
||||
{"seed.bitcoin.sipa.be", true},
|
||||
@@ -8,9 +8,8 @@ package chainhash
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"orly.dev/crypto/sha256"
|
||||
|
||||
"orly.dev/encoders/hex"
|
||||
"orly.dev/pkg/crypto/sha256"
|
||||
"orly.dev/pkg/encoders/hex"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -6,7 +6,7 @@
|
||||
package chainhash
|
||||
|
||||
import (
|
||||
"orly.dev/crypto/sha256"
|
||||
"orly.dev/pkg/crypto/sha256"
|
||||
)
|
||||
|
||||
// HashB calculates hash(b) and returns the resulting bytes.
|
||||
@@ -5,7 +5,7 @@
|
||||
package btcec
|
||||
|
||||
import (
|
||||
"orly.dev/crypto/ec/secp256k1"
|
||||
"orly.dev/pkg/crypto/ec/secp256k1"
|
||||
)
|
||||
|
||||
// GenerateSharedSecret generates a shared secret based on a secret key and a
|
||||
@@ -5,12 +5,12 @@ package btcec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
secp256k2 "orly.dev/crypto/ec/secp256k1"
|
||||
"orly.dev/pkg/crypto/ec/secp256k1"
|
||||
)
|
||||
|
||||
// JacobianPoint is an element of the group formed by the secp256k1 curve in
|
||||
// Jacobian projective coordinates and thus represents a point on the curve.
|
||||
type JacobianPoint = secp256k2.JacobianPoint
|
||||
type JacobianPoint = secp256k1.JacobianPoint
|
||||
|
||||
// infinityPoint is the jacobian representation of the point at infinity.
|
||||
var infinityPoint JacobianPoint
|
||||
@@ -18,13 +18,13 @@ var infinityPoint JacobianPoint
|
||||
// MakeJacobianPoint returns a Jacobian point with the provided X, Y, and Z
|
||||
// coordinates.
|
||||
func MakeJacobianPoint(x, y, z *FieldVal) JacobianPoint {
|
||||
return secp256k2.MakeJacobianPoint(x, y, z)
|
||||
return secp256k1.MakeJacobianPoint(x, y, z)
|
||||
}
|
||||
|
||||
// AddNonConst adds the passed Jacobian points together and stores the result
|
||||
// in the provided result param in *non-constant* time.
|
||||
func AddNonConst(p1, p2, result *JacobianPoint) {
|
||||
secp256k2.AddNonConst(p1, p2, result)
|
||||
secp256k1.AddNonConst(p1, p2, result)
|
||||
}
|
||||
|
||||
// DecompressY attempts to calculate the Y coordinate for the given X
|
||||
@@ -35,7 +35,7 @@ func AddNonConst(p1, p2, result *JacobianPoint) {
|
||||
// The magnitude of the provided X coordinate field val must be a max of 8 for
|
||||
// a correct result. The resulting Y field val will have a max magnitude of 2.
|
||||
func DecompressY(x *FieldVal, odd bool, resultY *FieldVal) bool {
|
||||
return secp256k2.DecompressY(x, odd, resultY)
|
||||
return secp256k1.DecompressY(x, odd, resultY)
|
||||
}
|
||||
|
||||
// DoubleNonConst doubles the passed Jacobian point and stores the result in
|
||||
@@ -44,7 +44,7 @@ func DecompressY(x *FieldVal, odd bool, resultY *FieldVal) bool {
|
||||
// NOTE: The point must be normalized for this function to return the correct
|
||||
// result. The resulting point will be normalized.
|
||||
func DoubleNonConst(p, result *JacobianPoint) {
|
||||
secp256k2.DoubleNonConst(p, result)
|
||||
secp256k1.DoubleNonConst(p, result)
|
||||
}
|
||||
|
||||
// ScalarBaseMultNonConst multiplies k*G where G is the base point of the group
|
||||
@@ -53,7 +53,7 @@ func DoubleNonConst(p, result *JacobianPoint) {
|
||||
//
|
||||
// NOTE: The resulting point will be normalized.
|
||||
func ScalarBaseMultNonConst(k *ModNScalar, result *JacobianPoint) {
|
||||
secp256k2.ScalarBaseMultNonConst(k, result)
|
||||
secp256k1.ScalarBaseMultNonConst(k, result)
|
||||
}
|
||||
|
||||
// ScalarMultNonConst multiplies k*P where k is a big endian integer modulo the
|
||||
@@ -63,7 +63,7 @@ func ScalarBaseMultNonConst(k *ModNScalar, result *JacobianPoint) {
|
||||
// NOTE: The point must be normalized for this function to return the correct
|
||||
// result. The resulting point will be normalized.
|
||||
func ScalarMultNonConst(k *ModNScalar, point, result *JacobianPoint) {
|
||||
secp256k2.ScalarMultNonConst(k, point, result)
|
||||
secp256k1.ScalarMultNonConst(k, point, result)
|
||||
}
|
||||
|
||||
// ParseJacobian parses a byte slice point as a secp256k1.Publickey and returns the
|
||||
@@ -76,12 +76,12 @@ func ParseJacobian(point []byte) (JacobianPoint, error) {
|
||||
"invalid nonce: invalid length: %v",
|
||||
len(point),
|
||||
)
|
||||
return JacobianPoint{}, makeError(secp256k2.ErrPubKeyInvalidLen, str)
|
||||
return JacobianPoint{}, makeError(secp256k1.ErrPubKeyInvalidLen, str)
|
||||
}
|
||||
if point[0] == 0x00 {
|
||||
return infinityPoint, nil
|
||||
}
|
||||
noncePk, err := secp256k2.ParsePubKey(point)
|
||||
noncePk, err := secp256k1.ParsePubKey(point)
|
||||
if err != nil {
|
||||
return JacobianPoint{}, err
|
||||
}
|
||||
@@ -6,22 +6,21 @@
|
||||
package ecdsa
|
||||
|
||||
import (
|
||||
secp256k2 "orly.dev/crypto/ec/secp256k1"
|
||||
"orly.dev/pkg/crypto/ec/secp256k1"
|
||||
"orly.dev/pkg/encoders/hex"
|
||||
"testing"
|
||||
|
||||
"orly.dev/encoders/hex"
|
||||
)
|
||||
|
||||
// hexToModNScalar converts the passed hex string into a ModNScalar and will
|
||||
// panic if there is an error. This is only provided for the hard-coded
|
||||
// constants so errors in the source code can be detected. It will only (and
|
||||
// must only) be called with hard-coded values.
|
||||
func hexToModNScalar(s string) *secp256k2.ModNScalar {
|
||||
func hexToModNScalar(s string) *secp256k1.ModNScalar {
|
||||
b, err := hex.Dec(s)
|
||||
if err != nil {
|
||||
panic("invalid hex in source file: " + s)
|
||||
}
|
||||
var scalar secp256k2.ModNScalar
|
||||
var scalar secp256k1.ModNScalar
|
||||
if overflow := scalar.SetByteSlice(b); overflow {
|
||||
panic("hex in source file overflows mod N scalar: " + s)
|
||||
}
|
||||
@@ -32,12 +31,12 @@ func hexToModNScalar(s string) *secp256k2.ModNScalar {
|
||||
// if there is an error. This is only provided for the hard-coded constants so
|
||||
// errors in the source code can be detected. It will only (and must only) be
|
||||
// called with hard-coded values.
|
||||
func hexToFieldVal(s string) *secp256k2.FieldVal {
|
||||
func hexToFieldVal(s string) *secp256k1.FieldVal {
|
||||
b, err := hex.Dec(s)
|
||||
if err != nil {
|
||||
panic("invalid hex in source file: " + s)
|
||||
}
|
||||
var f secp256k2.FieldVal
|
||||
var f secp256k1.FieldVal
|
||||
if overflow := f.SetByteSlice(b); overflow {
|
||||
panic("hex in source file overflows mod P: " + s)
|
||||
}
|
||||
@@ -49,7 +48,7 @@ func hexToFieldVal(s string) *secp256k2.FieldVal {
|
||||
func BenchmarkSigVerify(b *testing.B) {
|
||||
// Randomly generated keypair.
|
||||
// Secret key: 9e0699c91ca1e3b7e3c9ba71eb71c89890872be97576010fe593fbf3fd57e66d
|
||||
pubKey := secp256k2.NewPublicKey(
|
||||
pubKey := secp256k1.NewPublicKey(
|
||||
hexToFieldVal("d2e670a19c6d753d1a6d8b20bd045df8a08fb162cf508956c31268c6d81ffdab"),
|
||||
hexToFieldVal("ab65528eefbb8057aa85d597258a3fbd481a24633bc9b47a9aa045c91371de52"),
|
||||
)
|
||||
@@ -74,7 +73,7 @@ func BenchmarkSigVerify(b *testing.B) {
|
||||
func BenchmarkSign(b *testing.B) {
|
||||
// Randomly generated keypair.
|
||||
d := hexToModNScalar("9e0699c91ca1e3b7e3c9ba71eb71c89890872be97576010fe593fbf3fd57e66d")
|
||||
secKey := secp256k2.NewSecretKey(d)
|
||||
secKey := secp256k1.NewSecretKey(d)
|
||||
// blake256 of by{0x01, 0x02, 0x03, 0x04}.
|
||||
msgHash := hexToBytes("c301ba9de5d6053caad9f5eb46523f007702add2c62fa39de03146a36b8026b7")
|
||||
b.ReportAllocs()
|
||||
@@ -114,9 +113,9 @@ func BenchmarkNonceRFC6979(b *testing.B) {
|
||||
msgHash := hexToBytes("c301ba9de5d6053caad9f5eb46523f007702add2c62fa39de03146a36b8026b7")
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
var noElideNonce *secp256k2.ModNScalar
|
||||
var noElideNonce *secp256k1.ModNScalar
|
||||
for i := 0; i < b.N; i++ {
|
||||
noElideNonce = secp256k2.NonceRFC6979(secKey, msgHash, nil, nil, 0)
|
||||
noElideNonce = secp256k1.NonceRFC6979(secKey, msgHash, nil, nil, 0)
|
||||
}
|
||||
_ = noElideNonce
|
||||
}
|
||||
@@ -125,7 +124,7 @@ func BenchmarkNonceRFC6979(b *testing.B) {
|
||||
// signature for a message.
|
||||
func BenchmarkSignCompact(b *testing.B) {
|
||||
d := hexToModNScalar("9e0699c91ca1e3b7e3c9ba71eb71c89890872be97576010fe593fbf3fd57e66d")
|
||||
secKey := secp256k2.NewSecretKey(d)
|
||||
secKey := secp256k1.NewSecretKey(d)
|
||||
// blake256 of by{0x01, 0x02, 0x03, 0x04}.
|
||||
msgHash := hexToBytes("c301ba9de5d6053caad9f5eb46523f007702add2c62fa39de03146a36b8026b7")
|
||||
b.ReportAllocs()
|
||||
@@ -139,7 +138,7 @@ func BenchmarkSignCompact(b *testing.B) {
|
||||
// given a compact signature and message.
|
||||
func BenchmarkRecoverCompact(b *testing.B) {
|
||||
// Secret key: 9e0699c91ca1e3b7e3c9ba71eb71c89890872be97576010fe593fbf3fd57e66d
|
||||
wantPubKey := secp256k2.NewPublicKey(
|
||||
wantPubKey := secp256k1.NewPublicKey(
|
||||
hexToFieldVal("d2e670a19c6d753d1a6d8b20bd045df8a08fb162cf508956c31268c6d81ffdab"),
|
||||
hexToFieldVal("ab65528eefbb8057aa85d597258a3fbd481a24633bc9b47a9aa045c91371de52"),
|
||||
)
|
||||
@@ -7,7 +7,7 @@ package ecdsa
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
secp256k2 "orly.dev/crypto/ec/secp256k1"
|
||||
"orly.dev/pkg/crypto/ec/secp256k1"
|
||||
)
|
||||
|
||||
// References:
|
||||
@@ -27,9 +27,9 @@ var (
|
||||
// orderAsFieldVal is the order of the secp256k1 curve group stored as a
|
||||
// field value. It is provided here to avoid the need to create it multiple
|
||||
// times.
|
||||
orderAsFieldVal = func() secp256k2.FieldVal {
|
||||
var f secp256k2.FieldVal
|
||||
f.SetByteSlice(secp256k2.Params().N.Bytes())
|
||||
orderAsFieldVal = func() secp256k1.FieldVal {
|
||||
var f secp256k1.FieldVal
|
||||
f.SetByteSlice(secp256k1.Params().N.Bytes())
|
||||
return f
|
||||
}()
|
||||
)
|
||||
@@ -47,12 +47,12 @@ const (
|
||||
|
||||
// Signature is a type representing an ECDSA signature.
|
||||
type Signature struct {
|
||||
r secp256k2.ModNScalar
|
||||
s secp256k2.ModNScalar
|
||||
r secp256k1.ModNScalar
|
||||
s secp256k1.ModNScalar
|
||||
}
|
||||
|
||||
// NewSignature instantiates a new signature given some r and s values.
|
||||
func NewSignature(r, s *secp256k2.ModNScalar) *Signature {
|
||||
func NewSignature(r, s *secp256k1.ModNScalar) *Signature {
|
||||
return &Signature{*r, *s}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ func (sig *Signature) Serialize() []byte {
|
||||
// order of the group because both S and its negation are valid signatures
|
||||
// modulo the order, so this forces a consistent choice to reduce signature
|
||||
// malleability.
|
||||
sigS := new(secp256k2.ModNScalar).Set(&sig.s)
|
||||
sigS := new(secp256k1.ModNScalar).Set(&sig.s)
|
||||
if sigS.IsOverHalfOrder() {
|
||||
sigS.Negate()
|
||||
}
|
||||
@@ -135,27 +135,27 @@ func zeroArray32(b *[32]byte) {
|
||||
// Note that a bool is not used here because it is not possible in Go to convert
|
||||
// from a bool to numeric value in constant time and many constant-time
|
||||
// operations require a numeric value.
|
||||
func fieldToModNScalar(v *secp256k2.FieldVal) (secp256k2.ModNScalar, uint32) {
|
||||
func fieldToModNScalar(v *secp256k1.FieldVal) (secp256k1.ModNScalar, uint32) {
|
||||
var buf [32]byte
|
||||
v.PutBytes(&buf)
|
||||
var s secp256k2.ModNScalar
|
||||
var s secp256k1.ModNScalar
|
||||
overflow := s.SetBytes(&buf)
|
||||
zeroArray32(&buf)
|
||||
return s, overflow
|
||||
}
|
||||
|
||||
// modNScalarToField converts a scalar modulo the group order to a field value.
|
||||
func modNScalarToField(v *secp256k2.ModNScalar) secp256k2.FieldVal {
|
||||
func modNScalarToField(v *secp256k1.ModNScalar) secp256k1.FieldVal {
|
||||
var buf [32]byte
|
||||
v.PutBytes(&buf)
|
||||
var fv secp256k2.FieldVal
|
||||
var fv secp256k1.FieldVal
|
||||
fv.SetBytes(&buf)
|
||||
return fv
|
||||
}
|
||||
|
||||
// Verify returns whether the signature is valid for the provided hash
|
||||
// and secp256k1 public key.
|
||||
func (sig *Signature) Verify(hash []byte, pubKey *secp256k2.PublicKey) bool {
|
||||
func (sig *Signature) Verify(hash []byte, pubKey *secp256k1.PublicKey) bool {
|
||||
// The algorithm for verifying an ECDSA signature is given as algorithm 4.30
|
||||
// in [GECC].
|
||||
//
|
||||
@@ -221,26 +221,26 @@ func (sig *Signature) Verify(hash []byte, pubKey *secp256k2.PublicKey) bool {
|
||||
// Step 2.
|
||||
//
|
||||
// e = H(m)
|
||||
var e secp256k2.ModNScalar
|
||||
var e secp256k1.ModNScalar
|
||||
e.SetByteSlice(hash)
|
||||
// Step 3.
|
||||
//
|
||||
// w = S^-1 mod N
|
||||
w := new(secp256k2.ModNScalar).InverseValNonConst(&sig.s)
|
||||
w := new(secp256k1.ModNScalar).InverseValNonConst(&sig.s)
|
||||
// Step 4.
|
||||
//
|
||||
// u1 = e * w mod N
|
||||
// u2 = R * w mod N
|
||||
u1 := new(secp256k2.ModNScalar).Mul2(&e, w)
|
||||
u2 := new(secp256k2.ModNScalar).Mul2(&sig.r, w)
|
||||
u1 := new(secp256k1.ModNScalar).Mul2(&e, w)
|
||||
u2 := new(secp256k1.ModNScalar).Mul2(&sig.r, w)
|
||||
// Step 5.
|
||||
//
|
||||
// X = u1G + u2Q
|
||||
var X, Q, u1G, u2Q secp256k2.JacobianPoint
|
||||
var X, Q, u1G, u2Q secp256k1.JacobianPoint
|
||||
pubKey.AsJacobian(&Q)
|
||||
secp256k2.ScalarBaseMultNonConst(u1, &u1G)
|
||||
secp256k2.ScalarMultNonConst(u2, &Q, &u2Q)
|
||||
secp256k2.AddNonConst(&u1G, &u2Q, &X)
|
||||
secp256k1.ScalarBaseMultNonConst(u1, &u1G)
|
||||
secp256k1.ScalarMultNonConst(u2, &Q, &u2Q)
|
||||
secp256k1.AddNonConst(&u1G, &u2Q, &X)
|
||||
// Step 6.
|
||||
//
|
||||
// Fail if X is the point at infinity
|
||||
@@ -250,12 +250,12 @@ func (sig *Signature) Verify(hash []byte, pubKey *secp256k2.PublicKey) bool {
|
||||
// Step 7.
|
||||
//
|
||||
// z = (X.z)^2 mod P (X.z is the z coordinate of X)
|
||||
z := new(secp256k2.FieldVal).SquareVal(&X.Z)
|
||||
z := new(secp256k1.FieldVal).SquareVal(&X.Z)
|
||||
// Step 8.
|
||||
//
|
||||
// Verified if R * z == X.x (mod P)
|
||||
sigRModP := modNScalarToField(&sig.r)
|
||||
result := new(secp256k2.FieldVal).Mul2(&sigRModP, z).Normalize()
|
||||
result := new(secp256k1.FieldVal).Mul2(&sigRModP, z).Normalize()
|
||||
if result.Equals(&X.X) {
|
||||
return true
|
||||
}
|
||||
@@ -470,7 +470,7 @@ func ParseDERSignature(sig []byte) (*Signature, error) {
|
||||
// R must be in the range [1, N-1]. Notice the check for the maximum number
|
||||
// of bytes is required because SetByteSlice truncates as noted in its
|
||||
// comment so it could otherwise fail to detect the overflow.
|
||||
var r secp256k2.ModNScalar
|
||||
var r secp256k1.ModNScalar
|
||||
if len(rBytes) > 32 {
|
||||
str := "invalid signature: R is larger than 256 bits"
|
||||
return nil, signatureError(ErrSigRTooBig, str)
|
||||
@@ -491,7 +491,7 @@ func ParseDERSignature(sig []byte) (*Signature, error) {
|
||||
// S must be in the range [1, N-1]. Notice the check for the maximum number
|
||||
// of bytes is required because SetByteSlice truncates as noted in its
|
||||
// comment so it could otherwise fail to detect the overflow.
|
||||
var s secp256k2.ModNScalar
|
||||
var s secp256k1.ModNScalar
|
||||
if len(sBytes) > 32 {
|
||||
str := "invalid signature: S is larger than 256 bits"
|
||||
return nil, signatureError(ErrSigSTooBig, str)
|
||||
@@ -519,7 +519,7 @@ func ParseDERSignature(sig []byte) (*Signature, error) {
|
||||
// signing logic. It differs in that it accepts a nonce to use when signing and
|
||||
// may not successfully produce a valid signature for the given nonce. It is
|
||||
// primarily separated for testing purposes.
|
||||
func sign(secKey, nonce *secp256k2.ModNScalar, hash []byte) (
|
||||
func sign(secKey, nonce *secp256k1.ModNScalar, hash []byte) (
|
||||
*Signature, byte,
|
||||
bool,
|
||||
) {
|
||||
@@ -562,8 +562,8 @@ func sign(secKey, nonce *secp256k2.ModNScalar, hash []byte) (
|
||||
//
|
||||
// Note that the point must be in affine coordinates.
|
||||
k := nonce
|
||||
var kG secp256k2.JacobianPoint
|
||||
secp256k2.ScalarBaseMultNonConst(k, &kG)
|
||||
var kG secp256k1.JacobianPoint
|
||||
secp256k1.ScalarBaseMultNonConst(k, &kG)
|
||||
kG.ToAffine()
|
||||
// Step 3.
|
||||
//
|
||||
@@ -601,15 +601,15 @@ func sign(secKey, nonce *secp256k2.ModNScalar, hash []byte) (
|
||||
//
|
||||
// Note that this actually sets e = H(m) mod N which is correct since
|
||||
// it is only used in step 5 which itself is mod N.
|
||||
var e secp256k2.ModNScalar
|
||||
var e secp256k1.ModNScalar
|
||||
e.SetByteSlice(hash)
|
||||
// Step 5 with modification B.
|
||||
//
|
||||
// s = k^-1(e + dr) mod N
|
||||
// Repeat from step 1 if s = 0
|
||||
// s = -s if s > N/2
|
||||
kinv := new(secp256k2.ModNScalar).InverseValNonConst(k)
|
||||
s := new(secp256k2.ModNScalar).Mul2(secKey, &r).Add(&e).Mul(kinv)
|
||||
kinv := new(secp256k1.ModNScalar).InverseValNonConst(k)
|
||||
s := new(secp256k1.ModNScalar).Mul2(secKey, &r).Add(&e).Mul(kinv)
|
||||
if s.IsZero() {
|
||||
return nil, 0, false
|
||||
}
|
||||
@@ -630,7 +630,7 @@ func sign(secKey, nonce *secp256k2.ModNScalar, hash []byte) (
|
||||
// signRFC6979 generates a deterministic ECDSA signature according to RFC 6979
|
||||
// and BIP0062 and returns it along with an additional public key recovery code
|
||||
// for efficiently recovering the public key from the signature.
|
||||
func signRFC6979(secKey *secp256k2.SecretKey, hash []byte) (
|
||||
func signRFC6979(secKey *secp256k1.SecretKey, hash []byte) (
|
||||
*Signature,
|
||||
byte,
|
||||
) {
|
||||
@@ -673,7 +673,7 @@ func signRFC6979(secKey *secp256k2.SecretKey, hash []byte) (
|
||||
//
|
||||
// Generate a deterministic nonce in [1, N-1] parameterized by the
|
||||
// secret key, message being signed, and iteration count.
|
||||
k := secp256k2.NonceRFC6979(secKeyBytes[:], hash, nil, nil, iteration)
|
||||
k := secp256k1.NonceRFC6979(secKeyBytes[:], hash, nil, nil, iteration)
|
||||
// Steps 2-6.
|
||||
sig, pubKeyRecoveryCode, success := sign(secKeyScalar, k, hash)
|
||||
k.Zero()
|
||||
@@ -689,7 +689,7 @@ func signRFC6979(secKey *secp256k2.SecretKey, hash []byte) (
|
||||
// secret key. The produced signature is deterministic (same message and same
|
||||
// key yield the same signature) and canonical in accordance with RFC6979 and
|
||||
// BIP0062.
|
||||
func Sign(key *secp256k2.SecretKey, hash []byte) *Signature {
|
||||
func Sign(key *secp256k1.SecretKey, hash []byte) *Signature {
|
||||
signature, _ := signRFC6979(key, hash)
|
||||
return signature
|
||||
}
|
||||
@@ -730,7 +730,7 @@ const (
|
||||
// The compact sig recovery code is the value 27 + public key recovery code + 4
|
||||
// if the compact signature was created with a compressed public key.
|
||||
func SignCompact(
|
||||
key *secp256k2.SecretKey, hash []byte,
|
||||
key *secp256k1.SecretKey, hash []byte,
|
||||
isCompressedKey bool,
|
||||
) []byte {
|
||||
// Create the signature and associated pubkey recovery code and calculate
|
||||
@@ -753,7 +753,7 @@ func SignCompact(
|
||||
// the signature matches then the recovered public key will be returned as well
|
||||
// as a boolean indicating whether or not the original key was compressed.
|
||||
func RecoverCompact(signature, hash []byte) (
|
||||
*secp256k2.PublicKey, bool, error,
|
||||
*secp256k1.PublicKey, bool, error,
|
||||
) {
|
||||
// The following is very loosely based on the information and algorithm that
|
||||
// describes recovering a public key from and ECDSA signature in section
|
||||
@@ -850,7 +850,7 @@ func RecoverCompact(signature, hash []byte) (
|
||||
// Parse and validate the R and S signature components.
|
||||
//
|
||||
// Fail if r and s are not in [1, N-1].
|
||||
var r, s secp256k2.ModNScalar
|
||||
var r, s secp256k1.ModNScalar
|
||||
if overflow := r.SetByteSlice(signature[1:33]); overflow {
|
||||
str := "invalid signature: R >= group order"
|
||||
return nil, false, signatureError(ErrSigRTooBig, str)
|
||||
@@ -902,40 +902,40 @@ func RecoverCompact(signature, hash []byte) (
|
||||
// coord originally came from a random point on the curve which means there
|
||||
// must be a Y coord that satisfies the equation for a valid signature.
|
||||
oddY := pubKeyRecoveryCode&pubKeyRecoveryCodeOddnessBit != 0
|
||||
var y secp256k2.FieldVal
|
||||
if valid := secp256k2.DecompressY(&fieldR, oddY, &y); !valid {
|
||||
var y secp256k1.FieldVal
|
||||
if valid := secp256k1.DecompressY(&fieldR, oddY, &y); !valid {
|
||||
str := "invalid signature: not for a valid curve point"
|
||||
return nil, false, signatureError(ErrPointNotOnCurve, str)
|
||||
}
|
||||
// Step 5.
|
||||
//
|
||||
// X = (r, y)
|
||||
var X secp256k2.JacobianPoint
|
||||
var X secp256k1.JacobianPoint
|
||||
X.X.Set(fieldR.Normalize())
|
||||
X.Y.Set(y.Normalize())
|
||||
X.Z.SetInt(1)
|
||||
// Step 6.
|
||||
//
|
||||
// e = H(m) mod N
|
||||
var e secp256k2.ModNScalar
|
||||
var e secp256k1.ModNScalar
|
||||
e.SetByteSlice(hash)
|
||||
// Step 7.
|
||||
//
|
||||
// w = r^-1 mod N
|
||||
w := new(secp256k2.ModNScalar).InverseValNonConst(&r)
|
||||
w := new(secp256k1.ModNScalar).InverseValNonConst(&r)
|
||||
// Step 8.
|
||||
//
|
||||
// u1 = -(e * w) mod N
|
||||
// u2 = s * w mod N
|
||||
u1 := new(secp256k2.ModNScalar).Mul2(&e, w).Negate()
|
||||
u2 := new(secp256k2.ModNScalar).Mul2(&s, w)
|
||||
u1 := new(secp256k1.ModNScalar).Mul2(&e, w).Negate()
|
||||
u2 := new(secp256k1.ModNScalar).Mul2(&s, w)
|
||||
// Step 9.
|
||||
//
|
||||
// Q = u1G + u2X
|
||||
var Q, u1G, u2X secp256k2.JacobianPoint
|
||||
secp256k2.ScalarBaseMultNonConst(u1, &u1G)
|
||||
secp256k2.ScalarMultNonConst(u2, &X, &u2X)
|
||||
secp256k2.AddNonConst(&u1G, &u2X, &Q)
|
||||
var Q, u1G, u2X secp256k1.JacobianPoint
|
||||
secp256k1.ScalarBaseMultNonConst(u1, &u1G)
|
||||
secp256k1.ScalarMultNonConst(u2, &X, &u2X)
|
||||
secp256k1.AddNonConst(&u1G, &u2X, &Q)
|
||||
// Step 10.
|
||||
//
|
||||
// Fail if Q is the point at infinity.
|
||||
@@ -948,6 +948,6 @@ func RecoverCompact(signature, hash []byte) (
|
||||
}
|
||||
// Notice that the public key is in affine coordinates.
|
||||
Q.ToAffine()
|
||||
pubKey := secp256k2.NewPublicKey(&Q.X, &Q.Y)
|
||||
pubKey := secp256k1.NewPublicKey(&Q.X, &Q.Y)
|
||||
return pubKey, wasCompressed, nil
|
||||
}
|
||||
@@ -12,12 +12,11 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"math/rand"
|
||||
secp256k2 "orly.dev/crypto/ec/secp256k1"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/pkg/crypto/ec/secp256k1"
|
||||
"orly.dev/pkg/encoders/hex"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"orly.dev/encoders/hex"
|
||||
)
|
||||
|
||||
// hexToBytes converts the passed hex string into bytes and will panic if there
|
||||
@@ -321,8 +320,8 @@ func TestSignatureSerialize(t *testing.T) {
|
||||
}, {
|
||||
"zero signature",
|
||||
&Signature{
|
||||
r: *new(secp256k2.ModNScalar).SetInt(0),
|
||||
s: *new(secp256k2.ModNScalar).SetInt(0),
|
||||
r: *new(secp256k1.ModNScalar).SetInt(0),
|
||||
s: *new(secp256k1.ModNScalar).SetInt(0),
|
||||
},
|
||||
hexToBytes("3006020100020100"),
|
||||
},
|
||||
@@ -657,9 +656,9 @@ func TestSignAndVerifyRandom(t *testing.T) {
|
||||
if _, err := rng.Read(buf[:]); chk.T(err) {
|
||||
t.Fatalf("failed to read random secret key: %v", err)
|
||||
}
|
||||
var secKeyScalar secp256k2.ModNScalar
|
||||
var secKeyScalar secp256k1.ModNScalar
|
||||
secKeyScalar.SetBytes(&buf)
|
||||
secKey := secp256k2.NewSecretKey(&secKeyScalar)
|
||||
secKey := secp256k1.NewSecretKey(&secKeyScalar)
|
||||
// Generate a random hash to sign.
|
||||
var hash [32]byte
|
||||
if _, err := rng.Read(hash[:]); chk.T(err) {
|
||||
@@ -798,7 +797,7 @@ func TestVerifyFailures(t *testing.T) {
|
||||
s := hexToModNScalar(test.s)
|
||||
sig := NewSignature(r, s)
|
||||
// Ensure the verification is NOT successful.
|
||||
pubKey := secp256k2.NewSecretKey(secKey).PubKey()
|
||||
pubKey := secp256k1.NewSecretKey(secKey).PubKey()
|
||||
if sig.Verify(hash, pubKey) {
|
||||
t.Errorf(
|
||||
"%s: unexpected success for invalid signature: %x",
|
||||
@@ -1074,9 +1073,9 @@ func TestSignAndRecoverCompactRandom(t *testing.T) {
|
||||
if _, err := rng.Read(buf[:]); chk.T(err) {
|
||||
t.Fatalf("failed to read random secret key: %v", err)
|
||||
}
|
||||
var secKeyScalar secp256k2.ModNScalar
|
||||
var secKeyScalar secp256k1.ModNScalar
|
||||
secKeyScalar.SetBytes(&buf)
|
||||
secKey := secp256k2.NewSecretKey(&secKeyScalar)
|
||||
secKey := secp256k1.NewSecretKey(&secKeyScalar)
|
||||
wantPubKey := secKey.PubKey()
|
||||
// Generate a random hash to sign.
|
||||
var hash [32]byte
|
||||
@@ -1,7 +1,7 @@
|
||||
package ecdsa_test
|
||||
|
||||
import (
|
||||
"orly.dev/utils/lol"
|
||||
"orly.dev/pkg/utils/lol"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -4,7 +4,7 @@
|
||||
package btcec
|
||||
|
||||
import (
|
||||
"orly.dev/crypto/ec/secp256k1"
|
||||
"orly.dev/pkg/crypto/ec/secp256k1"
|
||||
)
|
||||
|
||||
// Error identifies an error related to public key cryptography using a
|
||||
@@ -1,7 +1,7 @@
|
||||
package btcec
|
||||
|
||||
import (
|
||||
"orly.dev/crypto/ec/secp256k1"
|
||||
"orly.dev/pkg/crypto/ec/secp256k1"
|
||||
)
|
||||
|
||||
// FieldVal implements optimized fixed-precision arithmetic over the secp256k1
|
||||
@@ -7,10 +7,9 @@ package btcec
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"orly.dev/utils/chk"
|
||||
"orly.dev/pkg/encoders/hex"
|
||||
"orly.dev/pkg/utils/chk"
|
||||
"testing"
|
||||
|
||||
"orly.dev/encoders/hex"
|
||||
)
|
||||
|
||||
// TestIsZero ensures that checking if a field IsZero works as expected.
|
||||
@@ -9,9 +9,8 @@
|
||||
package btcec
|
||||
|
||||
import (
|
||||
"orly.dev/pkg/encoders/hex"
|
||||
"testing"
|
||||
|
||||
"orly.dev/encoders/hex"
|
||||
)
|
||||
|
||||
func FuzzParsePubKey(f *testing.F) {
|
||||
@@ -4,7 +4,7 @@
|
||||
package btcec
|
||||
|
||||
import (
|
||||
secp256k2 "orly.dev/crypto/ec/secp256k1"
|
||||
"orly.dev/pkg/crypto/ec/secp256k1"
|
||||
)
|
||||
|
||||
// ModNScalar implements optimized 256-bit constant-time fixed-precision
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
// that should typically be avoided when possible as conversion to big.Ints
|
||||
// requires allocations, is not constant time, and is slower when working modulo
|
||||
// the group order.
|
||||
type ModNScalar = secp256k2.ModNScalar
|
||||
type ModNScalar = secp256k1.ModNScalar
|
||||
|
||||
// NonceRFC6979 generates a nonce deterministically according to RFC 6979 using
|
||||
// HMAC-SHA256 for the hashing function. It takes a 32-byte hash as an input
|
||||
@@ -43,7 +43,7 @@ func NonceRFC6979(
|
||||
extraIterations uint32,
|
||||
) *ModNScalar {
|
||||
|
||||
return secp256k2.NonceRFC6979(
|
||||
return secp256k1.NonceRFC6979(
|
||||
privKey, hash, extra, version,
|
||||
extraIterations,
|
||||
)
|
||||
@@ -6,11 +6,10 @@ package musig2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"orly.dev/crypto/ec"
|
||||
"orly.dev/crypto/ec/schnorr"
|
||||
"orly.dev/pkg/crypto/ec"
|
||||
"orly.dev/pkg/crypto/ec/schnorr"
|
||||
"orly.dev/pkg/encoders/hex"
|
||||
"testing"
|
||||
|
||||
"orly.dev/encoders/hex"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -247,7 +246,7 @@ func BenchmarkAggregateNonces(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
var testKey *btcec.PublicKey
|
||||
var testKey *btcec.btcec
|
||||
|
||||
// BenchmarkAggregateKeys benchmarks how long it takes to aggregate public
|
||||
// keys.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user