Compare commits

...

51 Commits

Author SHA1 Message Date
96eab2270d actually implement returning events, and cap at 512 2025-08-04 21:53:16 +01:00
c0bd7d8da3 skip init of relay secret key if there isn't one 2025-08-04 21:27:26 +01:00
1ffb7afb01 implement first draft of nwc client and cli 2025-08-04 14:54:15 +01:00
ffa9d85ba5 Fix error handling and logging in wallet service
- pkg/protocol/nwc/wallet_service.go
  - Added import for "orly.dev/pkg/utils/log"
  - Replaced fmt.Printf calls with log.E.F for consistent error logging
  - Simplified error handling using chk.E instead of explicit if err != nil checks
2025-08-03 11:39:19 +01:00
1223b1b20e implement nwc based on translation from js-sdk, refactor encryption to use bytes 2025-08-03 11:20:02 +01:00
deb56664e2 Update spider seeds and bump version to v0.4.6
- pkg/app/config/config.go
  - Updated default SpiderSeeds list with new relay URL
- pkg/version/version
  - Bumped version number from v0.4.5 to v0.4.6
2025-08-02 14:03:15 +01:00
1641d18993 Add IP blocking with authentication-based unblocking and offense tracking
- pkg/protocol/socketapi/handleAuth.go
  - Added import for "orly.dev/pkg/utils/iptracker"
  - Added logic to call iptracker.Global.Authenticate() on successful authentication
- pkg/protocol/openapi/event.go
  - Added logic to call iptracker.Global.Authenticate() on successful authentication
- pkg/utils/iptracker/iptracker.go
  - Introduced offense tracking and block duration calculation based on offense count
  - Added Authenticate method to remove blocks upon successful authentication
  - Modified isBlockedNoLock to always return true when an IP is blocked
  - Added HasBlockDurationPassed, GetBlockDuration, and Reset methods
- pkg/version/version
  - Bumped version number from v0.4.4 to v0.4.5
2025-08-02 12:30:47 +01:00
eab5d236db Add IP blocking based on failed authentication attempts
- pkg/protocol/socketapi/socketapi.go
  - Added import for "orly.dev/pkg/utils/iptracker"
  - Added logic to check if an IP is blocked and reject the connection if it is
- pkg/protocol/socketapi/handleEvent.go
  - Added imports for "orly.dev/pkg/utils/iptracker" and "time"
  - Added logic to check if an IP is blocked, send a notice to the client, and close the connection if it is
  - Added logic to record failed authentication attempts and block IPs that exceed the threshold
- pkg/protocol/openapi/event.go
  - Added imports for "orly.dev/pkg/utils/iptracker" and "time"
  - Added logic to check if an IP is blocked and return a forbidden error if it is
  - Added logic to record failed authentication attempts and return appropriate errors based on whether the IP is blocked or not
- pkg/utils/iptracker/iptracker.go
  - Created new package with functionality to track and block IPs based on failed authentication attempts
- pkg/version/version
  - Bumped version number from v0.4.3 to v0.4.4
2025-08-02 12:01:37 +01:00
f3e7188816 Bump version to v0.4.3
- pkg/version/version
  - Updated version number from v0.4.2 to v0.4.3
2025-08-02 11:44:05 +01:00
39957c2ebf implement correct handling when saving events to search for owner/peer deletes 2025-08-02 11:43:33 +01:00
4528d44fc7 Add owner check for deletion events
- pkg/protocol/socketapi/handleEvent.go
  - Added variable to track if the delete event's author is an owner
  - Modified checks to allow owners to delete events from other owners
  - Updated error messages for clarity and consistency

- bumped to version v0.4.2
2025-08-01 19:54:40 +01:00
7b19db5806 Remove redundant log statements and imports from spider-fetch.go
- pkg/app/relay/spider-fetch.go
  - Removed import of "orly.dev/pkg/utils/lol"
  - Removed log.I.S(ev) statement
  - Removed log.T.C(...) and log.I.F("saved event: %0x", ev.ID) statements
  - Removed redundant comments and whitespace
2025-08-01 09:35:06 +01:00
14d4417aec Bump version and add spider type configuration
pkg/version/version
- Updated version number from v0.4.0 to v0.4.1

pkg/app/config/config.go
- Added new config field `SpiderType` with default value "directory"

pkg/app/relay/peers.go
- Added check to skip empty addresses before processing peer information

pkg/app/relay/spider.go
- Modified spider fetch logic to conditionally execute based on spider type
- Added support for different kinds of events based on spider type
2025-08-01 08:52:22 +01:00
bdda37732c Remove redundant log statements from addEvent.go
- pkg/app/relay/addEvent.go
  - Removed log.I.S(pubkeys) statement
  - Removed log.I.F("sending to replica %s", a) statement
2025-07-31 22:37:34 +01:00
0024611179 Bump version to v0.4.0
- pkg/version/version
  - Updated version number from v0.3.0 to v0.4.0
2025-07-31 22:09:06 +01:00
699ba0554e Add event logging and replica handling improvements
- pkg/app/relay/server-publish.go
  - Added log statement for saved events
- pkg/app/relay/addEvent.go
  - Added support for multiple pubkeys
  - Modified logic to skip sending events back to replicas that already received them
  - Added header with pubkeys to prevent unnecessary resending
- pkg/protocol/openapi/event.go
  - Added parsing of X-Pubkeys header to avoid resending events
  - Updated AddEvent call to use pubkeys parameter
- pkg/protocol/httpauth/nip98auth.go
  - Removed log statement for nip-98 http auth event
- pkg/interfaces/server/server.go
  - Updated AddEvent method signature to accept pubkeys instead of pubkey
- pkg/protocol/httpauth/validate.go
  - Removed log statement for tolerance value
2025-07-31 22:08:25 +01:00
c62d685fa4 implement cluster replication
todo: need to add chain of senders in a header to a header to prevent unnecessary sends
2025-07-31 15:55:07 +01:00
6935575654 reduce the batch size to avoid missing things 2025-07-30 16:08:52 +01:00
80043b46b3 make the spider fetch everything of the last hour from all followeds 2025-07-30 16:05:55 +01:00
c68654dccc Fix documentation and code formatting issues
- cmd/lerproxy/README.md
  - Fixed grammar and punctuation in note about certificate selection
  - Improved clarity in instructions for appending intermediate certificates
  - Corrected wording in explanation of CLI tool issues with certificates

- cmd/lerproxy/app/reverse.go
  - Split long line for X-Forwarded-Host header comment to improve readability

- pkg/app/relay/server-publish.go
  - Reformatted comment block for function description to fit within line length limits
  - Added comments explaining why certain events aren't deleted from the database
  - don't delete any kind of directory events

- pkg/protocol/socketapi/handleReq.go
  - Split long lines for better readability in error message and log statements
  - Improved formatting of the notice envelope message

- pkg/app/config/config.go
  - Added new configuration fields for relay cluster replication authentication

- pkg/app/relay/publish/publisher.go
  - Removed redundant package imports and logging statements
2025-07-30 15:45:47 +01:00
72c6d16739 refactor lerproxy to be better laid out 2025-07-29 19:59:39 +01:00
366d35ec28 Refactor packages into main and remove redundant package declarations
- cmd/lerproxy/reverse/proxy.go
  - Changed package from 'reverse' to 'main'
  - Added standardized Forwarded header according to RFC 7239
  - Set X-Forwarded-* headers for backward compatibility
  - Updated URL path joining logic
- cmd/lerproxy/hsts/proxy.go
  - Changed package from 'hsts' to 'main'
  - Updated HSTS header setting
- cmd/lerproxy/timeout/conn.go
  - Changed package from 'timeout' to 'main'
- cmd/lerproxy/util/u.go
  - Changed package from 'util' to 'main'
- cmd/lerproxy/buf/bufpool.go
  - Changed package from 'buf' to 'main'
- cmd/lerproxy/main.go
  - Removed redundant package declarations and imports
  - Renamed struct and function names to follow camelCase convention
  - Updated server setup logic
- cmd/lerproxy/tcpkeepalive/listener.go
  - Changed package from 'tcpkeepalive' to 'main'
2025-07-29 19:00:41 +01:00
c36cec44c4 upgrade vainstr to actually using fast cgo, improve result format 2025-07-29 17:20:27 +01:00
c91a283520 implement fully working listener/subscribe/unsubscribe publisher 2025-07-28 05:05:56 +01:00
bb0693f455 Bump version to v0.2.20
- pkg/version/version
  - Bumped version from v0.2.19 to v0.2.20
2025-07-27 11:46:48 +01:00
0d7943be89 fixed bug with client message buffer causing overwriting of tag slices 2025-07-27 11:46:04 +01:00
978d9b88cd Refactor JSON marshaling with whitespace formatting in event encoder
- pkg/encoders/event/readwriter.go
  - Reformatted comment block for `MarshalWriteWithWhitespace` method
  - Removed redundant blank lines between code blocks
  - Improved alignment and spacing of comments
2025-07-25 21:37:35 +01:00
bbfb9b7300 Improve documentation for Events endpoint and update its description
- pkg/protocol/openapi/events.go
  - Updated the description of the Events HTTP API method to clarify that it uses a standard NIP-01 filter and returns events as a JSON array of event objects.
2025-07-25 20:54:13 +01:00
5b06906673 improve documentation and add events http endpoint for filter queries 2025-07-25 20:51:25 +01:00
f5c3da9bc3 Add systemd service file for lerproxy
- cmd/lerproxy/lerproxy.service
  - Created new file with systemd unit configuration to run lerproxy as a service
  - Defined [Unit], [Service], and [Install] sections with appropriate settings and dependencies
2025-07-25 17:46:47 +01:00
c608e1075b Remove authed flag and use authed pubkey instead
- pkg/protocol/ws/listener.go
  - Removed `isAuthed` field from Listener struct
  - Modified `IsAuthed()` method to check if `authedPubkey` is non-empty
  - Removed `SetAuthed()` method
- pkg/encoders/event/readwriter.go
  - Created new file with implementation of `MarshalWrite` and `UnmarshalRead` methods for event encoding/decoding
- pkg/protocol/openapi/events.go
  - Created new file with implementation of the Events HTTP API method
- pkg/protocol/socketapi/handleReq.go
  - Modified logging to display authed pubkey
  - Updated comment regarding authentication status
- pkg/encoders/bytesbuf/pool.go
  - Created new package for concurrent-safe buffer pool
- pkg/protocol/openapi/event.go
  - Removed import of `lol` package
  - Removed tracing logic around `RegisterEvent` function
- pkg/encoders/bytesbuf/pool_test.go
  - Created new test file for buffer pool implementation
2025-07-25 13:36:49 +01:00
5237fb1a1f Add notice envelope handling for relay access control
- pkg/protocol/socketapi/handleReq.go
  - Added import of "orly.dev/pkg/encoders/bech32encoding" and "orly.dev/pkg/encoders/envelopes/noticeenvelope"
  - Added logic to send a notice envelope when public read access is not allowed, listing owners' npub keys
- pkg/protocol/socketapi/handleEvent.go
  - Added import of "orly.dev/pkg/encoders/bech32encoding" and "orly.dev/pkg/encoders/envelopes/noticeenvelope"
  - Added logic to send a notice envelope when write access is not allowed, listing owners' npub keys
- pkg/version/version
  - Bumped version from v0.2.18 to v0.2.19
- pkg/interfaces/server/server.go
  - Added `OwnersPubkeys` method to return owners' public keys
- pkg/app/relay/owners-pubkeys.go
  - Created new file with implementation of `OwnersPubkeys` method for relay server
2025-07-24 09:31:11 +01:00
6901950059 Implement authentication handling in socket API event processing
- pkg/protocol/socketapi/handleAuth.go
  - Added comment block for handling pending events (currently commented out)
- pkg/protocol/ws/listener.go
  - Added import of "orly.dev/pkg/encoders/event"
  - Added `pendingEvent` field to Listener struct
  - Added `SetPendingEvent` and `GetPendingEvent` methods
- pkg/protocol/socketapi/socketapi.go
  - Added import of "orly.dev/pkg/encoders/envelopes/authenvelope"
  - Added authentication challenge logic including logging, RequestAuth call, and envelope writing
- pkg/protocol/socketapi/handleReq.go
  - Removed import of "orly.dev/pkg/encoders/envelopes/okenvelope"
  - Modified auth handling logic to send closed envelope with reason if auth is required and client is not authenticated
- pkg/protocol/socketapi/handleEvent.go
  - Removed import of "orly.dev/pkg/encoders/reason"
  - Added authentication challenge logic including logging, RequestAuth call, Ok.AuthRequired method, and envelope writing
- pkg/version/version
  - Bumped version from v0.2.17 to v0.2.18
2025-07-24 00:36:46 +01:00
251fc17933 Add authentication challenge handling in socket API event processing
- pkg/protocol/socketapi/handleEvent.go
  - Removed comment line
  - Added return statement to handle authentication challenge logic
2025-07-24 00:08:47 +01:00
fdb9e18b03 Add auth handling for relay and socket API
- pkg/app/relay/handleRelayinfo.go
  - Removed logging of info variable
- pkg/protocol/socketapi/socketapi.go
  - Removed import of "orly.dev/pkg/encoders/envelopes/authenvelope"
  - Removed authentication logic including challenge sending and logging
- pkg/app/relay/auth.go
  - Removed import of "orly.dev/pkg/utils/lol"
  - Removed tracing logic around ServiceURL function
- pkg/protocol/socketapi/handleReq.go
  - Added logging for auth status
  - Added logic to send authentication challenge if required and client is not authenticated
- pkg/protocol/socketapi/handleEvent.go
  - Added import of "orly.dev/pkg/encoders/envelopes/authenvelope" and "orly.dev/pkg/encoders/reason"
  - Added logging for auth status
  - Added logic to send authentication challenge if required and client is not authenticated
- pkg/version/version
  - Bumped version from v0.2.16 to v0.2.17
2025-07-24 00:04:26 +01:00
67552edf04 Bump version from v0.2.13 to v0.2.16
- pkg/version/version
  - Bumped version from v0.2.13 to v0.2.16
2025-07-23 23:25:56 +01:00
f25b760d84 Fix logging in socket API handling for close errors and privilege checks
- pkg/protocol/socketapi/socketapi.go
  - Added `message` parameter to log statement for unexpected close error
  - Updated format string to include message in log output
- pkg/protocol/socketapi/handleReq.go
  - Modified log message for not privileged event
  - Changed format string to be more descriptive and include client and event pubkeys
2025-07-23 23:24:09 +01:00
bfa38822e0 add event to openapi, properly implement relay accept event policy and auth with owners set 2025-07-23 23:10:39 +01:00
eac5e05e77 Add noVerify flag to SaveEvent to prevent duplicate events and enhance test reliability
- pkg/database/save-event.go
  - Added `noVerify` parameter to `SaveEvent` function
  - Added check for existing event using `GetSerialById` when `noVerify` is false
  - Modified logic to handle event verification based on `noVerify` flag
- pkg/app/relay/server-publish.go
  - Added `false` as third argument to `SaveEvent` calls
- pkg/database/export_test.go
  - Added `false` as third argument to `SaveEvent` call
- pkg/database/query-for-tags_test.go
  - Added `false` as third argument to `SaveEvent` call
- pkg/database/query-for-kinds-authors_test.go
  - Added `false` as third argument to `SaveEvent` call
- pkg/database/query-for-kinds-tags_test.go
  - Added `false` as third argument to `SaveEvent` call
- pkg/database/query-for-serials_test.go
  - Added `false` as third argument to `SaveEvent` call
- main.go
  - Modified pprof handling to support different profiling types (cpu, memory, allocation)
  - Changed `Pprof` configuration from boolean to string with enum values
- pkg/app/config/config.go
  - Changed `Pprof` field type from `bool` to `string` with enum values
- pkg/database/query-for-kinds-authors-tags_test.go
  - Added `false` as third argument to `SaveEvent` call
- pkg/version/version
  - Bumped version from v0.2.12 to v0.2.13
- pkg/database/fetch-event-by-serial_test.go
  - Added `false` as third argument to `SaveEvent` call
- pkg/database/query-for-kinds_test.go
  - Added `false` as third argument to `SaveEvent` call
- pkg/database/get-serials-by-range_test.go
  - Added `false` as third argument to `SaveEvent` call
- pkg/database/query-events-multiple-param-replaceable_test.go
  - Added `false` as third argument to `SaveEvent` calls
- pkg/database/query-events_test.go
  - Added `false` as third argument to `SaveEvent` calls
- pkg/interfaces/store/store_interface.go
  - Updated `Saver` interface to include `noVerify` parameter in `SaveEvent` method
  - Added `SerialByIder` interface with `GetSerialById` method
- pkg/database/save-event_test.go
  - Added `false` as third argument to `SaveEvent` calls
  - Added new test case for saving existing event
- pkg/database/query-for-ids_test.go
  - Added `false` as third argument to `SaveEvent` call
- pkg/protocol/ws/client.go
  - Changed comment about context cancellation from "context is canceled" to "context is cancelled"
- pkg/app/relay/spider-fetch.go
  - Added signature checker for WebSocket connections
  - Modified logic to check for existing events before saving
  - Added logging and memory optimization improvements
2025-07-23 13:51:02 +01:00
b72f2dd51e Bump version from v0.2.11 to v0.2.12 2025-07-23 05:58:25 +01:00
cc32703be0 Implement Import method for database and add OpenAPI import endpoint. 2025-07-23 05:45:07 +01:00
994d26bb09 Fix test to ensure binary marshaling/unmarshaling preserves JSON representation and add examples jsonl to .gitignore. 2025-07-23 04:32:47 +01:00
ea2d833e66 Bump version from v0.1.1 to v0.2.11 2025-07-23 04:11:26 +01:00
af04f89df8 Introduce ServeMux and OpenAPI export endpoint 2025-07-23 04:10:50 +01:00
fab2f104ff Refactor SpiderFetch to use IdPkTs with Kind and optimize memory usage 2025-07-23 01:49:46 +01:00
06940efcec make spider single threaded 2025-07-23 01:11:13 +01:00
0ba36a3f67 Add memory optimization by freeing OS memory after event processing 2025-07-23 00:59:25 +01:00
d4bee83992 Add check for value log GC and sync storage after event processing 2025-07-23 00:56:58 +01:00
aabb536d13 Refactor event handling to use IdPkTs for memory optimization and improve query efficiency. 2025-07-23 00:48:32 +01:00
498073460c skip nil FullIdPubkeyBySerial results 2025-07-22 15:15:03 +01:00
11d378bfc3 revert changes that cause malfunction 2025-07-22 15:08:50 +01:00
154 changed files with 18619 additions and 1629 deletions

3
.gitignore vendored
View File

@@ -85,6 +85,7 @@ node_modules/**
!.name
!.gitignore
!version
!out.jsonl
# ...even if they are in subdirectories
!*/
/blocklist.json
@@ -102,3 +103,5 @@ pkg/database/testrealy
/.idea/codeStyles/codeStyleConfig.xml
/.idea/material_theme_project_new.xml
/.idea/orly.iml
/.idea/go.imports.xml
/.idea/inspectionProfiles/Project_Default.xml

View File

@@ -56,17 +56,17 @@ as:
extensions and become active in place of the LetsEncrypt certificates
> Note that the match is greedy, so you can explicitly separately give a subdomain
certificate and it will be selected even if there is a wildcard that also matches.
certificate, and it will be selected even if there is a wildcard that also matches.
# IMPORTANT
With Comodo SSL (sectigo RSA) certificates you also need to append the intermediate certificate
to the `.crt` file in order to get it to work properly with openssl library based tools like
With Comodo SSL (sectigo RSA) certificates you also need to append the intermediate certificate
to the `.crt` file to get it to work properly with openssl library based tools like
wget, curl and the go tool, which is quite important if you want to do subdomains on a wildcard
certificate.
Probably the same applies to some of the other certificate authorities. If you sometimes get
issues with CLI tools refusing to accept these certificates on your web server or other, this
Probably the same applies to some of the other certificate authorities. If you sometimes get
issues with CLI tools refusing to accept these certificates on your web server or other, this
may be the problem.
## example mapping.txt

104
cmd/lerproxy/app/app.go Normal file
View File

@@ -0,0 +1,104 @@
package app
import (
"golang.org/x/sync/errgroup"
"net"
"net/http"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/log"
"time"
)
type RunArgs struct {
Addr string `arg:"-l,--listen" default:":https" help:"address to listen at"`
Conf string `arg:"-m,--map" default:"mapping.txt" help:"file with host/backend mapping"`
Cache string `arg:"-c,--cachedir" default:"/var/cache/letsencrypt" help:"path to directory to cache key and certificates"`
HSTS bool `arg:"-h,--hsts" help:"add Strict-Transport-Security header"`
Email string `arg:"-e,--email" help:"contact email address presented to letsencrypt CA"`
HTTP string `arg:"--http" default:":http" help:"optional address to serve http-to-https redirects and ACME http-01 challenge responses"`
RTO time.Duration `arg:"-r,--rto" default:"1m" help:"maximum duration before timing out read of the request"`
WTO time.Duration `arg:"-w,--wto" default:"5m" help:"maximum duration before timing out write of the response"`
Idle time.Duration `arg:"-i,--idle" help:"how long idle connection is kept before closing (set rto, wto to 0 to use this)"`
Certs []string `arg:"--cert,separate" help:"certificates and the domain they match: eg: orly.dev:/path/to/cert - this will indicate to load two, one with extension .key and one with .crt, each expected to be PEM encoded TLS private and public keys, respectively"`
// Rewrites string `arg:"-r,--rewrites" default:"rewrites.txt"`
}
func Run(c context.T, args RunArgs) (err error) {
if args.Cache == "" {
err = log.E.Err("no cache specified")
return
}
var srv *http.Server
var httpHandler http.Handler
if srv, httpHandler, err = SetupServer(args); chk.E(err) {
return
}
srv.ReadHeaderTimeout = 5 * time.Second
if args.RTO > 0 {
srv.ReadTimeout = args.RTO
}
if args.WTO > 0 {
srv.WriteTimeout = args.WTO
}
group, ctx := errgroup.WithContext(c)
if args.HTTP != "" {
httpServer := http.Server{
Addr: args.HTTP,
Handler: httpHandler,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
group.Go(
func() (err error) {
chk.E(httpServer.ListenAndServe())
return
},
)
group.Go(
func() error {
<-ctx.Done()
ctx, cancel := context.Timeout(
context.Bg(),
time.Second,
)
defer cancel()
return httpServer.Shutdown(ctx)
},
)
}
if srv.ReadTimeout != 0 || srv.WriteTimeout != 0 || args.Idle == 0 {
group.Go(
func() (err error) {
chk.E(srv.ListenAndServeTLS("", ""))
return
},
)
} else {
group.Go(
func() (err error) {
var ln net.Listener
if ln, err = net.Listen("tcp", srv.Addr); chk.E(err) {
return
}
defer ln.Close()
ln = Listener{
Duration: args.Idle,
TCPListener: ln.(*net.TCPListener),
}
err = srv.ServeTLS(ln, "", "")
chk.E(err)
return
},
)
}
group.Go(
func() error {
<-ctx.Done()
ctx, cancel := context.Timeout(context.Bg(), time.Second)
defer cancel()
return srv.Shutdown(ctx)
},
)
return group.Wait()
}

View File

@@ -1,5 +1,4 @@
// Package buf implements a simple concurrent safe buffer pool for raw bytes.
package buf
package app
import "sync"

View File

@@ -1,6 +1,4 @@
// Package timeout provides a simple extension of a net.TCPConn with a
// configurable read/write deadline.
package timeout
package app
import (
"net"

View File

@@ -0,0 +1,63 @@
package app
import (
"fmt"
"net/http"
"orly.dev/pkg/utils/log"
"strings"
)
// GoVanity configures an HTTP handler for redirecting requests to vanity URLs
// based on the provided hostname and backend address.
//
// # Parameters
//
// - hn (string): The hostname associated with the vanity URL.
//
// - ba (string): The backend address, expected to be in the format
// "git+<repository-path>".
//
// - mux (*http.ServeMux): The HTTP serve multiplexer where the handler will be
// registered.
//
// # Expected behaviour
//
// - Splits the backend address to extract the repository path from the "git+" prefix.
//
// - If the split fails, logs an error and returns without registering a handler.
//
// - Generates an HTML redirect page containing metadata for Go import and
// redirects to the extracted repository path.
//
// - Registers a handler on the provided ServeMux that serves this redirect page
// when requests are made to the specified hostname.
func GoVanity(hn, ba string, mux *http.ServeMux) {
split := strings.Split(ba, "git+")
if len(split) != 2 {
log.E.Ln("invalid go vanity redirect: %s: %s", hn, ba)
return
}
redirector := fmt.Sprintf(
`<html><head><meta name="go-import" content="%s git %s"/><meta http-equiv = "refresh" content = " 3 ; url = %s"/></head><body>redirecting to <a href="%s">%s</a></body></html>`,
hn, split[1], split[1], split[1], split[1],
)
mux.HandleFunc(
hn+"/",
func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set(
"Access-Control-Allow-Methods",
"GET,HEAD,PUT,PATCH,POST,DELETE",
)
writer.Header().Set("Access-Control-Allow-Origin", "*")
writer.Header().Set("Content-Type", "text/html")
writer.Header().Set(
"Content-Length", fmt.Sprint(len(redirector)),
)
writer.Header().Set(
"strict-transport-security",
"max-age=0; includeSubDomains",
)
fmt.Fprint(writer, redirector)
},
)
}

View File

@@ -1,20 +1,17 @@
// Package tcpkeepalive implements a net.TCPListener with a singleton set period
// for a default 3 minute keep-aline.
package tcpkeepalive
package app
import (
"net"
"orly.dev/cmd/lerproxy/timeout"
"orly.dev/pkg/utils/chk"
"time"
)
// Period can be changed prior to opening a Listener to alter its'
// Period can be changed before opening a Listener to alter its
// KeepAlivePeriod.
var Period = 3 * time.Minute
// Listener sets TCP keep-alive timeouts on accepted connections.
// It's used by ListenAndServe and ListenAndServeTLS so dead TCP connections
// It is used by ListenAndServe and ListenAndServeTLS so dead TCP connections
// (e.g. closing laptop mid-download) eventually go away.
type Listener struct {
time.Duration
@@ -33,7 +30,7 @@ func (ln Listener) Accept() (conn net.Conn, e error) {
return
}
if ln.Duration != 0 {
return timeout.Conn{Duration: ln.Duration, TCPConn: tc}, nil
return Conn{Duration: ln.Duration, TCPConn: tc}, nil
}
return tc, nil
}

View File

@@ -0,0 +1,80 @@
package app
import (
"encoding/json"
"fmt"
"net/http"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/log"
"os"
)
type NostrJSON struct {
Names map[string]string `json:"names"`
Relays map[string][]string `json:"relays"`
}
// NostrDNS handles the configuration and registration of a Nostr DNS endpoint
// for a given hostname and backend address.
//
// # Parameters
//
// - hn (string): The hostname for which the Nostr DNS entry is being configured.
//
// - ba (string): The path to the JSON file containing the Nostr DNS data.
//
// - mux (*http.ServeMux): The HTTP serve multiplexer to which the Nostr DNS
// handler will be registered.
//
// # Return Values
//
// - err (error): An error if any step fails during the configuration or
// registration process.
//
// # Expected behaviour
//
// - Reads the JSON file specified by `ba` and parses its contents into a
// NostrJSON struct.
//
// - Registers a new HTTP handler on the provided `mux` for the
// `.well-known/nostr.json` endpoint under the specified hostname.
//
// - The handler serves the parsed Nostr DNS data with appropriate HTTP headers
// set for CORS and content type.
func NostrDNS(hn, ba string, mux *http.ServeMux) (err error) {
log.T.Ln(hn, ba)
var fb []byte
if fb, err = os.ReadFile(ba); chk.E(err) {
return
}
var v NostrJSON
if err = json.Unmarshal(fb, &v); chk.E(err) {
return
}
var jb []byte
if jb, err = json.Marshal(v); chk.E(err) {
return
}
nostrJSON := string(jb)
mux.HandleFunc(
hn+"/.well-known/nostr.json",
func(writer http.ResponseWriter, request *http.Request) {
log.T.Ln("serving nostr json to", hn)
writer.Header().Set(
"Access-Control-Allow-Methods",
"GET,HEAD,PUT,PATCH,POST,DELETE",
)
writer.Header().Set("Access-Control-Allow-Origin", "*")
writer.Header().Set("Content-Type", "application/json")
writer.Header().Set(
"Content-Length", fmt.Sprint(len(nostrJSON)),
)
writer.Header().Set(
"strict-transport-security",
"max-age=0; includeSubDomains",
)
fmt.Fprint(writer, nostrJSON)
},
)
return
}

15
cmd/lerproxy/app/proxy.go Normal file
View File

@@ -0,0 +1,15 @@
package app
import "net/http"
type Proxy struct {
http.Handler
}
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload",
)
p.Handler.ServeHTTP(w, r)
}

View File

@@ -0,0 +1,62 @@
package app
import (
"bufio"
"fmt"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/log"
"os"
"strings"
)
// ReadMapping reads a mapping file and returns a map of hostnames to backend
// addresses.
//
// # Parameters
//
// - file (string): The path to the mapping file to read.
//
// # Return Values
//
// - m (map[string]string): A map containing the hostname to backend address
// mappings parsed from the file.
//
// - err (error): An error if any step during reading or parsing fails.
//
// # Expected behaviour
//
// - Opens the specified file and reads its contents line by line.
//
// - Skips lines that are empty or start with a '#'.
//
// - Splits each valid line into two parts using the first colon as the
// separator.
//
// - Trims whitespace from both parts and adds them to the map.
//
// - Returns any error encountered during file operations or parsing.
func ReadMapping(file string) (m map[string]string, err error) {
var f *os.File
if f, err = os.Open(file); chk.E(err) {
return
}
m = make(map[string]string)
sc := bufio.NewScanner(f)
for sc.Scan() {
if b := sc.Bytes(); len(b) == 0 || b[0] == '#' {
continue
}
s := strings.SplitN(sc.Text(), ":", 2)
if len(s) != 2 {
err = fmt.Errorf("invalid line: %q", sc.Text())
log.E.Ln(err)
chk.E(f.Close())
return
}
m[strings.TrimSpace(s[0])] = strings.TrimSpace(s[1])
}
err = sc.Err()
chk.E(err)
chk.E(f.Close())
return
}

View File

@@ -0,0 +1,63 @@
package app
import (
"net/http"
"net/http/httputil"
"net/url"
"orly.dev/cmd/lerproxy/utils"
"orly.dev/pkg/utils/log"
)
// NewSingleHostReverseProxy is a copy of httputil.NewSingleHostReverseProxy
// with the addition of forwarding headers:
//
// - Legacy X-Forwarded-* headers (X-Forwarded-Proto, X-Forwarded-For,
// X-Forwarded-Host)
//
// - Standardized Forwarded header according to RFC 7239
// (https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Forwarded)
func NewSingleHostReverseProxy(target *url.URL) (rp *httputil.ReverseProxy) {
targetQuery := target.RawQuery
director := func(req *http.Request) {
log.D.S(req)
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = utils.SingleJoiningSlash(target.Path, req.URL.Path)
if targetQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = targetQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
}
if _, ok := req.Header["User-Agent"]; !ok {
req.Header.Set("User-Agent", "")
}
// Set X-Forwarded-* headers for backward compatibility
req.Header.Set("X-Forwarded-Proto", "https")
// Get client IP address
clientIP := req.RemoteAddr
if fwdFor := req.Header.Get("X-Forwarded-For"); fwdFor != "" {
clientIP = fwdFor + ", " + clientIP
}
req.Header.Set("X-Forwarded-For", clientIP)
// Set X-Forwarded-Host if not already set
if _, exists := req.Header["X-Forwarded-Host"]; !exists {
req.Header.Set("X-Forwarded-Host", req.Host)
}
// Set standardized Forwarded header according to RFC 7239
// Format: Forwarded: by=<identifier>;for=<identifier>;host=<host>;proto=<http|https>
forwardedProto := "https"
forwardedHost := req.Host
forwardedFor := clientIP
// Build the Forwarded header value
forwardedHeader := "proto=" + forwardedProto
if forwardedFor != "" {
forwardedHeader += ";for=" + forwardedFor
}
if forwardedHost != "" {
forwardedHeader += ";host=" + forwardedHost
}
req.Header.Set("Forwarded", forwardedHeader)
}
rp = &httputil.ReverseProxy{Director: director}
return
}

View File

@@ -0,0 +1,124 @@
package app
import (
"fmt"
"io"
log2 "log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/log"
"os"
"path/filepath"
"runtime"
"strings"
"time"
)
// SetProxy creates an HTTP handler that routes incoming requests to specified
// backend addresses based on hostname mappings.
//
// # Parameters
//
// - mapping (map[string]string): A map where keys are hostnames and values are
// the corresponding backend addresses.
//
// # Return Values
//
// - h (http.Handler): The HTTP handler configured with the proxy settings.
// - err (error): An error if the mapping is empty or invalid.
//
// # Expected behaviour
//
// - Validates that the provided hostname to backend address mapping is not empty.
//
// - Creates a new ServeMux and configures it to route requests based on the
// specified hostnames and backend addresses.
//
// - Handles special cases such as vanity URLs, Nostr DNS entries, and Unix
// socket connections.
func SetProxy(mapping map[string]string) (h http.Handler, err error) {
if len(mapping) == 0 {
return nil, fmt.Errorf("empty mapping")
}
mux := http.NewServeMux()
for hostname, backendAddr := range mapping {
hn, ba := hostname, backendAddr
if strings.ContainsRune(hn, os.PathSeparator) {
err = log.E.Err("invalid hostname: %q", hn)
return
}
network := "tcp"
if ba != "" && ba[0] == '@' && runtime.GOOS == "linux" {
// append \0 to address so addrlen for connect(2) is calculated in a
// way compatible with some other implementations (i.e. uwsgi)
network, ba = "unix", ba+string(byte(0))
} else if strings.HasPrefix(ba, "git+") {
GoVanity(hn, ba, mux)
continue
} else if filepath.IsAbs(ba) {
network = "unix"
switch {
case strings.HasSuffix(ba, string(os.PathSeparator)):
// path specified as directory with explicit trailing slash; add
// this path as static site
fs := http.FileServer(http.Dir(ba))
mux.Handle(hn+"/", fs)
continue
case strings.HasSuffix(ba, "nostr.json"):
if err = NostrDNS(hn, ba, mux); err != nil {
continue
}
continue
}
} else if u, err := url.Parse(ba); err == nil {
switch u.Scheme {
case "http", "https":
rp := NewSingleHostReverseProxy(u)
modifyCORSResponse := func(res *http.Response) error {
res.Header.Set(
"Access-Control-Allow-Methods",
"GET,HEAD,PUT,PATCH,POST,DELETE",
)
// res.Header.Set("Access-Control-Allow-Credentials", "true")
res.Header.Set("Access-Control-Allow-Origin", "*")
return nil
}
rp.ModifyResponse = modifyCORSResponse
rp.ErrorLog = log2.New(
os.Stderr, "lerproxy", log2.Llongfile,
)
rp.BufferPool = Pool{}
mux.Handle(hn+"/", rp)
continue
}
}
rp := &httputil.ReverseProxy{
Director: func(req *http.Request) {
req.URL.Scheme = "http"
req.URL.Host = req.Host
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-For", req.RemoteAddr)
req.Header.Set(
"Access-Control-Allow-Methods",
"GET,HEAD,PUT,PATCH,POST,DELETE",
)
req.Header.Set("Access-Control-Allow-Origin", "*")
log.D.Ln(req.URL, req.RemoteAddr)
},
Transport: &http.Transport{
DialContext: func(c context.T, n, addr string) (
net.Conn, error,
) {
return net.DialTimeout(network, ba, 5*time.Second)
},
},
ErrorLog: log2.New(io.Discard, "", 0),
BufferPool: Pool{},
}
mux.Handle(hn+"/", rp)
}
return mux, nil
}

View File

@@ -0,0 +1,81 @@
package app
import (
"fmt"
"golang.org/x/crypto/acme/autocert"
"net/http"
"orly.dev/cmd/lerproxy/utils"
"orly.dev/pkg/utils/chk"
"os"
)
// SetupServer configures and returns an HTTP server instance with proxy
// handling and automatic certificate management based on the provided RunArgs
// configuration.
//
// # Parameters
//
// - a (RunArgs): The configuration arguments containing settings for the server
// address, cache directory, mapping file, HSTS header, email, and certificates.
//
// # Return Values
//
// - s (*http.Server): The configured HTTP server instance.
//
// - h (http.Handler): The HTTP handler used for proxying requests and managing
// automatic certificate challenges.
//
// - err (error): An error if any step during setup fails.
//
// # Expected behaviour
//
// - Reads the hostname to backend address mapping from the specified
// configuration file.
//
// - Sets up a proxy handler that routes incoming requests based on the defined
// mappings.
//
// - Enables HSTS header support if enabled in the RunArgs.
//
// - Creates the cache directory for storing certificates and keys if it does not
// already exist.
//
// - Configures an autocert.Manager to handle automatic certificate management,
// including hostname whitelisting, email contact, and cache storage.
//
// - Initializes the HTTP server with proxy handler, address, and TLS
// configuration.
func SetupServer(a RunArgs) (s *http.Server, h http.Handler, err error) {
var mapping map[string]string
if mapping, err = ReadMapping(a.Conf); chk.E(err) {
return
}
var proxy http.Handler
if proxy, err = SetProxy(mapping); chk.E(err) {
return
}
if a.HSTS {
proxy = &Proxy{Handler: proxy}
}
if err = os.MkdirAll(a.Cache, 0700); chk.E(err) {
err = fmt.Errorf(
"cannot create cache directory %q: %v",
a.Cache, err,
)
chk.E(err)
return
}
m := autocert.Manager{
Prompt: autocert.AcceptTOS,
Cache: autocert.DirCache(a.Cache),
HostPolicy: autocert.HostWhitelist(utils.GetKeys(mapping)...),
Email: a.Email,
}
s = &http.Server{
Handler: proxy,
Addr: a.Addr,
TLSConfig: TLSConfig(&m, a.Certs...),
}
h = m.HTTPHandler(nil)
return
}

View File

@@ -0,0 +1,87 @@
package app
import (
"crypto/tls"
"golang.org/x/crypto/acme/autocert"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/log"
"strings"
"sync"
)
// TLSConfig creates a custom TLS configuration that combines automatic
// certificate management with explicitly provided certificates.
//
// # Parameters
//
// - m (*autocert.Manager): The autocert manager used for managing automatic
// certificate generation and retrieval.
//
// - certs (...string): A variadic list of certificate definitions in the format
// "domain:/path/to/cert", where each domain maps to a certificate file. The
// corresponding key file is expected to be at "/path/to/cert.key".
//
// # Return Values
//
// - tc (*tls.Config): A new TLS configuration that prioritises explicitly
// provided certificates over automatically generated ones.
//
// # Expected behaviour
//
// - Loads all explicitly provided certificates and maps them to their
// respective domains.
//
// - Creates a custom GetCertificate function that checks if the requested
// domain matches any of the explicitly provided certificates, returning those
// first.
//
// - Falls back to the autocert manager's GetCertificate method if no explicit
// certificate is found for the requested domain.
func TLSConfig(m *autocert.Manager, certs ...string) (tc *tls.Config) {
certMap := make(map[string]*tls.Certificate)
var mx sync.Mutex
for _, cert := range certs {
split := strings.Split(cert, ":")
if len(split) != 2 {
log.E.F("invalid certificate parameter format: `%s`", cert)
continue
}
var err error
var c tls.Certificate
if c, err = tls.LoadX509KeyPair(
split[1]+".crt", split[1]+".key",
); chk.E(err) {
continue
}
certMap[split[0]] = &c
}
tc = m.TLSConfig()
tc.GetCertificate = func(helo *tls.ClientHelloInfo) (
cert *tls.Certificate, err error,
) {
mx.Lock()
var own string
for i := range certMap {
// to also handle explicit subdomain certs, prioritize over a root
// wildcard.
if helo.ServerName == i {
own = i
break
}
// if it got to us and ends in the same-name dot tld assume the
// subdomain was redirected, or it is a wildcard certificate; thus
// only the ending needs to match.
if strings.HasSuffix(helo.ServerName, i) {
own = i
break
}
}
if own != "" {
defer mx.Unlock()
return certMap[own], nil
}
mx.Unlock()
return m.GetCertificate(helo)
}
return
}

View File

@@ -1,15 +0,0 @@
// Package hsts implements a HTTP handler that enforces HSTS.
package hsts
import "net/http"
type Proxy struct {
http.Handler
}
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().
Set("Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload")
p.ServeHTTP(w, r)
}

View File

@@ -0,0 +1,16 @@
# systemd unit to run lerproxy as a service
[Unit]
Description=lerproxy
[Service]
Type=simple
User=mleku
ExecStart=/home/mleku/.local/bin/lerproxy -m /home/mleku/mapping.txt
Restart=always
Wants=network-online.target
# waits for wireguard service to come up before starting, remove the wg-quick@wg0 section if running it directly on an
# internet routeable connection
After=network.target network-online.target wg-quick@wg0.service
[Install]
WantedBy=multi-user.target

View File

@@ -1,402 +1,23 @@
// Command lerproxy implements https reverse proxy with automatic LetsEncrypt
// usage for multiple hostnames/backends,your own SSL certificates, nostr NIP-05
// DNS verification hosting and Go vanity redirects.
package main
import (
"bufio"
"crypto/tls"
"encoding/json"
"fmt"
"io"
stdLog "log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"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/cmd/lerproxy/app"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/log"
"os"
"os/signal"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/alexflint/go-arg"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/sync/errgroup"
)
type runArgs struct {
Addr string `arg:"-l,--listen" default:":https" help:"address to listen at"`
Conf string `arg:"-m,--map" default:"mapping.txt" help:"file with host/backend mapping"`
Cache string `arg:"-c,--cachedir" default:"/var/cache/letsencrypt" help:"path to directory to cache key and certificates"`
HSTS bool `arg:"-h,--hsts" help:"add Strict-Transport-Security header"`
Email string `arg:"-e,--email" help:"contact email address presented to letsencrypt CA"`
HTTP string `arg:"--http" default:":http" help:"optional address to serve http-to-https redirects and ACME http-01 challenge responses"`
RTO time.Duration `arg:"-r,--rto" default:"1m" help:"maximum duration before timing out read of the request"`
WTO time.Duration `arg:"-w,--wto" default:"5m" help:"maximum duration before timing out write of the response"`
Idle time.Duration `arg:"-i,--idle" help:"how long idle connection is kept before closing (set rto, wto to 0 to use this)"`
Certs []string `arg:"--cert,separate" help:"certificates and the domain they match: eg: orly.dev:/path/to/cert - this will indicate to load two, one with extension .key and one with .crt, each expected to be PEM encoded TLS private and public keys, respectively"`
// Rewrites string `arg:"-r,--rewrites" default:"rewrites.txt"`
}
var args runArgs
var args app.RunArgs
func main() {
arg.MustParse(&args)
ctx, cancel := signal.NotifyContext(context.Bg(), os.Interrupt)
defer cancel()
if err := run(ctx, args); chk.T(err) {
if err := app.Run(ctx, args); chk.T(err) {
log.F.Ln(err)
}
}
func run(c context.T, args runArgs) (err error) {
if args.Cache == "" {
err = log.E.Err("no cache specified")
return
}
var srv *http.Server
var httpHandler http.Handler
if srv, httpHandler, err = setupServer(args); chk.E(err) {
return
}
srv.ReadHeaderTimeout = 5 * time.Second
if args.RTO > 0 {
srv.ReadTimeout = args.RTO
}
if args.WTO > 0 {
srv.WriteTimeout = args.WTO
}
group, ctx := errgroup.WithContext(c)
if args.HTTP != "" {
httpServer := http.Server{
Addr: args.HTTP,
Handler: httpHandler,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
group.Go(
func() (err error) {
chk.E(httpServer.ListenAndServe())
return
},
)
group.Go(
func() error {
<-ctx.Done()
ctx, cancel := context.Timeout(
context.Bg(),
time.Second,
)
defer cancel()
return httpServer.Shutdown(ctx)
},
)
}
if srv.ReadTimeout != 0 || srv.WriteTimeout != 0 || args.Idle == 0 {
group.Go(
func() (err error) {
chk.E(srv.ListenAndServeTLS("", ""))
return
},
)
} else {
group.Go(
func() (err error) {
var ln net.Listener
if ln, err = net.Listen("tcp", srv.Addr); chk.E(err) {
return
}
defer ln.Close()
ln = tcpkeepalive.Listener{
Duration: args.Idle,
TCPListener: ln.(*net.TCPListener),
}
err = srv.ServeTLS(ln, "", "")
chk.E(err)
return
},
)
}
group.Go(
func() error {
<-ctx.Done()
ctx, cancel := context.Timeout(context.Bg(), time.Second)
defer cancel()
return srv.Shutdown(ctx)
},
)
return group.Wait()
}
// TLSConfig returns a TLSConfig that works with a LetsEncrypt automatic SSL cert issuer as well
// as any provided .pem certificates from providers.
//
// The certs are provided in the form "example.com:/path/to/cert.pem"
func TLSConfig(m *autocert.Manager, certs ...string) (tc *tls.Config) {
certMap := make(map[string]*tls.Certificate)
var mx sync.Mutex
for _, cert := range certs {
split := strings.Split(cert, ":")
if len(split) != 2 {
log.E.F("invalid certificate parameter format: `%s`", cert)
continue
}
var err error
var c tls.Certificate
if c, err = tls.LoadX509KeyPair(
split[1]+".crt", split[1]+".key",
); chk.E(err) {
continue
}
certMap[split[0]] = &c
}
tc = m.TLSConfig()
tc.GetCertificate = func(helo *tls.ClientHelloInfo) (
cert *tls.Certificate, err error,
) {
mx.Lock()
var own string
for i := range certMap {
// to also handle explicit subdomain certs, prioritize over a root wildcard.
if helo.ServerName == i {
own = i
break
}
// if it got to us and ends in the same name dot tld assume the subdomain was
// redirected or it's a wildcard certificate, thus only the ending needs to match.
if strings.HasSuffix(helo.ServerName, i) {
own = i
break
}
}
if own != "" {
defer mx.Unlock()
return certMap[own], nil
}
mx.Unlock()
return m.GetCertificate(helo)
}
return
}
func setupServer(a runArgs) (s *http.Server, h http.Handler, err error) {
var mapping map[string]string
if mapping, err = readMapping(a.Conf); chk.E(err) {
return
}
var proxy http.Handler
if proxy, err = setProxy(mapping); chk.E(err) {
return
}
if a.HSTS {
proxy = &hsts.Proxy{Handler: proxy}
}
if err = os.MkdirAll(a.Cache, 0700); chk.E(err) {
err = fmt.Errorf(
"cannot create cache directory %q: %v",
a.Cache, err,
)
chk.E(err)
return
}
m := autocert.Manager{
Prompt: autocert.AcceptTOS,
Cache: autocert.DirCache(a.Cache),
HostPolicy: autocert.HostWhitelist(util.GetKeys(mapping)...),
Email: a.Email,
}
s = &http.Server{
Handler: proxy,
Addr: a.Addr,
TLSConfig: TLSConfig(&m, a.Certs...),
}
h = m.HTTPHandler(nil)
return
}
type NostrJSON struct {
Names map[string]string `json:"names"`
Relays map[string][]string `json:"relays"`
}
func setProxy(mapping map[string]string) (h http.Handler, err error) {
if len(mapping) == 0 {
return nil, fmt.Errorf("empty mapping")
}
mux := http.NewServeMux()
for hostname, backendAddr := range mapping {
hn, ba := hostname, backendAddr
if strings.ContainsRune(hn, os.PathSeparator) {
err = log.E.Err("invalid hostname: %q", hn)
return
}
network := "tcp"
if ba != "" && ba[0] == '@' && runtime.GOOS == "linux" {
// append \0 to address so addrlen for connect(2) is calculated in a
// way compatible with some other implementations (i.e. uwsgi)
network, ba = "unix", ba+string(byte(0))
} else if strings.HasPrefix(ba, "git+") {
split := strings.Split(ba, "git+")
if len(split) != 2 {
log.E.Ln("invalid go vanity redirect: %s: %s", hn, ba)
continue
}
redirector := fmt.Sprintf(
`<html><head><meta name="go-import" content="%s git %s"/><meta http-equiv = "refresh" content = " 3 ; url = %s"/></head><body>redirecting to <a href="%s">%s</a></body></html>`,
hn, split[1], split[1], split[1], split[1],
)
mux.HandleFunc(
hn+"/",
func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set(
"Access-Control-Allow-Methods",
"GET,HEAD,PUT,PATCH,POST,DELETE",
)
writer.Header().Set("Access-Control-Allow-Origin", "*")
writer.Header().Set("Content-Type", "text/html")
writer.Header().Set(
"Content-Length", fmt.Sprint(len(redirector)),
)
writer.Header().Set(
"strict-transport-security",
"max-age=0; includeSubDomains",
)
fmt.Fprint(writer, redirector)
},
)
continue
} else if filepath.IsAbs(ba) {
network = "unix"
switch {
case strings.HasSuffix(ba, string(os.PathSeparator)):
// path specified as directory with explicit trailing slash; add
// this path as static site
fs := http.FileServer(http.Dir(ba))
mux.Handle(hn+"/", fs)
continue
case strings.HasSuffix(ba, "nostr.json"):
log.I.Ln(hn, ba)
var fb []byte
if fb, err = os.ReadFile(ba); chk.E(err) {
continue
}
var v NostrJSON
if err = json.Unmarshal(fb, &v); chk.E(err) {
continue
}
var jb []byte
if jb, err = json.Marshal(v); chk.E(err) {
continue
}
nostrJSON := string(jb)
mux.HandleFunc(
hn+"/.well-known/nostr.json",
func(writer http.ResponseWriter, request *http.Request) {
log.I.Ln("serving nostr json to", hn)
writer.Header().Set(
"Access-Control-Allow-Methods",
"GET,HEAD,PUT,PATCH,POST,DELETE",
)
writer.Header().Set("Access-Control-Allow-Origin", "*")
writer.Header().Set("Content-Type", "application/json")
writer.Header().Set(
"Content-Length", fmt.Sprint(len(nostrJSON)),
)
writer.Header().Set(
"strict-transport-security",
"max-age=0; includeSubDomains",
)
fmt.Fprint(writer, nostrJSON)
},
)
continue
}
} else if u, err := url.Parse(ba); err == nil {
switch u.Scheme {
case "http", "https":
rp := reverse.NewSingleHostReverseProxy(u)
modifyCORSResponse := func(res *http.Response) error {
res.Header.Set(
"Access-Control-Allow-Methods",
"GET,HEAD,PUT,PATCH,POST,DELETE",
)
// res.Header.Set("Access-Control-Allow-Credentials", "true")
res.Header.Set("Access-Control-Allow-Origin", "*")
return nil
}
rp.ModifyResponse = modifyCORSResponse
rp.ErrorLog = stdLog.New(
os.Stderr, "lerproxy", stdLog.Llongfile,
)
rp.BufferPool = buf.Pool{}
mux.Handle(hn+"/", rp)
continue
}
}
rp := &httputil.ReverseProxy{
Director: func(req *http.Request) {
req.URL.Scheme = "http"
req.URL.Host = req.Host
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-For", req.RemoteAddr)
req.Header.Set(
"Access-Control-Allow-Methods",
"GET,HEAD,PUT,PATCH,POST,DELETE",
)
// req.Header.Set("Access-Control-Allow-Credentials", "true")
req.Header.Set("Access-Control-Allow-Origin", "*")
log.D.Ln(req.URL, req.RemoteAddr)
},
Transport: &http.Transport{
DialContext: func(c context.T, n, addr string) (
net.Conn, error,
) {
return net.DialTimeout(network, ba, 5*time.Second)
},
},
ErrorLog: stdLog.New(io.Discard, "", 0),
BufferPool: buf.Pool{},
}
mux.Handle(hn+"/", rp)
}
return mux, nil
}
func readMapping(file string) (m map[string]string, err error) {
var f *os.File
if f, err = os.Open(file); chk.E(err) {
return
}
m = make(map[string]string)
sc := bufio.NewScanner(f)
for sc.Scan() {
if b := sc.Bytes(); len(b) == 0 || b[0] == '#' {
continue
}
s := strings.SplitN(sc.Text(), ":", 2)
if len(s) != 2 {
err = fmt.Errorf("invalid line: %q", sc.Text())
log.E.Ln(err)
chk.E(f.Close())
return
}
m[strings.TrimSpace(s[0])] = strings.TrimSpace(s[1])
}
err = sc.Err()
chk.E(err)
chk.E(f.Close())
return
}

View File

@@ -1,34 +0,0 @@
// Package reverse is a copy of httputil.NewSingleHostReverseProxy with addition
// of "X-Forwarded-Proto" header.
package reverse
import (
"net/http"
"net/http/httputil"
"net/url"
"orly.dev/cmd/lerproxy/util"
"orly.dev/pkg/utils/log"
)
// NewSingleHostReverseProxy is a copy of httputil.NewSingleHostReverseProxy
// with addition of "X-Forwarded-Proto" header.
func NewSingleHostReverseProxy(target *url.URL) (rp *httputil.ReverseProxy) {
targetQuery := target.RawQuery
director := func(req *http.Request) {
log.D.S(req)
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = util.SingleJoiningSlash(target.Path, req.URL.Path)
if targetQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = targetQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
}
if _, ok := req.Header["User-Agent"]; !ok {
req.Header.Set("User-Agent", "")
}
req.Header.Set("X-Forwarded-Proto", "https")
}
rp = &httputil.ReverseProxy{Director: director}
return
}

View File

@@ -1,26 +0,0 @@
// Package util provides some helpers for lerproxy, a tool to convert maps of
// strings to slices of the same strings, and a helper to avoid putting two / in
// a URL.
package util
import "strings"
func GetKeys(m map[string]string) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
func SingleJoiningSlash(a, b string) string {
suffixSlash := strings.HasSuffix(a, "/")
prefixSlash := strings.HasPrefix(b, "/")
switch {
case suffixSlash && prefixSlash:
return a + b[1:]
case !suffixSlash && !prefixSlash:
return a + "/" + b
}
return a + b
}

View File

@@ -0,0 +1,62 @@
package utils
import "strings"
// GetKeys returns a slice containing all the keys from the provided map.
//
// # Parameters
//
// - m (map[string]string): The input map from which to extract keys.
//
// # Return Values
//
// - []string: A slice of strings representing the keys in the map.
//
// # Expected behaviour
//
// - Iterates over each key in the map and appends it to a new slice.
//
// - Returns the slice containing all the keys.
func GetKeys(m map[string]string) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
// SingleJoiningSlash joins two strings with a single slash between them,
// ensuring that the resulting path doesn't contain multiple consecutive
// slashes.
//
// # Parameters
//
// - a (string): The first string to join.
//
// - b (string): The second string to join.
//
// # Return Values
//
// - result (string): The joined string with a single slash between them if
// needed.
//
// # Expected behaviour
//
// - If both a and b start and end with a slash, the resulting string will have
// only one slash between them.
//
// - If neither a nor b starts or ends with a slash, the strings will be joined
// with a single slash in between.
//
// - Otherwise, the two strings are simply concatenated.
func SingleJoiningSlash(a, b string) string {
suffixSlash := strings.HasSuffix(a, "/")
prefixSlash := strings.HasPrefix(b, "/")
switch {
case suffixSlash && prefixSlash:
return a + b[1:]
case !suffixSlash && !prefixSlash:
return a + "/" + b
}
return a + b
}

View File

@@ -190,6 +190,5 @@ func Post(f string, ur *url.URL, sign signer.I) (err error) {
if io.Copy(os.Stdout, res.Body); chk.E(err) {
return
}
fmt.Println()
return
}

162
cmd/nwcclient/README.md Normal file
View File

@@ -0,0 +1,162 @@
# NWC Client CLI Tool
A command-line interface tool for making calls to Nostr Wallet Connect (NWC) services.
## Overview
This CLI tool allows you to interact with NWC wallet services using the methods defined in the NIP-47 specification. It provides a simple interface for executing wallet operations and displays the JSON response from the wallet service.
## Usage
```
nwcclient <connection URL> <method> [parameters...]
```
### Connection URL
The connection URL should be in the Nostr Wallet Connect format:
```
nostr+walletconnect://<wallet_pubkey>?relay=<relay_url>&secret=<secret>
```
### Supported Methods
The following methods are supported by this CLI tool:
- `get_info` - Get wallet information
- `get_balance` - Get wallet balance
- `get_budget` - Get wallet budget
- `make_invoice` - Create an invoice
- `pay_invoice` - Pay an invoice
- `pay_keysend` - Send a keysend payment
- `lookup_invoice` - Look up an invoice
- `list_transactions` - List transactions
- `sign_message` - Sign a message
### Unsupported Methods
The following methods are defined in the NIP-47 specification but are not directly supported by this CLI tool due to limitations in the underlying nwc package:
- `create_connection` - Create a connection
- `make_hold_invoice` - Create a hold invoice
- `settle_hold_invoice` - Settle a hold invoice
- `cancel_hold_invoice` - Cancel a hold invoice
- `multi_pay_invoice` - Pay multiple invoices
- `multi_pay_keysend` - Send multiple keysend payments
## Method Parameters
### Methods with No Parameters
- `get_info`
- `get_balance`
- `get_budget`
Example:
```
nwcclient <connection URL> get_info
```
### Methods with Parameters
#### make_invoice
```
nwcclient <connection URL> make_invoice <amount> <description> [description_hash] [expiry]
```
- `amount` - Amount in millisatoshis (msats)
- `description` - Invoice description
- `description_hash` (optional) - Hash of the description
- `expiry` (optional) - Expiry time in seconds
Example:
```
nwcclient <connection URL> make_invoice 1000000 "Test invoice" "" 3600
```
#### pay_invoice
```
nwcclient <connection URL> pay_invoice <invoice> [amount]
```
- `invoice` - BOLT11 invoice
- `amount` (optional) - Amount in millisatoshis (msats)
Example:
```
nwcclient <connection URL> pay_invoice lnbc1...
```
#### pay_keysend
```
nwcclient <connection URL> pay_keysend <amount> <pubkey> [preimage]
```
- `amount` - Amount in millisatoshis (msats)
- `pubkey` - Recipient's public key
- `preimage` (optional) - Payment preimage
Example:
```
nwcclient <connection URL> pay_keysend 1000000 03...
```
#### lookup_invoice
```
nwcclient <connection URL> lookup_invoice <payment_hash_or_invoice>
```
- `payment_hash_or_invoice` - Payment hash or BOLT11 invoice
Example:
```
nwcclient <connection URL> lookup_invoice 3d...
```
#### list_transactions
```
nwcclient <connection URL> list_transactions [from <timestamp>] [until <timestamp>] [limit <count>] [offset <count>] [unpaid <true|false>] [type <incoming|outgoing>]
```
Parameters are specified as name-value pairs:
- `from` - Start timestamp
- `until` - End timestamp
- `limit` - Maximum number of transactions to return
- `offset` - Number of transactions to skip
- `unpaid` - Whether to include unpaid transactions
- `type` - Transaction type (incoming or outgoing)
Example:
```
nwcclient <connection URL> list_transactions limit 10 type incoming
```
#### sign_message
```
nwcclient <connection URL> sign_message <message>
```
- `message` - Message to sign
Example:
```
nwcclient <connection URL> sign_message "Hello, world!"
```
## Output
The tool prints the JSON response from the wallet service to stdout. If an error occurs, an error message is printed to stderr.
## Limitations
- The tool only supports methods that have direct client methods in the nwc package.
- Complex parameters like metadata are not supported.
- The tool does not support interactive authentication or authorization.

285
cmd/nwcclient/main.go Normal file
View File

@@ -0,0 +1,285 @@
package main
import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"orly.dev/pkg/protocol/nwc"
)
func printUsage() {
fmt.Println("Usage: nwcclient \"<connection URL>\" <method> [parameters...]")
fmt.Println("\nSupported methods:")
fmt.Println(" get_info - Get wallet information")
fmt.Println(" get_balance - Get wallet balance")
fmt.Println(" get_budget - Get wallet budget")
fmt.Println(" make_invoice - Create an invoice (amount, description, [description_hash], [expiry])")
fmt.Println(" pay_invoice - Pay an invoice (invoice, [amount])")
fmt.Println(" pay_keysend - Send a keysend payment (amount, pubkey, [preimage])")
fmt.Println(" lookup_invoice - Look up an invoice (payment_hash or invoice)")
fmt.Println(" list_transactions - List transactions ([from], [until], [limit], [offset], [unpaid], [type])")
fmt.Println(" sign_message - Sign a message (message)")
fmt.Println("\nUnsupported methods (due to limitations in the nwc package):")
fmt.Println(" create_connection - Create a connection")
fmt.Println(" make_hold_invoice - Create a hold invoice")
fmt.Println(" settle_hold_invoice - Settle a hold invoice")
fmt.Println(" cancel_hold_invoice - Cancel a hold invoice")
fmt.Println(" multi_pay_invoice - Pay multiple invoices")
fmt.Println(" multi_pay_keysend - Send multiple keysend payments")
fmt.Println("\nParameters format:")
fmt.Println(" - Positional parameters are used for required fields")
fmt.Println(" - For list_transactions, named parameters are used: 'from', 'until', 'limit', 'offset', 'unpaid', 'type'")
fmt.Println(" Example: nwcclient <url> list_transactions limit 10 type incoming")
os.Exit(1)
}
func main() {
// Check if we have enough arguments
if len(os.Args) < 3 {
printUsage()
}
// Parse connection URL and method
connectionURL := os.Args[1]
methodStr := os.Args[2]
method := nwc.Method(methodStr)
// Parse the wallet connect URL
opts, err := nwc.ParseWalletConnectURL(connectionURL)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing connection URL: %v\n", err)
os.Exit(1)
}
// Create a new NWC client
client, err := nwc.NewNWCClient(opts)
if err != nil {
fmt.Fprintf(os.Stderr, "Error creating NWC client: %v\n", err)
os.Exit(1)
}
defer client.Close()
// Execute the requested method
var result interface{}
switch method {
case nwc.GetInfo:
result, err = client.GetInfo()
case nwc.GetBalance:
result, err = client.GetBalance()
case nwc.GetBudget:
result, err = client.GetBudget()
case nwc.MakeInvoice:
if len(os.Args) < 5 {
fmt.Fprintf(
os.Stderr,
"Error: make_invoice requires at least amount and description\n",
)
printUsage()
}
amount, err := strconv.ParseInt(os.Args[3], 10, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing amount: %v\n", err)
os.Exit(1)
}
description := os.Args[4]
req := &nwc.MakeInvoiceRequest{
Amount: amount,
Description: description,
}
// Optional parameters
if len(os.Args) > 5 {
req.DescriptionHash = os.Args[5]
}
if len(os.Args) > 6 {
expiry, err := strconv.ParseInt(os.Args[6], 10, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing expiry: %v\n", err)
os.Exit(1)
}
req.Expiry = &expiry
}
result, err = client.MakeInvoice(req)
case nwc.PayInvoice:
if len(os.Args) < 4 {
fmt.Fprintf(os.Stderr, "Error: pay_invoice requires an invoice\n")
printUsage()
}
req := &nwc.PayInvoiceRequest{
Invoice: os.Args[3],
}
// Optional amount parameter
if len(os.Args) > 4 {
amount, err := strconv.ParseInt(os.Args[4], 10, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing amount: %v\n", err)
os.Exit(1)
}
req.Amount = &amount
}
result, err = client.PayInvoice(req)
case nwc.PayKeysend:
if len(os.Args) < 5 {
fmt.Fprintf(
os.Stderr, "Error: pay_keysend requires amount and pubkey\n",
)
printUsage()
}
amount, err := strconv.ParseInt(os.Args[3], 10, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing amount: %v\n", err)
os.Exit(1)
}
req := &nwc.PayKeysendRequest{
Amount: amount,
Pubkey: os.Args[4],
}
// Optional preimage
if len(os.Args) > 5 {
req.Preimage = os.Args[5]
}
result, err = client.PayKeysend(req)
case nwc.LookupInvoice:
if len(os.Args) < 4 {
fmt.Fprintf(
os.Stderr,
"Error: lookup_invoice requires a payment_hash or invoice\n",
)
printUsage()
}
param := os.Args[3]
req := &nwc.LookupInvoiceRequest{}
// Determine if the parameter is a payment hash or an invoice
if strings.HasPrefix(param, "ln") {
req.Invoice = param
} else {
req.PaymentHash = param
}
result, err = client.LookupInvoice(req)
case nwc.ListTransactions:
req := &nwc.ListTransactionsRequest{}
// Parse optional parameters
paramIndex := 3
for paramIndex < len(os.Args) {
if paramIndex+1 >= len(os.Args) {
break
}
paramName := os.Args[paramIndex]
paramValue := os.Args[paramIndex+1]
switch paramName {
case "from":
val, err := strconv.ParseInt(paramValue, 10, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing from: %v\n", err)
os.Exit(1)
}
req.From = &val
case "until":
val, err := strconv.ParseInt(paramValue, 10, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing until: %v\n", err)
os.Exit(1)
}
req.Until = &val
case "limit":
val, err := strconv.ParseInt(paramValue, 10, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing limit: %v\n", err)
os.Exit(1)
}
req.Limit = &val
case "offset":
val, err := strconv.ParseInt(paramValue, 10, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing offset: %v\n", err)
os.Exit(1)
}
req.Offset = &val
case "unpaid":
val := paramValue == "true"
req.Unpaid = &val
case "type":
req.Type = &paramValue
default:
fmt.Fprintf(os.Stderr, "Unknown parameter: %s\n", paramName)
os.Exit(1)
}
paramIndex += 2
}
result, err = client.ListTransactions(req)
case nwc.SignMessage:
if len(os.Args) < 4 {
fmt.Fprintf(os.Stderr, "Error: sign_message requires a message\n")
printUsage()
}
req := &nwc.SignMessageRequest{
Message: os.Args[3],
}
result, err = client.SignMessage(req)
case nwc.CreateConnection, nwc.MakeHoldInvoice, nwc.SettleHoldInvoice, nwc.CancelHoldInvoice, nwc.MultiPayInvoice, nwc.MultiPayKeysend:
fmt.Fprintf(
os.Stderr,
"Error: Method %s is not directly supported by the CLI tool.\n",
methodStr,
)
fmt.Fprintf(
os.Stderr,
"This is because these methods don't have exported client methods in the nwc package.\n",
)
fmt.Fprintf(
os.Stderr,
"Only the following methods are currently supported: get_info, get_balance, get_budget, make_invoice, pay_invoice, pay_keysend, lookup_invoice, list_transactions, sign_message\n",
)
os.Exit(1)
default:
fmt.Fprintf(os.Stderr, "Error: Unsupported method: %s\n", methodStr)
printUsage()
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error executing method: %v\n", err)
os.Exit(1)
}
// Print the result as JSON
jsonData, err := json.MarshalIndent(result, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "Error marshaling result to JSON: %v\n", err)
os.Exit(1)
}
fmt.Println(string(jsonData))
}

View File

@@ -7,13 +7,14 @@ import (
"encoding/hex"
"fmt"
"orly.dev/pkg/crypto/ec/bech32"
"orly.dev/pkg/crypto/ec/schnorr"
"orly.dev/pkg/crypto/ec/secp256k1"
"orly.dev/pkg/crypto/p256k"
"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/lol"
"orly.dev/pkg/utils/qu"
"os"
"runtime"
@@ -33,9 +34,9 @@ const (
)
type Result struct {
sec *secp256k1.SecretKey
sec []byte
npub []byte
pub *secp256k1.PublicKey
pub []byte
}
var args struct {
@@ -45,6 +46,7 @@ var args struct {
}
func main() {
lol.SetLogLevel("info")
arg.MustParse(&args)
if args.String == "" {
_, _ = fmt.Fprintln(
@@ -79,7 +81,7 @@ Options:
}
}
func Vanity(str string, where int, threads int) (e error) {
func Vanity(str string, where int, threads int) (err error) {
// check the string has valid bech32 ciphers
for i := range str {
@@ -122,7 +124,7 @@ out:
wm := workingFor % time.Second
workingFor -= wm
fmt.Printf(
"working for %v, attempts %d\n",
" working for %v, attempts %d",
workingFor, counter.Load(),
)
case r := <-resC:
@@ -142,20 +144,16 @@ out:
wg.Wait()
fmt.Printf(
"generated in %d attempts using %d threads, taking %v\n",
"\r# generated in %d attempts using %d threads, taking %v ",
counter.Load(), args.Threads, time.Now().Sub(started),
)
secBytes := res.sec.Serialize()
log.D.Ln(
"generated key pair:\n"+
"\nhex:\n"+
"\tsecret: %s\n"+
"\tpublic: %s\n\n",
hex.EncodeToString(secBytes),
hex.EncodeToString(schnorr.SerializePubKey(res.pub)),
fmt.Printf(
"\nHSEC = %s\nHPUB = %s\n",
hex.EncodeToString(res.sec),
hex.EncodeToString(res.pub),
)
nsec, _ := bech32encoding.SecretKeyToNsec(res.sec)
fmt.Printf("\nNSEC = %s\nNPUB = %s\n\n", nsec, res.npub)
nsec, _ := bech32encoding.BinToNsec(res.sec)
fmt.Printf("NSEC = %s\nNPUB = %s\n", nsec, res.npub)
return
}
@@ -185,16 +183,19 @@ out:
default:
}
counter.Inc()
r.sec, r.pub, e = GenKeyPair()
// r.sec, r.pub, e = GenKeyPair()
r.sec, r.pub, e = Gen()
if e != nil {
log.E.Ln("error generating key: '%v' worker stopping", e)
break out
}
r.npub, e = bech32encoding.PublicKeyToNpub(r.pub)
if e != nil {
// r.npub, e = bech32encoding.PublicKeyToNpub(r.pub)
if r.npub, e = bech32encoding.BinToNpub(r.pub); e != nil {
log.E.Ln("fatal error generating npub: %s\n", e)
break out
}
fmt.Printf("\rgenerating key: %s", r.npub)
// log.I.F("%s", r.npub)
switch where {
case PositionBeginning:
if bytes.HasPrefix(r.npub, append(prefix, []byte(str)...)) {
@@ -215,6 +216,11 @@ out:
}
}
func Gen() (skb, pkb []byte, err error) {
skb, pkb, _, _, err = p256k.Generate()
return
}
// GenKeyPair creates a fresh new key pair using the entropy source used by
// crypto/rand (ie, /dev/random on posix systems).
func GenKeyPair() (

46
main.go
View File

@@ -1,26 +1,26 @@
// Package main is a nostr relay with a simple follow/mute list authentication
// scheme and the new HTTP REST based protocol. Configuration is via environment
// scheme and the new HTTP REST-based protocol. Configuration is via environment
// variables or an optional .env file.
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"os"
"github.com/pkg/profile"
_ "net/http/pprof"
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/protocol/openapi"
"orly.dev/pkg/protocol/servemux"
"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"
"os"
)
func main() {
@@ -43,15 +43,24 @@ func main() {
os.Exit(0)
}
lol.SetLogLevel(cfg.LogLevel)
if cfg.Pprof {
defer profile.Start(profile.MemProfile).Stop()
go func() {
chk.E(http.ListenAndServe("127.0.0.1:6060", nil))
}()
if cfg.Pprof != "" {
switch cfg.Pprof {
case "cpu":
prof := profile.Start(profile.CPUProfile)
defer prof.Stop()
case "memory":
prof := profile.Start(profile.MemProfile)
defer prof.Stop()
case "allocation":
prof := profile.Start(profile.MemProfileAllocs)
defer prof.Stop()
}
}
c, cancel := context.Cancel(context.Bg())
storage, err := database.New(c, cancel, cfg.DataDir, cfg.DbLogLevel)
if chk.E(err) {
var storage *database.D
if storage, err = database.New(
c, cancel, cfg.DataDir, cfg.DbLogLevel,
); chk.E(err) {
os.Exit(1)
}
r := &app2.Relay{C: cfg, Store: storage}
@@ -66,9 +75,20 @@ func main() {
C: cfg,
}
var opts []options.O
if server, err = relay.NewServer(serverParams, opts...); chk.E(err) {
serveMux := servemux.NewServeMux()
if server, err = relay.NewServer(
serverParams, serveMux, opts...,
); chk.E(err) {
os.Exit(1)
}
openapi.New(
server,
cfg.AppName,
version.V,
version.Description,
"/api",
serveMux,
)
if err != nil {
log.F.F("failed to create server: %v", err)
}

View File

@@ -34,12 +34,16 @@ type C struct {
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"`
Pprof string `env:"ORLY_PPROF" usage:"enable pprof on 127.0.0.1:6060" enum:"cpu,memory,allocation"`
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/"`
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://profiles.nostr1.com/,wss://relay.nostr.band/,wss://relay.damus.io/,wss://nostr.wine/,wss://nostr.land/,wss://theforest.nostr1.com/"`
SpiderType string `env:"ORLY_SPIDER_TYPE" usage:"whether to spider, and what degree of spidering: none, directory, follows (follows means to the second degree of the follow graph)" default:"directory"`
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"`
Whitelist []string `env:"ORLY_WHITELIST" usage:"only allow connections from this list of IP addresses"`
RelaySecret string `env:"ORLY_SECRET_KEY" usage:"secret key for relay cluster replication authentication"`
PeerRelays []string `env:"ORLY_PEER_RELAYS" usage:"list of peer relays URLs that new events are pushed to in format <pubkey>|<url>"`
}
// New creates and initializes a new configuration object for the relay
@@ -73,6 +77,9 @@ func New() (cfg *C, err error) {
if cfg.State == "" || strings.Contains(cfg.State, "~") {
cfg.State = filepath.Join(xdg.StateHome, cfg.AppName)
}
if len(cfg.Owners) > 0 {
cfg.AuthRequired = true
}
envPath := filepath.Join(cfg.Config, ".env")
if apputil.FileExists(envPath) {
var e env2.Env
@@ -87,6 +94,17 @@ func New() (cfg *C, err error) {
lol.SetLogLevel(cfg.LogLevel)
log.I.F("loaded configuration from %s", envPath)
}
// if spider seeds has no elements, there still is a single entry with an
// empty string; and also if any of the fields are empty strings, they need
// to be removed.
var seeds []string
for _, u := range cfg.SpiderSeeds {
if u == "" {
continue
}
seeds = append(seeds, u)
}
cfg.SpiderSeeds = seeds
return
}

View File

@@ -41,8 +41,13 @@ func (s *Server) AcceptEvent(
c context.T, ev *event.E, hr *http.Request, authedPubkey []byte,
remote string,
) (accept bool, notice string, afterSave func()) {
if !s.AuthRequired() {
accept = true
return
}
// if auth is required and the user is not authed, reject
if s.AuthRequired() && len(authedPubkey) == 0 {
notice = "client isn't authed"
return
}
// check if the authed user is on the lists
@@ -53,6 +58,14 @@ func (s *Server) AcceptEvent(
break
}
}
// todo: check if event author is on owners' mute lists or block list
if !accept {
return
}
for _, u := range s.OwnersMuted() {
if bytes.Equal(u, authedPubkey) {
notice = "event author is banned from this relay"
return
}
}
return
}

View File

@@ -1,8 +1,18 @@
package relay
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"orly.dev/pkg/crypto/ec/secp256k1"
"orly.dev/pkg/encoders/hex"
"orly.dev/pkg/protocol/httpauth"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/log"
realy_lol "orly.dev/pkg/version"
"regexp"
"strings"
@@ -17,6 +27,21 @@ var (
NIP20prefixmatcher = regexp.MustCompile(`^\w+: `)
)
var userAgent = fmt.Sprintf("orly/%s", realy_lol.V)
type WriteCloser struct {
*bytes.Buffer
}
func (w *WriteCloser) Close() error {
w.Buffer.Reset()
return nil
}
func NewWriteCloser(w []byte) *WriteCloser {
return &WriteCloser{bytes.NewBuffer(w)}
}
// AddEvent processes an incoming event, saves it if valid, and delivers it to
// subscribers.
//
@@ -55,6 +80,7 @@ var (
// relevant message.
func (s *Server) AddEvent(
c context.T, rl relay.I, ev *event.E, hr *http.Request, origin string,
pubkeys [][]byte,
) (accepted bool, message []byte) {
if ev == nil {
@@ -85,6 +111,77 @@ func (s *Server) AddEvent(
}
// notify subscribers
s.listeners.Deliver(ev)
// push the new event to replicas if replicas are configured, and the relay
// has an identity key.
var err error
if len(s.Peers.Addresses) > 0 &&
len(s.Peers.I.Sec()) == secp256k1.SecKeyBytesLen {
evb := ev.Marshal(nil)
var payload io.ReadCloser
payload = NewWriteCloser(evb)
replica:
for i, a := range s.Peers.Addresses {
// the peer address index is the same as the list of pubkeys
// (they're unpacked from a string containing both, appended at the
// same time), so if the pubkeys from the http event endpoint sent
// us here matches the index of this address, we can skip it.
for _, pk := range pubkeys {
if bytes.Equal(s.Peers.Pubkeys[i], pk) {
log.I.F(
"not sending back to replica that just sent us this event %0x %s",
ev.ID, a,
)
continue replica
}
}
var ur *url.URL
if ur, err = url.Parse(a + "/api/event"); chk.E(err) {
continue
}
var r *http.Request
r = &http.Request{
Method: "POST",
URL: ur,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
Body: payload,
ContentLength: int64(len(evb)),
Host: ur.Host,
}
r.Header.Add("User-Agent", userAgent)
if err = httpauth.AddNIP98Header(
r, ur, "POST", "", s.Peers.I, 0,
); chk.E(err) {
continue
}
// add this replica's pubkey to the list to prevent re-sending to
// other replicas more than twice
pubkeys = append(pubkeys, s.Peers.Pub())
var pubkeysHeader []byte
for j, pk := range pubkeys {
pubkeysHeader = hex.EncAppend(pubkeysHeader, pk)
if j < len(pubkeys)-1 {
pubkeysHeader = append(pubkeysHeader, ':')
}
}
r.Header.Add("X-Pubkeys", string(pubkeysHeader))
r.GetBody = func() (rc io.ReadCloser, err error) {
rc = payload
return
}
client := &http.Client{}
if _, err = client.Do(r); chk.E(err) {
continue
}
log.I.F(
"event pushed to replica %s\n%s",
ur.String(), evb,
)
break
}
}
accepted = true
return
}

View File

@@ -0,0 +1,39 @@
package relay
import (
"bytes"
"net/http"
"orly.dev/pkg/protocol/httpauth"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/log"
"time"
)
func (s *Server) AdminAuth(
r *http.Request, remote string,
tolerance ...time.Duration,
) (authed bool, pubkey []byte) {
var valid bool
var err error
var tolerate time.Duration
if len(tolerance) > 0 {
tolerate = tolerance[0]
}
if valid, pubkey, err = httpauth.CheckAuth(r, tolerate); chk.E(err) {
return
}
if !valid {
log.E.F(
"invalid auth %s from %s",
r.Header.Get("Authorization"), remote,
)
return
}
for _, pk := range s.ownersPubkeys {
if bytes.Equal(pk, pubkey) {
authed = true
return
}
}
return
}

View File

@@ -7,7 +7,6 @@ import (
"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
@@ -34,8 +33,6 @@ import (
//
// - 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

10
pkg/app/relay/config.go Normal file
View File

@@ -0,0 +1,10 @@
package relay
import (
"orly.dev/pkg/app/config"
)
func (s *Server) Config() (c *config.C) {
c = s.C
return
}

View File

@@ -55,7 +55,8 @@ func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) {
Nips: supportedNIPs, Software: version.URL,
Version: version.V,
Limitation: relayinfo.Limits{
AuthRequired: s.C.AuthRequired,
AuthRequired: s.C.AuthRequired,
RestrictedWrites: s.C.AuthRequired,
},
Icon: "https://cdn.satellite.earth/ac9778868fbf23b63c47c769a74e163377e6ea94d3f0f31711931663d035c4f6.png",
}

View File

@@ -0,0 +1,39 @@
package relay
import (
"bytes"
"net/http"
"orly.dev/pkg/protocol/httpauth"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/log"
"time"
)
func (s *Server) OwnersFollowedAuth(
r *http.Request, remote string,
tolerance ...time.Duration,
) (authed bool, pubkey []byte) {
var valid bool
var err error
var tolerate time.Duration
if len(tolerance) > 0 {
tolerate = tolerance[0]
}
if valid, pubkey, err = httpauth.CheckAuth(r, tolerate); chk.E(err) {
return
}
if !valid {
log.E.F(
"invalid auth %s from %s",
r.Header.Get("Authorization"), remote,
)
return
}
for _, pk := range s.ownersFollowed {
if bytes.Equal(pk, pubkey) {
authed = true
return
}
}
return
}

View File

@@ -0,0 +1,6 @@
package relay
func (s *Server) OwnersPubkeys() (pks [][]byte) {
pks = s.ownersPubkeys
return
}

72
pkg/app/relay/peers.go Normal file
View File

@@ -0,0 +1,72 @@
package relay
import (
"orly.dev/pkg/crypto/p256k"
"orly.dev/pkg/encoders/bech32encoding"
"orly.dev/pkg/interfaces/signer"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/keys"
"orly.dev/pkg/utils/log"
"strings"
)
// Peers is a structure that keeps the information required when peer
// replication is enabled.
//
// - Addresses are the relay addresses that will be pushed new events when
// accepted. From ORLY_PEER_RELAYS first field after the |.
//
// - Pubkeys are the relay peer public keys that we will send any event to
// including privileged type. From ORLY_PEER_RELAYS before the |.
//
// - I - the signer of this relay, generated from the nsec in
// ORLY_SECRET_KEY.
type Peers struct {
Addresses []string
Pubkeys [][]byte
signer.I
}
// Init accepts the lists which will come from config.C for peer relay settings
// and populate the Peers with this data after decoding it.
func (p *Peers) Init(
addresses []string, sec string,
) (err error) {
for _, address := range addresses {
if len(address) == 0 {
continue
}
split := strings.Split(address, "@")
if len(split) != 2 {
log.E.F("invalid peer address: %s", address)
continue
}
p.Addresses = append(p.Addresses, split[1])
var pk []byte
if pk, err = keys.DecodeNpubOrHex(split[0]); chk.D(err) {
continue
}
p.Pubkeys = append(p.Pubkeys, pk)
log.I.F("peer %s added; pubkey: %0x", split[1], pk)
}
if sec == "" {
return
}
p.I = &p256k.Signer{}
var s []byte
if s, err = keys.DecodeNsecOrHex(sec); chk.E(err) {
return
}
if err = p.I.InitSec(s); chk.E(err) {
return
}
var npub []byte
if npub, err = bech32encoding.BinToNpub(p.I.Pub()); chk.E(err) {
return
}
log.I.F(
"relay peer initialized, relay's npub: %s",
npub,
)
return
}

View File

@@ -1,11 +1,9 @@
// Package publisher is a singleton package that keeps track of subscriptions in
// both websockets and http SSE, including managing the authentication state of
// a connection.
package publish
import (
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/interfaces/publisher"
"orly.dev/pkg/interfaces/typer"
)
// S is the control structure for the subscription management scheme.
@@ -26,11 +24,10 @@ func (s *S) Type() string { return "publish" }
func (s *S) Deliver(ev *event.E) {
for _, p := range s.Publishers {
p.Deliver(ev)
return
}
}
func (s *S) Receive(msg publisher.Message) {
func (s *S) Receive(msg typer.T) {
t := msg.Type()
for _, p := range s.Publishers {
if p.Type() == t {

View File

@@ -18,7 +18,9 @@ import (
"orly.dev/pkg/utils/normalize"
)
// Publish processes and stores an event in the server's storage. It handles different types of events: ephemeral, replaceable, and parameterized replaceable.
// Publish processes and stores an event in the server's storage. It handles
// different types of events: ephemeral, replaceable, and parameterized
// replaceable.
//
// # Parameters
//
@@ -60,8 +62,14 @@ func (s *Server) Publish(c context.T, evt *event.E) (err error) {
log.T.F("found %d possible duplicate events", len(evs))
for _, ev := range evs {
del := true
if bytes.Equal(ev.Id, evt.Id) {
continue
if bytes.Equal(ev.ID, evt.ID) {
return errorf.W(
string(
normalize.Duplicate.F(
"event already in relay database",
),
),
)
}
log.I.F(
"maybe replace %s with %s", ev.Serialize(), evt.Serialize(),
@@ -75,6 +83,12 @@ func (s *Server) Publish(c context.T, evt *event.E) (err error) {
),
)
}
// 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
@@ -88,7 +102,7 @@ func (s *Server) Publish(c context.T, evt *event.E) (err error) {
}
if isFollowed {
if _, _, err = sto.SaveEvent(
c, evt,
c, evt, false, nil,
); err != nil && !errors.Is(
err, store.ErrDupEvent,
) {
@@ -99,7 +113,7 @@ func (s *Server) Publish(c context.T, evt *event.E) (err error) {
err = nil
}
// event has been saved and lists updated.
return
// return
}
}
@@ -110,7 +124,7 @@ func (s *Server) Publish(c context.T, evt *event.E) (err error) {
for _, pk := range owners {
if bytes.Equal(evt.Pubkey, pk) {
if _, _, err = sto.SaveEvent(
c, evt,
c, evt, false, nil,
); err != nil && !errors.Is(
err, store.ErrDupEvent,
) {
@@ -121,7 +135,7 @@ func (s *Server) Publish(c context.T, evt *event.E) (err error) {
err = nil
}
// event has been saved and lists updated.
return
// return
}
}
}
@@ -222,10 +236,17 @@ func (s *Server) Publish(c context.T, evt *event.E) (err error) {
}
}
}
if _, _, err = sto.SaveEvent(c, evt); err != nil && !errors.Is(
if _, _, err = sto.SaveEvent(
c, evt, false, append(s.Peers.Pubkeys, s.ownersPubkeys...),
); err != nil && !errors.Is(
err, store.ErrDupEvent,
) {
return
}
log.T.C(
func() string {
return fmt.Sprintf("saved event:\n%s", evt.Serialize())
},
)
return
}

View File

@@ -6,8 +6,10 @@ import (
"fmt"
"net"
"net/http"
"orly.dev/pkg/protocol/openapi"
"orly.dev/pkg/protocol/socketapi"
"strconv"
"strings"
"time"
"orly.dev/pkg/app/config"
@@ -37,6 +39,8 @@ type Server struct {
listeners *publish.S
*config.C
*Lists
*Peers
Mux *servemux.S
}
// ServerParams represents the configuration parameters for initializing a
@@ -48,6 +52,7 @@ type ServerParams struct {
Rl relay.I
DbPath string
MaxLimit int
Mux *servemux.S
*config.C
}
@@ -78,7 +83,9 @@ type ServerParams struct {
// - 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) {
func NewServer(
sp *ServerParams, serveMux *servemux.S, opts ...options.O,
) (s *Server, err error) {
op := options.Default()
for _, opt := range opts {
opt(op)
@@ -88,7 +95,6 @@ func NewServer(sp *ServerParams, opts ...options.O) (s *Server, err error) {
return nil, fmt.Errorf("storage init: %w", err)
}
}
serveMux := servemux.NewServeMux()
s = &Server{
Ctx: sp.Ctx,
Cancel: sp.Cancel,
@@ -97,8 +103,12 @@ func NewServer(sp *ServerParams, opts ...options.O) (s *Server, err error) {
options: op,
C: sp.C,
Lists: new(Lists),
Peers: new(Peers),
}
s.listeners = publish.New(socketapi.New(s))
chk.E(
s.Peers.Init(sp.C.PeerRelays, sp.C.RelaySecret),
)
s.listeners = publish.New(socketapi.New(s), openapi.NewPublisher(s))
go func() {
if err := s.relay.Init(); chk.E(err) {
s.Shutdown()
@@ -130,6 +140,21 @@ func NewServer(sp *ServerParams, opts ...options.O) (s *Server, err error) {
//
// - For all other paths, delegates to the internal mux's ServeHTTP method.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c := s.Config()
remote := helpers.GetRemoteFromReq(r)
var whitelisted bool
if len(c.Whitelist) > 0 {
for _, addr := range c.Whitelist {
if strings.HasPrefix(remote, addr) {
whitelisted = true
}
}
} else {
whitelisted = true
}
if !whitelisted {
return
}
// standard nostr protocol only governs the "root" path of the relay and
// websockets
if r.URL.Path == "/" {
@@ -209,8 +234,8 @@ func (s *Server) Start(
}()
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 {
var ln net.Listener
if ln, err = net.Listen("tcp", addr); err != nil {
return err
}
s.httpServer = &http.Server{

View File

@@ -2,46 +2,109 @@ package relay
import (
"orly.dev/pkg/crypto/ec/schnorr"
"orly.dev/pkg/database/indexes/types"
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/encoders/filter"
"orly.dev/pkg/encoders/hex"
"orly.dev/pkg/encoders/kinds"
"orly.dev/pkg/encoders/tag"
"orly.dev/pkg/encoders/timestamp"
"orly.dev/pkg/protocol/ws"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/errorf"
"orly.dev/pkg/utils/log"
"sort"
"sync"
"runtime/debug"
"time"
)
// IdPkTs is a map of event IDs to their id, pubkey, kind, and timestamp
// This is used to reduce memory usage by storing only the essential information
// instead of the full events
type IdPkTs struct {
Id []byte
Pubkey []byte
Kind uint16
Timestamp int64
}
func (s *Server) SpiderFetch(
k *kinds.T, noFetch, noExtract bool, pubkeys ...[]byte,
) (pks [][]byte, err error) {
// Map to store id, pubkey, kind, and timestamp for each event
// Key is a combination of pubkey and kind for deduplication
pkKindMap := make(map[string]*IdPkTs)
// Map to collect pubkeys from p tags
pkMap := make(map[string]struct{})
// first search the local database
pkList := tag.New(pubkeys...)
f := &filter.F{
Kinds: k,
Authors: pkList,
}
var evs event.S
if evs, err = s.Storage().QueryEvents(s.Ctx, f); chk.E(err) {
var kindsList string
if k != nil {
for i, kk := range k.K {
if i > 0 {
kindsList += ","
}
kindsList += kk.Name()
}
} else {
kindsList = "*"
}
// Query local database
var localEvents event.S
if localEvents, err = s.Storage().QueryEvents(s.Ctx, f); chk.E(err) {
// none were found, so we need to scan the spiders
err = nil
}
var kindsList string
for i, kk := range k.K {
if i > 0 {
kindsList += ","
// Process local events
for _, ev := range localEvents {
// Create a key based on pubkey and kind for deduplication
pkKindKey := string(ev.Pubkey) + string(ev.Kind.Marshal(nil))
// Check if we already have an event with this pubkey and kind
existing, exists := pkKindMap[pkKindKey]
// If it doesn't exist or the new event is newer, store it
if !exists || ev.CreatedAtInt64() > existing.Timestamp {
pkKindMap[pkKindKey] = &IdPkTs{
Id: ev.ID,
Pubkey: ev.Pubkey,
Kind: ev.Kind.ToU16(),
Timestamp: ev.CreatedAtInt64(),
}
// Extract p tags if not in noExtract mode
if !noExtract {
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); err != nil {
err = nil
continue
}
pkMap[string(pk)] = struct{}{}
}
}
}
kindsList += kk.Name()
// Nil the event to free memory
ev = nil
}
log.I.F("%d events found of type %s", len(evs), kindsList)
// for _, ev := range evs {
// o += fmt.Sprintf("%s\n\n", ev.Marshal(nil))
// }
// log.I.F("%s", o)
if !noFetch {
log.I.F("%d events found of type %s", len(pkKindMap), kindsList)
if !noFetch && len(s.C.SpiderSeeds) > 0 {
// we need to search the spider seeds.
// Break up pubkeys into batches of 128
for i := 0; i < len(pubkeys); i += 128 {
@@ -56,94 +119,107 @@ func (s *Server) SpiderFetch(
)
batchPkList := tag.New(batchPubkeys...)
lim := uint(batchPkList.Len())
l := &lim
var since *timestamp.T
if k == nil {
since = timestamp.FromTime(time.Now().Add(-1 * time.Hour))
} else {
l = nil
}
batchFilter := &filter.F{
Kinds: k,
Authors: batchPkList,
Limit: &lim,
Since: since,
Limit: l,
}
var mx sync.Mutex
var wg sync.WaitGroup
for _, seed := range s.C.SpiderSeeds {
wg.Add(1)
go func() {
defer wg.Done()
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
return
}
if evss, err = cli.QuerySync(
context.Bg(), batchFilter,
); chk.E(err) {
err = nil
return
}
mx.Lock()
// save the events to the database
for _, ev := range evss {
log.I.F("saving event:\n%s", ev.Marshal(nil))
select {
case <-s.Ctx.Done():
return
default:
}
var evss event.S
var cli *ws.Client
if cli, err = ws.RelayConnect(
context.Bg(), seed, ws.WithSignatureChecker(
func(e *event.E) bool {
return true
},
),
); chk.E(err) {
err = nil
return
}
if evss, err = cli.QuerySync(
context.Bg(), batchFilter,
); chk.E(err) {
err = nil
return
}
// Process each event immediately
for i, ev := range evss {
// log.I.S(ev)
// Create a key based on pubkey and kind for deduplication
pkKindKey := string(ev.Pubkey) + string(ev.Kind.Marshal(nil))
// Check if we already have an event with this pubkey and kind
existing, exists := pkKindMap[pkKindKey]
// If it doesn't exist or the new event is newer, store it and save to database
if !exists || ev.CreatedAtInt64() > existing.Timestamp {
var ser *types.Uint40
if ser, err = s.Storage().GetSerialById(ev.ID); err == nil && ser != nil {
err = errorf.E("event already exists: %0x", ev.ID)
return
} else {
// verify the signature
var valid bool
if valid, err = ev.Verify(); chk.E(err) || !valid {
continue
}
}
// Save the event to the database
if _, _, err = s.Storage().SaveEvent(
s.Ctx, ev,
s.Ctx, ev, true, nil,
); chk.E(err) {
err = nil
continue
}
// Store the essential information
pkKindMap[pkKindKey] = &IdPkTs{
Id: ev.ID,
Pubkey: ev.Pubkey,
Kind: ev.Kind.ToU16(),
Timestamp: ev.CreatedAtInt64(),
}
// Extract p tags if not in noExtract mode
if !noExtract {
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); err != nil {
err = nil
continue
}
pkMap[string(pk)] = struct{}{}
}
}
}
for _, ev := range evss {
evs = append(evs, ev)
}
mx.Unlock()
}()
// Nil the event in the slice to free memory
evss[i] = nil
}
}
wg.Wait()
}
}
// 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, extract the p tags
chk.E(s.Storage().Sync())
debug.FreeOSMemory()
// If we're in noExtract mode, just return
if noExtract {
return
}
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); err != nil {
err = nil
continue
}
pkMap[string(pk)] = struct{}{}
}
}
// Convert the collected pubkeys to the return format
for pk := range pkMap {
pks = append(pks, []byte(pk))
}

View File

@@ -2,41 +2,19 @@ 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/encoders/kinds"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/keys"
"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
}
if pk, err = keys.DecodeNpubOrHex(v); chk.E(err) {
continue
}
// owners themselves are on the OwnersFollowed list as first level
ownersPubkeys = append(ownersPubkeys, pk)
@@ -118,15 +96,19 @@ func (s *Server) Spider(noFetch ...bool) (err error) {
s.SetOwnersFollowed(ownersFollowed)
s.SetFollowedFollows(followedFollows)
s.SetOwnersMuted(ownersMuted)
// lastly, update users profile metadata and relay lists in the background
if !dontFetch {
// lastly, update all followed users new events in the background
if !dontFetch && s.C.SpiderType != "none" {
go func() {
everyone := append(ownersFollowed, followedFollows...)
s.SpiderFetch(
kinds.New(
var k *kinds.T
if s.C.SpiderType == "directory" {
k = kinds.New(
kind.ProfileMetadata, kind.RelayListMetadata,
kind.DMRelaysList,
), false, true, everyone...,
)
}
everyone := append(ownersFollowed, followedFollows...)
_, _ = s.SpiderFetch(
k, false, true, everyone...,
)
}()
}

View File

@@ -7,6 +7,7 @@ import (
"orly.dev/pkg/encoders/eventid"
"orly.dev/pkg/encoders/filter"
"orly.dev/pkg/interfaces/store"
"orly.dev/pkg/protocol/servemux"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/units"
"testing"
@@ -14,6 +15,7 @@ import (
func startTestRelay(c context.T, t *testing.T, tr *testRelay) *Server {
t.Helper()
serveMux := servemux.NewServeMux()
srv, _ := NewServer(
&ServerParams{
Ctx: c,
@@ -21,6 +23,7 @@ func startTestRelay(c context.T, t *testing.T, tr *testRelay) *Server {
Rl: tr,
MaxLimit: 500 * units.Kb,
},
serveMux,
)
started := make(chan bool)
go srv.Start("127.0.0.1", 0, started)

View File

@@ -0,0 +1,50 @@
package relay
import (
"bytes"
"net/http"
"orly.dev/pkg/protocol/httpauth"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/log"
"time"
)
func (s *Server) UserAuth(
r *http.Request, remote string, tolerance ...time.Duration,
) (authed bool, pubkey []byte, super bool) {
var valid bool
var err error
var tolerate time.Duration
if len(tolerance) > 0 {
tolerate = tolerance[0]
}
if valid, pubkey, err = httpauth.CheckAuth(r, tolerate); chk.E(err) {
return
}
if !valid {
log.E.F(
"invalid auth %s from %s",
r.Header.Get("Authorization"), remote,
)
return
}
for _, pk := range append(s.ownersFollowed, s.followedFollows...) {
if bytes.Equal(pk, pubkey) {
authed = true
return
}
}
// if the client is one of the relay cluster replicas, also set the super
// flag to indicate that privilege checks can be bypassed.
if len(s.Peers.Pubkeys) > 0 {
for _, pk := range s.Peers.Pubkeys {
if bytes.Equal(pk, pubkey) {
authed = true
super = true
pubkey = pk
return
}
}
}
return
}

View File

@@ -5,42 +5,16 @@ import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"orly.dev/pkg/crypto/p256k"
"orly.dev/pkg/encoders/hex"
"lukechampine.com/frand"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/errorf"
"strings"
"lukechampine.com/frand"
)
// ComputeSharedSecret returns a shared secret key used to encrypt messages. The private and public keys should be hex
// encoded. Uses the Diffie-Hellman key exchange (ECDH) (RFC 4753).
func ComputeSharedSecret(pkh, skh string) (sharedSecret []byte, err error) {
var skb, pkb []byte
if skb, err = hex.Dec(skh); chk.E(err) {
return
}
if pkb, err = hex.Dec(pkh); chk.E(err) {
return
}
signer := new(p256k.Signer)
if err = signer.InitSec(skb); chk.E(err) {
return
}
if sharedSecret, err = signer.ECDH(pkb); chk.E(err) {
return
}
return
}
// EncryptNip4 encrypts message with key using aes-256-cbc. key should be the shared secret generated by
// ComputeSharedSecret.
//
// Returns: base64(encrypted_bytes) + "?iv=" + base64(initialization_vector).
//
// Deprecated: upgrade to using Decrypt with the NIP-44 algorithm.
func EncryptNip4(msg string, key []byte) (ct []byte, err error) {
func EncryptNip4(msg, key []byte) (ct []byte, err error) {
// block size is 16 bytes
iv := make([]byte, 16)
if _, err = frand.Read(iv); chk.E(err) {
@@ -71,22 +45,20 @@ func EncryptNip4(msg string, key []byte) (ct []byte, err error) {
// DecryptNip4 decrypts a content string using the shared secret key. The inverse operation to message ->
// EncryptNip4(message, key).
//
// Deprecated: upgrade to using Decrypt with the NIP-44 algorithm.
func DecryptNip4(content string, key []byte) (msg []byte, err error) {
parts := strings.Split(content, "?iv=")
func DecryptNip4(content, key []byte) (msg []byte, err error) {
parts := bytes.Split(content, []byte("?iv="))
if len(parts) < 2 {
return nil, errorf.E(
"error parsing encrypted message: no initialization vector",
)
}
var ciphertext []byte
if ciphertext, err = base64.StdEncoding.DecodeString(parts[0]); chk.E(err) {
ciphertext := make([]byte, base64.StdEncoding.EncodedLen(len(parts[0])))
if _, err = base64.StdEncoding.Decode(ciphertext, parts[0]); chk.E(err) {
err = errorf.E("error decoding ciphertext from base64: %w", err)
return
}
var iv []byte
if iv, err = base64.StdEncoding.DecodeString(parts[1]); chk.E(err) {
iv := make([]byte, base64.StdEncoding.EncodedLen(len(parts[1])))
if _, err = base64.StdEncoding.Decode(iv, parts[1]); chk.E(err) {
err = errorf.E("error decoding iv from base64: %w", err)
return
}

View File

@@ -10,7 +10,9 @@ import (
"golang.org/x/crypto/hkdf"
"io"
"math"
"orly.dev/pkg/crypto/p256k"
"orly.dev/pkg/crypto/sha256"
"orly.dev/pkg/interfaces/signer"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/errorf"
)
@@ -43,11 +45,9 @@ func WithCustomNonce(salt []byte) func(opts *Opts) {
// Encrypt data using a provided symmetric conversation key using NIP-44
// encryption (chacha20 cipher stream and sha256 HMAC).
func Encrypt(
plaintext string, conversationKey []byte,
applyOptions ...func(opts *Opts),
plaintext, conversationKey []byte, applyOptions ...func(opts *Opts),
) (
cipherString string,
err error,
cipherString []byte, err error,
) {
var o Opts
@@ -70,7 +70,7 @@ func Encrypt(
); chk.E(err) {
return
}
plain := []byte(plaintext)
plain := plaintext
size := len(plain)
if size < MinPlaintextSize || size > MaxPlaintextSize {
err = errorf.E("plaintext should be between 1b and 64kB")
@@ -93,14 +93,15 @@ func Encrypt(
ct = append(ct, o.nonce...)
ct = append(ct, cipher...)
ct = append(ct, mac...)
cipherString = base64.StdEncoding.EncodeToString(ct)
cipherString = make([]byte, base64.StdEncoding.EncodedLen(len(ct)))
base64.StdEncoding.Encode(cipherString, ct)
return
}
// Decrypt data that has been encoded using a provided symmetric conversation
// key using NIP-44 encryption (chacha20 cipher stream and sha256 HMAC).
func Decrypt(b64ciphertextWrapped string, conversationKey []byte) (
plaintext string,
func Decrypt(b64ciphertextWrapped, conversationKey []byte) (
plaintext []byte,
err error,
) {
cLen := len(b64ciphertextWrapped)
@@ -108,12 +109,12 @@ func Decrypt(b64ciphertextWrapped string, conversationKey []byte) (
err = errorf.E("invalid payload length: %d", cLen)
return
}
if b64ciphertextWrapped[:1] == "#" {
if len(b64ciphertextWrapped) > 0 && b64ciphertextWrapped[0] == '#' {
err = errorf.E("unknown version")
return
}
var decoded []byte
if decoded, err = base64.StdEncoding.DecodeString(b64ciphertextWrapped); chk.E(err) {
if decoded, err = base64.StdEncoding.DecodeString(string(b64ciphertextWrapped)); chk.E(err) {
return
}
if decoded[0] != version {
@@ -153,7 +154,7 @@ func Decrypt(b64ciphertextWrapped string, conversationKey []byte) (
err = errorf.E("invalid padding")
return
}
plaintext = string(unpadded)
plaintext = unpadded
return
}
@@ -167,8 +168,16 @@ func GenerateConversationKey(pkh, skh string) (ck []byte, err error) {
)
return
}
var sign signer.I
if sign, err = p256k.NewSecFromHex(skh); chk.E(err) {
return
}
var pk []byte
if pk, err = p256k.HexToBin(pkh); chk.E(err) {
return
}
var shared []byte
if shared, err = ComputeSharedSecret(pkh, skh); chk.E(err) {
if shared, err = sign.ECDH(pk); chk.E(err) {
return
}
ck = hkdf.Extract(sha256.New, shared, []byte("nip44-v2"))

View File

@@ -19,10 +19,10 @@ func assertCryptPriv(
sk1, sk2, conversationKey, salt, plaintext, expected string,
) {
var (
k1, s []byte
actual, decrypted string
ok bool
err error
k1, s, plaintextBytes, actualBytes,
expectedBytes, decrypted []byte
ok bool
err error
)
k1, err = hex.Dec(conversationKey)
if ok = assert.NoErrorf(
@@ -41,25 +41,27 @@ func assertCryptPriv(
); !ok {
return
}
actual, err = Encrypt(plaintext, k1, WithCustomNonce(s))
plaintextBytes = []byte(plaintext)
actualBytes, err = Encrypt(plaintextBytes, k1, WithCustomNonce(s))
if ok = assert.NoError(t, err, "encryption failed: %v", err); !ok {
return
}
if ok = assert.Equalf(t, expected, actual, "wrong encryption"); !ok {
expectedBytes = []byte(expected)
if ok = assert.Equalf(t, string(expectedBytes), string(actualBytes), "wrong encryption"); !ok {
return
}
decrypted, err = Decrypt(expected, k1)
decrypted, err = Decrypt(expectedBytes, k1)
if ok = assert.NoErrorf(t, err, "decryption failed: %v", err); !ok {
return
}
assert.Equal(t, decrypted, plaintext, "wrong decryption")
assert.Equal(t, decrypted, plaintextBytes, "wrong decryption")
}
func assertDecryptFail(
t *testing.T, conversationKey, plaintext, ciphertext, msg string,
) {
var (
k1 []byte
k1, ciphertextBytes []byte
ok bool
err error
)
@@ -69,7 +71,8 @@ func assertDecryptFail(
); !ok {
return
}
_, err = Decrypt(ciphertext, k1)
ciphertextBytes = []byte(ciphertext)
_, err = Decrypt(ciphertextBytes, k1)
assert.ErrorContains(t, err, msg)
}
@@ -196,15 +199,15 @@ func assertMessageKeyGeneration(
}
func assertCryptLong(
t *testing.T, conversationKey, salt, pattern string, repeat int,
t *testing.T, conversationKey, salt string, pattern []byte, repeat int,
plaintextSha256, payloadSha256 string,
) {
var (
convKey, convSalt []byte
plaintext, actualPlaintextSha256, actualPayload, actualPayloadSha256 string
h hash.Hash
ok bool
err error
convKey, convSalt, plaintext, payloadBytes []byte
actualPlaintextSha256, actualPayloadSha256 string
h hash.Hash
ok bool
err error
)
convKey, err = hex.Dec(conversationKey)
if ok = assert.NoErrorf(
@@ -218,12 +221,12 @@ func assertCryptLong(
); !ok {
return
}
plaintext = ""
plaintext = make([]byte, 0, len(pattern)*repeat)
for i := 0; i < repeat; i++ {
plaintext += pattern
plaintext = append(plaintext, pattern...)
}
h = sha256.New()
h.Write([]byte(plaintext))
h.Write(plaintext)
actualPlaintextSha256 = hex.Enc(h.Sum(nil))
if ok = assert.Equalf(
t, plaintextSha256, actualPlaintextSha256,
@@ -231,12 +234,14 @@ func assertCryptLong(
); !ok {
return
}
actualPayload, err = Encrypt(plaintext, convKey, WithCustomNonce(convSalt))
payloadBytes, err = Encrypt(
plaintext, convKey, WithCustomNonce(convSalt),
)
if ok = assert.NoErrorf(t, err, "encryption failed: %v", err); !ok {
return
}
h.Reset()
h.Write([]byte(actualPayload))
h.Write(payloadBytes)
actualPayloadSha256 = hex.Enc(h.Sum(nil))
if ok = assert.Equalf(
t, payloadSha256, actualPayloadSha256,
@@ -383,7 +388,7 @@ func TestCryptLong001(t *testing.T) {
t,
"8fc262099ce0d0bb9b89bac05bb9e04f9bc0090acc181fef6840ccee470371ed",
"326bcb2c943cd6bb717588c9e5a7e738edf6ed14ec5f5344caa6ef56f0b9cff7",
"x",
[]byte("x"),
65535,
"09ab7495d3e61a76f0deb12cb0306f0696cbb17ffc12131368c7a939f12f56d3",
"90714492225faba06310bff2f249ebdc2a5e609d65a629f1c87f2d4ffc55330a",
@@ -395,7 +400,7 @@ func TestCryptLong002(t *testing.T) {
t,
"56adbe3720339363ab9c3b8526ffce9fd77600927488bfc4b59f7a68ffe5eae0",
"ad68da81833c2a8ff609c3d2c0335fd44fe5954f85bb580c6a8d467aa9fc5dd0",
"!",
[]byte("!"),
65535,
"6af297793b72ae092c422e552c3bb3cbc310da274bd1cf9e31023a7fe4a2d75e",
"8013e45a109fad3362133132b460a2d5bce235fe71c8b8f4014793fb52a49844",
@@ -407,7 +412,7 @@ func TestCryptLong003(t *testing.T) {
t,
"7fc540779979e472bb8d12480b443d1e5eb1098eae546ef2390bee499bbf46be",
"34905e82105c20de9a2f6cd385a0d541e6bcc10601d12481ff3a7575dc622033",
"🦄",
[]byte("🦄"),
16383,
"a249558d161b77297bc0cb311dde7d77190f6571b25c7e4429cd19044634a61f",
"b3348422471da1f3c59d79acfe2fe103f3cd24488109e5b18734cdb5953afd15",
@@ -1309,7 +1314,10 @@ func TestMaxLength(t *testing.T) {
rand.Read(salt)
conversationKey, _ := GenerateConversationKey(pub2, string(sk1))
plaintext := strings.Repeat("a", MaxPlaintextSize)
encrypted, err := Encrypt(plaintext, conversationKey, WithCustomNonce(salt))
plaintextBytes := []byte(plaintext)
encrypted, err := Encrypt(
plaintextBytes, conversationKey, WithCustomNonce(salt),
)
if chk.E(err) {
t.Error(err)
}
@@ -1321,7 +1329,7 @@ func TestMaxLength(t *testing.T) {
fmt.Sprintf("%x", conversationKey),
fmt.Sprintf("%x", salt),
plaintext,
encrypted,
string(encrypted),
)
}
@@ -1330,10 +1338,10 @@ func assertCryptPub(
sk1, pub2, conversationKey, salt, plaintext, expected string,
) {
var (
k1, s []byte
actual, decrypted string
ok bool
err error
k1, s, plaintextBytes,
actualBytes, expectedBytes, decrypted []byte
ok bool
err error
)
k1, err = hex.Dec(conversationKey)
if ok = assert.NoErrorf(
@@ -1352,16 +1360,18 @@ func assertCryptPub(
); !ok {
return
}
actual, err = Encrypt(plaintext, k1, WithCustomNonce(s))
plaintextBytes = []byte(plaintext)
actualBytes, err = Encrypt(plaintextBytes, k1, WithCustomNonce(s))
if ok = assert.NoError(t, err, "encryption failed: %v", err); !ok {
return
}
if ok = assert.Equalf(t, expected, actual, "wrong encryption"); !ok {
expectedBytes = []byte(expected)
if ok = assert.Equalf(t, string(expectedBytes), string(actualBytes), "wrong encryption"); !ok {
return
}
decrypted, err = Decrypt(expected, k1)
decrypted, err = Decrypt(expectedBytes, k1)
if ok = assert.NoErrorf(t, err, "decryption failed: %v", err); !ok {
return
}
assert.Equal(t, decrypted, plaintext, "wrong decryption")
assert.Equal(t, decrypted, plaintextBytes, "wrong decryption")
}

View File

@@ -18,3 +18,7 @@ type Signer = btcec.Signer
type Keygen = btcec.Keygen
func NewKeygen() (k *Keygen) { return new(Keygen) }
var NewSecFromHex = btcec.NewSecFromHex
var NewPubFromHex = btcec.NewPubFromHex
var HexToBin = btcec.HexToBin

View File

@@ -55,10 +55,20 @@ func (s *Signer) InitPub(pub []byte) (err error) {
}
// Sec returns the raw secret key bytes.
func (s *Signer) Sec() (b []byte) { return s.skb }
func (s *Signer) Sec() (b []byte) {
if s == nil {
return nil
}
return s.skb
}
// Pub returns the raw BIP-340 schnorr public key bytes.
func (s *Signer) Pub() (b []byte) { return s.pkb }
func (s *Signer) Pub() (b []byte) {
if s == nil {
return nil
}
return s.pkb
}
// Sign a message with the Signer. Requires an initialised secret key.
func (s *Signer) Sign(msg []byte) (sig []byte, err error) {

View File

@@ -0,0 +1,40 @@
//go:build !cgo
package btcec
import (
"orly.dev/pkg/encoders/hex"
"orly.dev/pkg/interfaces/signer"
"orly.dev/pkg/utils/chk"
)
func NewSecFromHex[V []byte | string](skh V) (sign signer.I, err error) {
var sk []byte
if _, err = hex.DecBytes(sk, []byte(skh)); chk.E(err) {
return
}
sign = &Signer{}
if err = sign.InitSec(sk); chk.E(err) {
return
}
return
}
func NewPubFromHex[V []byte | string](pkh V) (sign signer.I, err error) {
var sk []byte
if _, err = hex.DecBytes(sk, []byte(pkh)); chk.E(err) {
return
}
sign = &Signer{}
if err = sign.InitPub(sk); chk.E(err) {
return
}
return
}
func HexToBin(hexStr string) (b []byte, err error) {
if _, err = hex.DecBytes(b, []byte(hexStr)); chk.E(err) {
return
}
return
}

View File

@@ -0,0 +1,40 @@
//go:build cgo
package p256k
import (
"orly.dev/pkg/encoders/hex"
"orly.dev/pkg/interfaces/signer"
"orly.dev/pkg/utils/chk"
)
func NewSecFromHex[V []byte | string](skh V) (sign signer.I, err error) {
var sk []byte
if _, err = hex.DecBytes(sk, []byte(skh)); chk.E(err) {
return
}
sign = &Signer{}
if err = sign.InitSec(sk); chk.E(err) {
return
}
return
}
func NewPubFromHex[V []byte | string](pkh V) (sign signer.I, err error) {
var sk []byte
if _, err = hex.DecBytes(sk, []byte(pkh)); chk.E(err) {
return
}
sign = &Signer{}
if err = sign.InitPub(sk); chk.E(err) {
return
}
return
}
func HexToBin(hexStr string) (b []byte, err error) {
if _, err = hex.DecBytes(b, []byte(hexStr)); chk.E(err) {
return
}
return
}

View File

@@ -77,8 +77,18 @@ func (s *Signer) InitPub(pub []byte) (err error) {
return
}
func (s *Signer) Sec() (b []byte) { return s.skb }
func (s *Signer) Pub() (b []byte) { return s.pkb }
func (s *Signer) Sec() (b []byte) {
if s == nil {
return nil
}
return s.skb
}
func (s *Signer) Pub() (b []byte) {
if s == nil {
return nil
}
return s.pkb
}
// func (s *Signer) ECPub() (b []byte) { return s.pkb }

View File

@@ -61,7 +61,7 @@ func TestSignerVerify(t *testing.T) {
continue
}
if valid, err = signer.Verify(id, ev.Sig); chk.E(err) {
t.Errorf("failed to verify: %s\n%0x", err, ev.Id)
t.Errorf("failed to verify: %s\n%0x", err, ev.ID)
continue
}
if !valid {

View File

@@ -2,7 +2,6 @@ package database
import (
"github.com/dgraph-io/badger/v4"
"io"
"orly.dev/pkg/encoders/eventidserial"
"orly.dev/pkg/utils/apputil"
"orly.dev/pkg/utils/chk"
@@ -75,11 +74,6 @@ func (d *D) Wipe() (err error) {
panic("implement me")
}
func (d *D) Import(r io.Reader) {
// TODO implement me
panic("implement me")
}
func (d *D) SetLogLevel(level string) {
d.Logger.SetLogLevel(lol.GetLogLevel(level))
}
@@ -101,6 +95,7 @@ func (d *D) Init(path string) (err error) {
// Sync flushes the database buffers to disk.
func (d *D) Sync() (err error) {
d.DB.RunValueLogGC(0.5)
return d.DB.Sync()
}

View File

@@ -23,7 +23,7 @@ func (d *D) DeleteEvent(c context.T, eid *eventid.T) (err error) {
return
}
if ser == nil {
// Event not found, nothing to delete
// Event wasn't found, nothing to delete
return
}
// Fetch the event to get its data
@@ -33,7 +33,7 @@ func (d *D) DeleteEvent(c context.T, eid *eventid.T) (err error) {
return
}
if ev == nil {
// Event not found, nothing to delete
// Event wasn't found, nothing to delete
return
}
// Get all indexes for the event

View File

@@ -6,7 +6,6 @@ import (
"io"
"orly.dev/pkg/database/indexes"
"orly.dev/pkg/database/indexes/types"
"orly.dev/pkg/encoders/codecbuf"
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/context"
@@ -17,34 +16,39 @@ import (
// JSON.
func (d *D) Export(c context.T, w io.Writer, pubkeys ...[]byte) {
var err error
evB := make([]byte, 0, units.Mb)
evBuf := bytes.NewBuffer(evB)
if len(pubkeys) == 0 {
if err = d.View(
func(txn *badger.Txn) (err error) {
buf := codecbuf.Get()
defer codecbuf.Put(buf)
buf := new(bytes.Buffer)
if err = indexes.EventEnc(nil).MarshalWrite(buf); chk.E(err) {
return
}
it := txn.NewIterator(badger.IteratorOptions{Prefix: buf.Bytes()})
evB := make([]byte, 0, units.Mb)
defer it.Close()
for it.Rewind(); it.Valid(); it.Next() {
item := it.Item()
if evB, err = item.ValueCopy(evB); chk.E(err) {
if err = item.Value(
func(val []byte) (err error) {
evBuf.Write(val)
return
},
); chk.E(err) {
continue
}
evBuf := bytes.NewBuffer(evB)
ev := event.New()
if err = ev.UnmarshalBinary(evBuf); chk.E(err) {
continue
}
// Serialize the event to JSON and write it to the output
if _, err = w.Write(ev.Serialize()); chk.E(err) {
continue
return
}
if _, err = w.Write([]byte{'\n'}); chk.E(err) {
continue
return
}
evBuf.Reset()
}
return
},
@@ -55,8 +59,7 @@ func (d *D) Export(c context.T, w io.Writer, pubkeys ...[]byte) {
for _, pubkey := range pubkeys {
if err = d.View(
func(txn *badger.Txn) (err error) {
pkBuf := codecbuf.Get()
defer codecbuf.Put(pkBuf)
pkBuf := new(bytes.Buffer)
ph := &types.PubHash{}
if err = ph.FromPubkey(pubkey); chk.E(err) {
return
@@ -67,14 +70,17 @@ func (d *D) Export(c context.T, w io.Writer, pubkeys ...[]byte) {
return
}
it := txn.NewIterator(badger.IteratorOptions{Prefix: pkBuf.Bytes()})
evB := make([]byte, 0, units.Mb)
defer it.Close()
for it.Rewind(); it.Valid(); it.Next() {
item := it.Item()
if evB, err = item.ValueCopy(evB); chk.E(err) {
if err = item.Value(
func(val []byte) (err error) {
evBuf.Write(val)
return
},
); chk.E(err) {
continue
}
evBuf := bytes.NewBuffer(evB)
ev := event.New()
if err = ev.UnmarshalBinary(evBuf); chk.E(err) {
continue
@@ -86,6 +92,7 @@ func (d *D) Export(c context.T, w io.Writer, pubkeys ...[]byte) {
if _, err = w.Write([]byte{'\n'}); chk.E(err) {
continue
}
evBuf.Reset()
}
return
},

View File

@@ -55,7 +55,7 @@ func TestExport(t *testing.T) {
}
// Save the event to the database
if _, _, err = db.SaveEvent(ctx, ev); err != nil {
if _, _, err = db.SaveEvent(ctx, ev, false, nil); err != nil {
t.Fatalf("Failed to save event: %v", err)
}

View File

@@ -5,7 +5,6 @@ import (
"github.com/dgraph-io/badger/v4"
"orly.dev/pkg/database/indexes"
"orly.dev/pkg/database/indexes/types"
"orly.dev/pkg/encoders/codecbuf"
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/utils/chk"
)
@@ -13,8 +12,7 @@ import (
func (d *D) FetchEventBySerial(ser *types.Uint40) (ev *event.E, err error) {
if err = d.View(
func(txn *badger.Txn) (err error) {
buf := codecbuf.Get()
defer codecbuf.Put(buf)
buf := new(bytes.Buffer)
if err = indexes.EventEnc(ser).MarshalWrite(buf); chk.E(err) {
return
}

View File

@@ -56,7 +56,7 @@ func TestFetchEventBySerial(t *testing.T) {
events = append(events, ev)
// Save the event to the database
if _, _, err = db.SaveEvent(ctx, ev); err != nil {
if _, _, err = db.SaveEvent(ctx, ev, false, nil); err != nil {
t.Fatalf("Failed to save event #%d: %v", eventCount+1, err)
}
@@ -78,7 +78,7 @@ func TestFetchEventBySerial(t *testing.T) {
var sers types.Uint40s
sers, err = db.QueryForSerials(
ctx, &filter.F{
Ids: tag.New(testEvent.Id),
Ids: tag.New(testEvent.ID),
},
)
if err != nil {
@@ -102,10 +102,10 @@ func TestFetchEventBySerial(t *testing.T) {
}
// Verify the fetched event has the same ID as the original event
if !bytes.Equal(fetchedEvent.Id, testEvent.Id) {
if !bytes.Equal(fetchedEvent.ID, testEvent.ID) {
t.Fatalf(
"Fetched event ID doesn't match original event ID. Got %x, expected %x",
fetchedEvent.Id, testEvent.Id,
fetchedEvent.ID, testEvent.ID,
)
}

View File

@@ -5,10 +5,8 @@ import (
"github.com/dgraph-io/badger/v4"
"orly.dev/pkg/database/indexes"
"orly.dev/pkg/database/indexes/types"
"orly.dev/pkg/encoders/codecbuf"
"orly.dev/pkg/interfaces/store"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/errorf"
)
func (d *D) GetFullIdPubkeyBySerial(ser *types.Uint40) (
@@ -16,8 +14,7 @@ func (d *D) GetFullIdPubkeyBySerial(ser *types.Uint40) (
) {
if err = d.View(
func(txn *badger.Txn) (err error) {
buf := codecbuf.Get()
defer codecbuf.Put(buf)
buf := new(bytes.Buffer)
if err = indexes.FullIdPubkeyEnc(
ser, nil, nil, nil,
).MarshalWrite(buf); chk.E(err) {
@@ -54,11 +51,5 @@ func (d *D) GetFullIdPubkeyBySerial(ser *types.Uint40) (
); chk.E(err) {
return
}
if fidpk != nil {
err = errorf.E(
"failed to fetch full id pubkey by serial %d",
ser.Get(),
)
}
return
}

View File

@@ -39,9 +39,9 @@ func GetIndexesForEvent(ev *event.E, serial uint64) (
if err = ser.Set(serial); chk.E(err) {
return
}
// Id index
// ID index
idHash := new(IdHash)
if err = idHash.FromId(ev.Id); chk.E(err) {
if err = idHash.FromId(ev.ID); chk.E(err) {
return
}
idIndex := indexes.IdEnc(idHash, ser)
@@ -50,7 +50,7 @@ func GetIndexesForEvent(ev *event.E, serial uint64) (
}
// FullIdPubkey index
fullID := new(Id)
if err = fullID.FromId(ev.Id); chk.E(err) {
if err = fullID.FromId(ev.ID); chk.E(err) {
return
}
pubHash := new(PubHash)

View File

@@ -60,7 +60,7 @@ func testBasicEvent(t *testing.T) {
for i := range id {
id[i] = byte(i)
}
ev.Id = id
ev.ID = id
// Set Pubkey
pubkey := make([]byte, 32)
@@ -92,7 +92,7 @@ func testBasicEvent(t *testing.T) {
// Create and verify the expected indexes
// 1. Id index
// 1. ID index
ser := new(types2.Uint40)
err = ser.Set(serial)
if chk.E(err) {
@@ -100,7 +100,7 @@ func testBasicEvent(t *testing.T) {
}
idHash := new(types2.IdHash)
err = idHash.FromId(ev.Id)
err = idHash.FromId(ev.ID)
if chk.E(err) {
t.Fatalf("Failed to create IdHash: %v", err)
}
@@ -109,9 +109,9 @@ func testBasicEvent(t *testing.T) {
// 2. FullIdPubkey index
fullID := new(types2.Id)
err = fullID.FromId(ev.Id)
err = fullID.FromId(ev.ID)
if chk.E(err) {
t.Fatalf("Failed to create Id: %v", err)
t.Fatalf("Failed to create ID: %v", err)
}
pubHash := new(types2.PubHash)
@@ -156,7 +156,7 @@ func testEventWithTags(t *testing.T) {
for i := range id {
id[i] = byte(i)
}
ev.Id = id
ev.ID = id
// Set Pubkey
pubkey := make([]byte, 32)
@@ -210,7 +210,7 @@ func testEventWithTags(t *testing.T) {
}
idHash := new(types2.IdHash)
err = idHash.FromId(ev.Id)
err = idHash.FromId(ev.ID)
if chk.E(err) {
t.Fatalf("Failed to create IdHash: %v", err)
}
@@ -268,7 +268,7 @@ func testErrorHandling(t *testing.T) {
for i := range id {
id[i] = byte(i)
}
ev.Id = id
ev.ID = id
// Set Pubkey
pubkey := make([]byte, 32)

View File

@@ -76,7 +76,7 @@ func CreatePubHashFromData(data []byte) (p *types2.PubHash, err error) {
// complete set of combinations of all fields in the event, thus there is no
// need to decode events until they are to be delivered.
func GetIndexesFromFilter(f *filter.F) (idxs []Range, err error) {
// Id eid
// ID eid
//
// If there is any Ids in the filter, none of the other fields matter. It
// should be an error, but convention just ignores it.

View File

@@ -19,7 +19,7 @@ import (
// TestGetIndexesFromFilter tests the GetIndexesFromFilter function
func TestGetIndexesFromFilter(t *testing.T) {
t.Run("Id", testIdFilter)
t.Run("ID", testIdFilter)
t.Run("Pubkey", testPubkeyFilter)
t.Run("CreatedAt", testCreatedAtFilter)
t.Run("CreatedAtUntil", testCreatedAtUntilFilter)
@@ -77,9 +77,9 @@ func verifyIndex(
}
}
// Test Id filter
// Test ID filter
func testIdFilter(t *testing.T) {
// Create a filter with an Id
// Create a filter with an ID
f := filter.New()
id := make([]byte, sha256.Size)
for i := range id {
@@ -102,7 +102,7 @@ func testIdFilter(t *testing.T) {
expectedIdx := indexes.IdEnc(idHash, nil)
// Verify the generated index
// For Id filter, both start and end indexes are the same
// For ID filter, both start and end indexes are the same
verifyIndex(t, idxs, expectedIdx, expectedIdx)
}

View File

@@ -53,7 +53,7 @@ func TestGetSerialById(t *testing.T) {
events = append(events, ev)
// Save the event to the database
if _, _, err = db.SaveEvent(ctx, ev); err != nil {
if _, _, err = db.SaveEvent(ctx, ev, false, nil); err != nil {
t.Fatalf("Failed to save event #%d: %v", eventCount+1, err)
}
@@ -71,7 +71,7 @@ func TestGetSerialById(t *testing.T) {
testEvent := events[3] // Using the same event as in QueryForIds test
// Get the serial by ID
serial, err := db.GetSerialById(testEvent.Id)
serial, err := db.GetSerialById(testEvent.ID)
if err != nil {
t.Fatalf("Failed to get serial by ID: %v", err)
}
@@ -82,10 +82,10 @@ func TestGetSerialById(t *testing.T) {
}
// Test with a non-existent ID
nonExistentId := make([]byte, len(testEvent.Id))
nonExistentId := make([]byte, len(testEvent.ID))
// Ensure it's different from any real ID
for i := range nonExistentId {
nonExistentId[i] = ^testEvent.Id[i]
nonExistentId[i] = ^testEvent.ID[i]
}
serial, err = db.GetSerialById(nonExistentId)

View File

@@ -60,12 +60,12 @@ func TestGetSerialsByRange(t *testing.T) {
events = append(events, ev)
// Save the event to the database
if _, _, err = db.SaveEvent(ctx, ev); err != nil {
if _, _, err = db.SaveEvent(ctx, ev, false, nil); err != nil {
t.Fatalf("Failed to save event #%d: %v", eventCount+1, err)
}
// Get the serial for this event
serial, err := db.GetSerialById(ev.Id)
serial, err := db.GetSerialById(ev.ID)
if err != nil {
t.Fatalf(
"Failed to get serial for event #%d: %v", eventCount+1, err,
@@ -73,7 +73,7 @@ func TestGetSerialsByRange(t *testing.T) {
}
if serial != nil {
eventSerials[string(ev.Id)] = serial
eventSerials[string(ev.ID)] = serial
}
eventCount++

82
pkg/database/import.go Normal file
View File

@@ -0,0 +1,82 @@
package database
import (
"bufio"
"io"
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/log"
"os"
"runtime/debug"
)
const maxLen = 500000000
// Import a collection of events in line structured minified JSON format (JSONL).
func (d *D) Import(rr io.Reader) {
// store to disk so we can return fast
tmpPath := os.TempDir() + string(os.PathSeparator) + "orly"
os.MkdirAll(tmpPath, 0700)
tmp, err := os.CreateTemp(tmpPath, "")
if chk.E(err) {
return
}
log.I.F("buffering upload to %s", tmp.Name())
if _, err = io.Copy(tmp, rr); chk.E(err) {
return
}
if _, err = tmp.Seek(0, 0); chk.E(err) {
return
}
go func() {
var err error
// Create a scanner to read the buffer line by line
scan := bufio.NewScanner(tmp)
scanBuf := make([]byte, maxLen)
scan.Buffer(scanBuf, maxLen)
var count, total int
for scan.Scan() {
select {
case <-d.ctx.Done():
log.I.F("context closed")
return
default:
}
b := scan.Bytes()
total += len(b) + 1
if len(b) < 1 {
continue
}
ev := &event.E{}
if _, err = ev.Unmarshal(b); err != nil {
continue
}
if _, _, err = d.SaveEvent(d.ctx, ev, false, nil); err != nil {
continue
}
b = nil
ev = nil
count++
if count%100 == 0 {
log.I.F("received %d events", count)
debug.FreeOSMemory()
}
}
log.I.F("read %d bytes and saved %d events", total, count)
err = scan.Err()
if chk.E(err) {
}
// Help garbage collection
tmp = nil
}()
return
}

View File

@@ -186,7 +186,7 @@ func EventDec(ser *types.Uint40) (enc *T) { return New(NewPrefix(), ser) }
// Id contains a truncated 8-byte hash of an event index. This is the secondary
// key of an event, the primary key is the serial found in the Event.
//
// 3 prefix|8 Id hash|5 serial
// 3 prefix|8 ID hash|5 serial
var Id = next()
func IdVars() (id *types.IdHash, ser *types.Uint40) {
@@ -202,7 +202,7 @@ func IdDec(id *types.IdHash, ser *types.Uint40) (enc *T) {
// FullIdPubkey is an index designed to enable sorting and filtering of
// results found via other indexes, without having to decode the event.
//
// 3 prefix|5 serial|32 Id|8 pubkey hash|8 timestamp
// 3 prefix|5 serial|32 ID|8 pubkey hash|8 timestamp
var FullIdPubkey = next()
func FullIdPubkeyVars() (

View File

@@ -83,7 +83,7 @@ func TestPrefixFunction(t *testing.T) {
expected I
}{
{"Event", Event, EventPrefix},
{"Id", Id, IdPrefix},
{"ID", Id, IdPrefix},
{"FullIdPubkey", FullIdPubkey, FullIdPubkeyPrefix},
{"Pubkey", Pubkey, PubkeyPrefix},
{"CreatedAt", CreatedAt, CreatedAtPrefix},
@@ -122,7 +122,7 @@ func TestIdentify(t *testing.T) {
expected int
}{
{"Event", EventPrefix, Event},
{"Id", IdPrefix, Id},
{"ID", IdPrefix, Id},
{"FullIdPubkey", FullIdPubkeyPrefix, FullIdPubkey},
{"Pubkey", PubkeyPrefix, Pubkey},
{"CreatedAt", CreatedAtPrefix, CreatedAt},

View File

@@ -15,7 +15,7 @@ type Id struct {
func (fi *Id) FromId(id []byte) (err error) {
if len(id) != IdLen {
err = errorf.E(
"fullid.FromId: invalid Id length, got %d require %d", len(id),
"fullid.FromId: invalid ID length, got %d require %d", len(id),
IdLen,
)
return

View File

@@ -43,7 +43,7 @@ func TestFromId(t *testing.T) {
}
func TestIdMarshalWriteUnmarshalRead(t *testing.T) {
// Create a Id with a known value
// Create a ID with a known value
fi1 := &Id{}
validId := make([]byte, sha256.Size)
for i := 0; i < sha256.Size; i++ {
@@ -80,7 +80,7 @@ func TestIdMarshalWriteUnmarshalRead(t *testing.T) {
}
func TestIdUnmarshalReadWithCorruptedData(t *testing.T) {
// Create a Id with a known value
// Create a ID with a known value
fi1 := &Id{}
validId := make([]byte, sha256.Size)
for i := 0; i < sha256.Size; i++ {
@@ -91,7 +91,7 @@ func TestIdUnmarshalReadWithCorruptedData(t *testing.T) {
t.Fatalf("FromId failed: %v", err)
}
// Create a second Id with a different value
// Create a second ID with a different value
fi2 := &Id{}
differentId := make([]byte, sha256.Size)
for i := 0; i < sha256.Size; i++ {

View File

@@ -23,7 +23,7 @@ func (i *IdHash) Set(idh []byte) {
func (i *IdHash) FromId(id []byte) (err error) {
if len(id) != sha256.Size {
err = errorf.E(
"FromId: invalid Id length, got %d require %d", len(id),
"FromId: invalid ID length, got %d require %d", len(id),
sha256.Size,
)
return
@@ -43,7 +43,7 @@ func (i *IdHash) FromIdBase64(idb64 string) (err error) {
// Check if the decoded ID has the correct length
if len(decoded) != sha256.Size {
err = errorf.E(
"FromIdBase64: invalid Id length, got %d require %d", len(decoded),
"FromIdBase64: invalid ID length, got %d require %d", len(decoded),
sha256.Size,
)
return
@@ -62,7 +62,7 @@ func (i *IdHash) FromIdHex(idh string) (err error) {
}
if len(id) != sha256.Size {
err = errorf.E(
"FromIdHex: invalid Id length, got %d require %d", len(id),
"FromIdHex: invalid ID length, got %d require %d", len(id),
sha256.Size,
)
return

View File

@@ -3,7 +3,6 @@ package types
import (
"bytes"
"io"
"orly.dev/pkg/encoders/codecbuf"
"orly.dev/pkg/utils/chk"
)
@@ -29,7 +28,7 @@ func (ts *Timestamp) ToTimestamp() (timestamp int64) {
func (ts *Timestamp) Bytes() (b []byte, err error) {
v := new(Uint64)
v.Set(uint64(ts.val))
buf := codecbuf.Get()
buf := new(bytes.Buffer)
if err = v.MarshalWrite(buf); chk.E(err) {
return
}

View File

@@ -1,8 +1,8 @@
package types
import (
"bytes"
"io"
"orly.dev/pkg/encoders/codecbuf"
"orly.dev/pkg/utils/chk"
)
@@ -35,8 +35,7 @@ func (w *Word) MarshalWrite(wr io.Writer) (err error) {
// UnmarshalRead reads the word from the reader, stopping at the zero-byte marker
func (w *Word) UnmarshalRead(r io.Reader) error {
buf := codecbuf.Get()
defer codecbuf.Put(buf)
buf := new(bytes.Buffer)
tmp := make([]byte, 1)
foundEndMarker := false

View File

@@ -45,7 +45,7 @@ func TestMultipleParameterizedReplaceableEvents(t *testing.T) {
baseEvent.Sign(sign)
// Save the base parameterized replaceable event
if _, _, err := db.SaveEvent(ctx, baseEvent); err != nil {
if _, _, err := db.SaveEvent(ctx, baseEvent, false, nil); err != nil {
t.Fatalf("Failed to save base parameterized replaceable event: %v", err)
}
@@ -63,7 +63,7 @@ func TestMultipleParameterizedReplaceableEvents(t *testing.T) {
newerEvent.Sign(sign)
// Save the newer parameterized replaceable event
if _, _, err := db.SaveEvent(ctx, newerEvent); err != nil {
if _, _, err := db.SaveEvent(ctx, newerEvent, false, nil); err != nil {
t.Fatalf(
"Failed to save newer parameterized replaceable event: %v", err,
)
@@ -83,7 +83,7 @@ func TestMultipleParameterizedReplaceableEvents(t *testing.T) {
newestEvent.Sign(sign)
// Save the newest parameterized replaceable event
if _, _, err := db.SaveEvent(ctx, newestEvent); err != nil {
if _, _, err := db.SaveEvent(ctx, newestEvent, false, nil); err != nil {
t.Fatalf(
"Failed to save newest parameterized replaceable event: %v", err,
)
@@ -127,10 +127,10 @@ func TestMultipleParameterizedReplaceableEvents(t *testing.T) {
}
// Verify it's the newest event
if !bytes.Equal(evs[0].Id, newestEvent.Id) {
if !bytes.Equal(evs[0].ID, newestEvent.ID) {
t.Fatalf(
"Event ID doesn't match the newest event. Got %x, expected %x",
evs[0].Id, newestEvent.Id,
evs[0].ID, newestEvent.ID,
)
}
@@ -145,7 +145,7 @@ func TestMultipleParameterizedReplaceableEvents(t *testing.T) {
// Query for the base event by ID
evs, err = db.QueryEvents(
ctx, &filter.F{
Ids: tag.New(baseEvent.Id),
Ids: tag.New(baseEvent.ID),
},
)
if err != nil {
@@ -161,10 +161,10 @@ func TestMultipleParameterizedReplaceableEvents(t *testing.T) {
}
// Verify it's the base event
if !bytes.Equal(evs[0].Id, baseEvent.Id) {
if !bytes.Equal(evs[0].ID, baseEvent.ID) {
t.Fatalf(
"Event ID doesn't match when querying for base event by ID. Got %x, expected %x",
evs[0].Id, baseEvent.Id,
evs[0].ID, baseEvent.ID,
)
}
}

View File

@@ -16,11 +16,6 @@ import (
"strconv"
)
// QueryEvents retrieves events based on the provided filter. If the filter
// contains Ids, it fetches events by those Ids directly, overriding other
// filter criteria. Otherwise, it queries by other filter criteria and fetches
// matching events. Results are returned in reverse chronological order of their
// creation timestamps.
func (d *D) QueryEvents(c context.T, f *filter.F) (evs event.S, err error) {
// if there is Ids in the query, this overrides anything else
if f.Ids != nil && f.Ids.Len() > 0 {
@@ -46,6 +41,7 @@ func (d *D) QueryEvents(c context.T, f *filter.F) (evs event.S, err error) {
} else {
var idPkTs []store.IdPkTs
if idPkTs, err = d.QueryForIds(c, f); chk.E(err) {
return
}
// Create a map to store the latest version of replaceable events
@@ -217,7 +213,7 @@ func (d *D) QueryEvents(c context.T, f *filter.F) (evs event.S, err error) {
isIdInFilter := false
if f.Ids != nil && f.Ids.Len() > 0 {
for i := 0; i < f.Ids.Len(); i++ {
if bytes.Equal(ev.Id, f.Ids.B(i)) {
if bytes.Equal(ev.ID, f.Ids.B(i)) {
isIdInFilter = true
break
}

View File

@@ -62,7 +62,7 @@ func setupTestDB(t *testing.T) (
events = append(events, ev)
// Save the event to the database
if _, _, err = db.SaveEvent(ctx, ev); err != nil {
if _, _, err = db.SaveEvent(ctx, ev, false, nil); err != nil {
t.Fatalf("Failed to save event #%d: %v", eventCount+1, err)
}
@@ -90,7 +90,7 @@ func TestQueryEventsByID(t *testing.T) {
evs, err := db.QueryEvents(
ctx, &filter.F{
Ids: tag.New(testEvent.Id),
Ids: tag.New(testEvent.ID),
},
)
if err != nil {
@@ -103,10 +103,10 @@ func TestQueryEventsByID(t *testing.T) {
}
// Verify it's the correct event
if !bytes.Equal(evs[0].Id, testEvent.Id) {
if !bytes.Equal(evs[0].ID, testEvent.ID) {
t.Fatalf(
"Event ID doesn't match. Got %x, expected %x", evs[0].Id,
testEvent.Id,
"Event ID doesn't match. Got %x, expected %x", evs[0].ID,
testEvent.ID,
)
}
}
@@ -202,7 +202,9 @@ func TestReplaceableEventsAndDeletion(t *testing.T) {
replaceableEvent.Tags = tags.New()
replaceableEvent.Sign(sign)
// Save the replaceable event
if _, _, err := db.SaveEvent(ctx, replaceableEvent); err != nil {
if _, _, err := db.SaveEvent(
ctx, replaceableEvent, false, nil,
); err != nil {
t.Fatalf("Failed to save replaceable event: %v", err)
}
@@ -216,14 +218,14 @@ func TestReplaceableEventsAndDeletion(t *testing.T) {
newerEvent.Tags = tags.New()
newerEvent.Sign(sign)
// Save the newer event
if _, _, err := db.SaveEvent(ctx, newerEvent); err != nil {
if _, _, err := db.SaveEvent(ctx, newerEvent, false, nil); err != nil {
t.Fatalf("Failed to save newer event: %v", err)
}
// Query for the original event by ID
evs, err := db.QueryEvents(
ctx, &filter.F{
Ids: tag.New(replaceableEvent.Id),
Ids: tag.New(replaceableEvent.ID),
},
)
if err != nil {
@@ -239,10 +241,10 @@ func TestReplaceableEventsAndDeletion(t *testing.T) {
}
// Verify it's the original event
if !bytes.Equal(evs[0].Id, replaceableEvent.Id) {
if !bytes.Equal(evs[0].ID, replaceableEvent.ID) {
t.Fatalf(
"Event ID doesn't match when querying for replaced event. Got %x, expected %x",
evs[0].Id, replaceableEvent.Id,
evs[0].ID, replaceableEvent.ID,
)
}
@@ -269,10 +271,10 @@ func TestReplaceableEventsAndDeletion(t *testing.T) {
}
// Verify it's the newer event
if !bytes.Equal(evs[0].Id, newerEvent.Id) {
if !bytes.Equal(evs[0].ID, newerEvent.ID) {
t.Fatalf(
"Event ID doesn't match when querying for replaceable events. Got %x, expected %x",
evs[0].Id, newerEvent.Id,
evs[0].ID, newerEvent.ID,
)
}
@@ -289,11 +291,11 @@ func TestReplaceableEventsAndDeletion(t *testing.T) {
// Add an e-tag referencing the replaceable event
deletionEvent.Tags = deletionEvent.Tags.AppendTags(
tag.New([]byte{'e'}, []byte(hex.Enc(replaceableEvent.Id))),
tag.New([]byte{'e'}, []byte(hex.Enc(replaceableEvent.ID))),
)
// Save the deletion event
if _, _, err = db.SaveEvent(ctx, deletionEvent); err != nil {
if _, _, err = db.SaveEvent(ctx, deletionEvent, false, nil); err != nil {
t.Fatalf("Failed to save deletion event: %v", err)
}
@@ -319,17 +321,17 @@ func TestReplaceableEventsAndDeletion(t *testing.T) {
}
// Verify it's still the newer event
if !bytes.Equal(evs[0].Id, newerEvent.Id) {
if !bytes.Equal(evs[0].ID, newerEvent.ID) {
t.Fatalf(
"Event ID doesn't match after deletion. Got %x, expected %x",
evs[0].Id, newerEvent.Id,
evs[0].ID, newerEvent.ID,
)
}
// Query for the original event by ID
evs, err = db.QueryEvents(
ctx, &filter.F{
Ids: tag.New(replaceableEvent.Id),
Ids: tag.New(replaceableEvent.ID),
},
)
if err != nil {
@@ -345,10 +347,10 @@ func TestReplaceableEventsAndDeletion(t *testing.T) {
}
// Verify it's the original event
if !bytes.Equal(evs[0].Id, replaceableEvent.Id) {
if !bytes.Equal(evs[0].ID, replaceableEvent.ID) {
t.Fatalf(
"Event ID doesn't match when querying for deleted event by ID. Got %x, expected %x",
evs[0].Id, replaceableEvent.Id,
evs[0].ID, replaceableEvent.ID,
)
}
}
@@ -379,7 +381,7 @@ func TestParameterizedReplaceableEventsAndDeletion(t *testing.T) {
paramEvent.Sign(sign)
// Save the parameterized replaceable event
if _, _, err := db.SaveEvent(ctx, paramEvent); err != nil {
if _, _, err := db.SaveEvent(ctx, paramEvent, false, nil); err != nil {
t.Fatalf("Failed to save parameterized replaceable event: %v", err)
}
@@ -405,7 +407,9 @@ func TestParameterizedReplaceableEventsAndDeletion(t *testing.T) {
paramDeletionEvent.Sign(sign)
// Save the parameterized deletion event
if _, _, err := db.SaveEvent(ctx, paramDeletionEvent); err != nil {
if _, _, err := db.SaveEvent(
ctx, paramDeletionEvent, false, nil,
); err != nil {
t.Fatalf("Failed to save parameterized deletion event: %v", err)
}
@@ -433,12 +437,14 @@ func TestParameterizedReplaceableEventsAndDeletion(t *testing.T) {
paramDeletionEvent2.Tags = tags.New()
// Add an e-tag referencing the parameterized replaceable event
paramDeletionEvent2.Tags = paramDeletionEvent2.Tags.AppendTags(
tag.New([]byte{'e'}, []byte(hex.Enc(paramEvent.Id))),
tag.New([]byte{'e'}, []byte(hex.Enc(paramEvent.ID))),
)
paramDeletionEvent2.Sign(sign)
// Save the parameterized deletion event with e-tag
if _, _, err := db.SaveEvent(ctx, paramDeletionEvent2); err != nil {
if _, _, err := db.SaveEvent(
ctx, paramDeletionEvent2, false, nil,
); err != nil {
t.Fatalf(
"Failed to save parameterized deletion event with e-tag: %v", err,
)
@@ -483,7 +489,7 @@ func TestParameterizedReplaceableEventsAndDeletion(t *testing.T) {
// Query for the parameterized event by ID
evs, err = db.QueryEvents(
ctx, &filter.F{
Ids: tag.New(paramEvent.Id),
Ids: tag.New(paramEvent.ID),
},
)
if err != nil {
@@ -501,10 +507,10 @@ func TestParameterizedReplaceableEventsAndDeletion(t *testing.T) {
}
// Verify it's the correct event
if !bytes.Equal(evs[0].Id, paramEvent.Id) {
if !bytes.Equal(evs[0].ID, paramEvent.ID) {
t.Fatalf(
"Event ID doesn't match when querying for deleted parameterized event by ID. Got %x, expected %x",
evs[0].Id, paramEvent.Id,
evs[0].ID, paramEvent.ID,
)
}
}

View File

@@ -57,7 +57,7 @@ func TestQueryForAuthorsTags(t *testing.T) {
events = append(events, ev)
// Save the event to the database
if _, _, err = db.SaveEvent(ctx, ev); err != nil {
if _, _, err = db.SaveEvent(ctx, ev, false, nil); err != nil {
t.Fatalf("Failed to save event #%d: %v", eventCount+1, err)
}
@@ -131,7 +131,7 @@ func TestQueryForAuthorsTags(t *testing.T) {
// Find the event with this ID
var found bool
for _, ev := range events {
if bytes.Equal(result.Id, ev.Id) {
if bytes.Equal(result.Id, ev.ID) {
found = true
if !bytes.Equal(ev.Pubkey, testEvent.Pubkey) {

View File

@@ -56,7 +56,7 @@ func TestQueryForCreatedAt(t *testing.T) {
events = append(events, ev)
// Save the event to the database
if _, _, err = db.SaveEvent(ctx, ev); err != nil {
if _, _, err = db.SaveEvent(ctx, ev, false, nil); err != nil {
t.Fatalf("Failed to save event #%d: %v", eventCount+1, err)
}
@@ -105,7 +105,7 @@ func TestQueryForCreatedAt(t *testing.T) {
// Find the event with this ID
var found bool
for _, ev := range events {
if bytes.Equal(result.Id, ev.Id) {
if bytes.Equal(result.Id, ev.ID) {
found = true
break
}
@@ -143,7 +143,7 @@ func TestQueryForCreatedAt(t *testing.T) {
// Find the event with this ID
var found bool
for _, ev := range events {
if bytes.Equal(result.Id, ev.Id) {
if bytes.Equal(result.Id, ev.ID) {
found = true
break
}
@@ -181,7 +181,7 @@ func TestQueryForCreatedAt(t *testing.T) {
// Find the event with this ID
var found bool
for _, ev := range events {
if bytes.Equal(result.Id, ev.Id) {
if bytes.Equal(result.Id, ev.ID) {
found = true
break
}

View File

@@ -43,6 +43,9 @@ func (d *D) QueryForIds(c context.T, f *filter.F) (
if fidpk, err = d.GetFullIdPubkeyBySerial(ser); chk.E(err) {
return
}
if fidpk == nil {
continue
}
tagIdPkTs = append(tagIdPkTs, *fidpk)
}
} else {
@@ -59,6 +62,9 @@ func (d *D) QueryForIds(c context.T, f *filter.F) (
if fidpk, err = d.GetFullIdPubkeyBySerial(ser); chk.E(err) {
return
}
if fidpk == nil {
continue
}
temp = append(temp, *fidpk)
}
var intersecting []store.IdPkTs
@@ -93,6 +99,9 @@ func (d *D) QueryForIds(c context.T, f *filter.F) (
if fidpk, err = d.GetFullIdPubkeyBySerial(ser); chk.E(err) {
return
}
if fidpk == nil {
continue
}
idPkTs = append(idPkTs, *fidpk)
}
}

View File

@@ -60,7 +60,7 @@ func TestQueryForIds(t *testing.T) {
events = append(events, ev)
// Save the event to the database
if _, _, err = db.SaveEvent(ctx, ev); err != nil {
if _, _, err = db.SaveEvent(ctx, ev, false, nil); err != nil {
t.Fatalf("Failed to save event #%d: %v", eventCount+1, err)
}
@@ -86,34 +86,34 @@ func TestQueryForIds(t *testing.T) {
len(idTsPk),
)
}
if !bytes.Equal(idTsPk[0].Id, events[5474].Id) {
if !bytes.Equal(idTsPk[0].Id, events[5474].ID) {
t.Fatalf(
"failed to get expected event, got %0x, expected %0x", idTsPk[0].Id,
events[5474].Id,
events[5474].ID,
)
}
if !bytes.Equal(idTsPk[1].Id, events[272].Id) {
if !bytes.Equal(idTsPk[1].Id, events[272].ID) {
t.Fatalf(
"failed to get expected event, got %0x, expected %0x", idTsPk[1].Id,
events[272].Id,
events[272].ID,
)
}
if !bytes.Equal(idTsPk[2].Id, events[1].Id) {
if !bytes.Equal(idTsPk[2].Id, events[1].ID) {
t.Fatalf(
"failed to get expected event, got %0x, expected %0x", idTsPk[2].Id,
events[1].Id,
events[1].ID,
)
}
if !bytes.Equal(idTsPk[3].Id, events[80].Id) {
if !bytes.Equal(idTsPk[3].Id, events[80].ID) {
t.Fatalf(
"failed to get expected event, got %0x, expected %0x", idTsPk[3].Id,
events[80].Id,
events[80].ID,
)
}
if !bytes.Equal(idTsPk[4].Id, events[123].Id) {
if !bytes.Equal(idTsPk[4].Id, events[123].ID) {
t.Fatalf(
"failed to get expected event, got %0x, expected %0x", idTsPk[4].Id,
events[123].Id,
events[123].ID,
)
}
@@ -141,7 +141,7 @@ func TestQueryForIds(t *testing.T) {
// Find the event with this ID
var found bool
for _, ev := range events {
if bytes.Equal(result.Id, ev.Id) {
if bytes.Equal(result.Id, ev.ID) {
found = true
if ev.Kind.K != testKind.K {
t.Fatalf(
@@ -207,7 +207,7 @@ func TestQueryForIds(t *testing.T) {
// Find the event with this ID
var found bool
for _, ev := range events {
if bytes.Equal(result.Id, ev.Id) {
if bytes.Equal(result.Id, ev.ID) {
found = true
// Check if the event has the tag we're looking for
@@ -258,7 +258,7 @@ func TestQueryForIds(t *testing.T) {
// Find the event with this ID
var found bool
for _, ev := range events {
if bytes.Equal(result.Id, ev.Id) {
if bytes.Equal(result.Id, ev.ID) {
found = true
if ev.Kind.K != testKind.K {
t.Fatalf(
@@ -305,7 +305,7 @@ func TestQueryForIds(t *testing.T) {
// Find the event with this ID
var found bool
for _, ev := range events {
if bytes.Equal(result.Id, ev.Id) {
if bytes.Equal(result.Id, ev.ID) {
found = true
if ev.Kind.K != testEvent.Kind.K {
t.Fatalf(
@@ -366,7 +366,7 @@ func TestQueryForIds(t *testing.T) {
// Find the event with this ID
var found bool
for _, ev := range events {
if bytes.Equal(result.Id, ev.Id) {
if bytes.Equal(result.Id, ev.ID) {
found = true
if ev.Kind.K != testEvent.Kind.K {
t.Fatalf(
@@ -433,7 +433,7 @@ func TestQueryForIds(t *testing.T) {
// Find the event with this ID
var found bool
for _, ev := range events {
if bytes.Equal(result.Id, ev.Id) {
if bytes.Equal(result.Id, ev.ID) {
found = true
if !bytes.Equal(ev.Pubkey, testEvent.Pubkey) {
@@ -506,7 +506,7 @@ func TestQueryForIds(t *testing.T) {
// Find the event with this ID
var found bool
for _, ev := range events {
if bytes.Equal(result.Id, ev.Id) {
if bytes.Equal(result.Id, ev.ID) {
found = true
break
}

View File

@@ -58,7 +58,7 @@ func TestQueryForKindsAuthorsTags(t *testing.T) {
events = append(events, ev)
// Save the event to the database
if _, _, err = db.SaveEvent(ctx, ev); err != nil {
if _, _, err = db.SaveEvent(ctx, ev, false, nil); err != nil {
t.Fatalf("Failed to save event #%d: %v", eventCount+1, err)
}
@@ -136,7 +136,7 @@ func TestQueryForKindsAuthorsTags(t *testing.T) {
// Find the event with this ID
var found bool
for _, ev := range events {
if bytes.Equal(result.Id, ev.Id) {
if bytes.Equal(result.Id, ev.ID) {
found = true
if ev.Kind.K != testKind.K {
t.Fatalf(

View File

@@ -58,7 +58,7 @@ func TestQueryForKindsAuthors(t *testing.T) {
events = append(events, ev)
// Save the event to the database
if _, _, err = db.SaveEvent(ctx, ev); err != nil {
if _, _, err = db.SaveEvent(ctx, ev, false, nil); err != nil {
t.Fatalf("Failed to save event #%d: %v", eventCount+1, err)
}
@@ -102,7 +102,7 @@ func TestQueryForKindsAuthors(t *testing.T) {
// Find the event with this ID
var found bool
for _, ev := range events {
if bytes.Equal(result.Id, ev.Id) {
if bytes.Equal(result.Id, ev.ID) {
found = true
if ev.Kind.K != testKind.K {
t.Fatalf(

View File

@@ -58,7 +58,7 @@ func TestQueryForKindsTags(t *testing.T) {
events = append(events, ev)
// Save the event to the database
if _, _, err = db.SaveEvent(ctx, ev); err != nil {
if _, _, err = db.SaveEvent(ctx, ev, false, nil); err != nil {
t.Fatalf("Failed to save event #%d: %v", eventCount+1, err)
}
@@ -132,7 +132,7 @@ func TestQueryForKindsTags(t *testing.T) {
// Find the event with this ID
var found bool
for _, ev := range events {
if bytes.Equal(result.Id, ev.Id) {
if bytes.Equal(result.Id, ev.ID) {
found = true
if ev.Kind.K != testKind.K {
t.Fatalf(

View File

@@ -57,7 +57,7 @@ func TestQueryForKinds(t *testing.T) {
events = append(events, ev)
// Save the event to the database
if _, _, err = db.SaveEvent(ctx, ev); err != nil {
if _, _, err = db.SaveEvent(ctx, ev, false, nil); err != nil {
t.Fatalf("Failed to save event #%d: %v", eventCount+1, err)
}
@@ -96,7 +96,7 @@ func TestQueryForKinds(t *testing.T) {
// Find the event with this ID
var found bool
for _, ev := range events {
if bytes.Equal(result.Id, ev.Id) {
if bytes.Equal(result.Id, ev.ID) {
found = true
if ev.Kind.K != testKind.K {
t.Fatalf(

View File

@@ -31,6 +31,9 @@ func (d *D) QueryForSerials(c context.T, f *filter.F) (
if fidpk, err = d.GetFullIdPubkeyBySerial(ser); chk.E(err) {
return
}
if fidpk == nil {
continue
}
idPkTs = append(idPkTs, *fidpk)
// sort by timestamp
sort.Slice(

View File

@@ -60,12 +60,12 @@ func TestQueryForSerials(t *testing.T) {
events = append(events, ev)
// Save the event to the database
if _, _, err = db.SaveEvent(ctx, ev); err != nil {
if _, _, err = db.SaveEvent(ctx, ev, false, nil); err != nil {
t.Fatalf("Failed to save event #%d: %v", eventCount+1, err)
}
// Get the serial for this event
serial, err := db.GetSerialById(ev.Id)
serial, err := db.GetSerialById(ev.ID)
if err != nil {
t.Fatalf(
"Failed to get serial for event #%d: %v", eventCount+1, err,
@@ -73,7 +73,7 @@ func TestQueryForSerials(t *testing.T) {
}
if serial != nil {
eventSerials[string(ev.Id)] = serial
eventSerials[string(ev.ID)] = serial
}
eventCount++
@@ -91,7 +91,7 @@ func TestQueryForSerials(t *testing.T) {
serials, err := db.QueryForSerials(
ctx, &filter.F{
Ids: tag.New(testEvent.Id),
Ids: tag.New(testEvent.ID),
},
)
if err != nil {
@@ -110,10 +110,10 @@ func TestQueryForSerials(t *testing.T) {
t.Fatalf("Failed to fetch event for serial: %v", err)
}
if !bytes.Equal(ev.Id, testEvent.Id) {
if !bytes.Equal(ev.ID, testEvent.ID) {
t.Fatalf(
"Event ID doesn't match. Got %x, expected %x",
ev.Id, testEvent.Id,
ev.ID, testEvent.ID,
)
}

View File

@@ -57,7 +57,7 @@ func TestQueryForTags(t *testing.T) {
events = append(events, ev)
// Save the event to the database
if _, _, err = db.SaveEvent(ctx, ev); err != nil {
if _, _, err = db.SaveEvent(ctx, ev, false, nil); err != nil {
t.Fatalf("Failed to save event #%d: %v", eventCount+1, err)
}
@@ -126,7 +126,7 @@ func TestQueryForTags(t *testing.T) {
// Find the event with this ID
var found bool
for _, ev := range events {
if bytes.Equal(result.Id, ev.Id) {
if bytes.Equal(result.Id, ev.ID) {
found = true
// Check if the event has the tag we're looking for

View File

@@ -20,11 +20,17 @@ import (
)
// SaveEvent saves an event to the database, generating all the necessary indexes.
func (d *D) SaveEvent(c context.T, ev *event.E) (kc, vc int, err error) {
// Get a buffer from the pool
buf := new(bytes.Buffer)
// Marshal the event to binary
ev.MarshalBinary(buf)
func (d *D) SaveEvent(
c context.T, ev *event.E, noVerify bool, owners [][]byte,
) (kc, vc int, err error) {
if !noVerify {
// check if the event already exists
var ser *types.Uint40
if ser, err = d.GetSerialById(ev.ID); err == nil && ser != nil {
err = errorf.E("event already exists: %0x", ev.ID)
return
}
}
// check if an existing delete event references this event submission
if ev.Kind.IsParameterizedReplaceable() {
@@ -59,13 +65,16 @@ func (d *D) SaveEvent(c context.T, ev *event.E) (kc, vc int, err error) {
// stable value but refers to any event from the author, of the
// kind, with the identifier. so we need to fetch the full ID index
// to get the timestamp and ensure that the event post-dates it.
// otherwise it should be rejected.
// otherwise, it should be rejected.
var idPkTss []*store.IdPkTs
for _, ser := range sers {
var fidpk *store.IdPkTs
if fidpk, err = d.GetFullIdPubkeyBySerial(ser); chk.E(err) {
return
}
if fidpk == nil {
continue
}
idPkTss = append(idPkTss, fidpk)
}
// sort by timestamp, so the first is the newest
@@ -77,7 +86,7 @@ func (d *D) SaveEvent(c context.T, ev *event.E) (kc, vc int, err error) {
if ev.CreatedAt.I64() < idPkTss[0].Ts {
err = errorf.E(
"blocked: %0x was deleted by address %s because it is older than the delete: event: %d delete: %d",
ev.Id, at, ev.CreatedAt.I64(), idPkTss[0].Ts,
ev.ID, at, ev.CreatedAt.I64(), idPkTss[0].Ts,
)
return
}
@@ -85,17 +94,19 @@ func (d *D) SaveEvent(c context.T, ev *event.E) (kc, vc int, err error) {
}
} else {
var idxs []Range
// log.I.S(ev.Pubkey)
keys := [][]byte{ev.Pubkey}
for _, owner := range owners {
keys = append(keys, owner)
}
if idxs, err = GetIndexesFromFilter(
&filter.F{
Authors: tag.New(ev.Pubkey),
Authors: tag.New(keys...),
Kinds: kinds.New(kind.Deletion),
Tags: tags.New(tag.New([]byte("#e"), ev.Id)),
Tags: tags.New(tag.New([]byte("#e"), ev.ID)),
},
); chk.E(err) {
return
}
// log.I.S(idxs)
var sers types.Uint40s
for _, idx := range idxs {
var s types.Uint40s
@@ -108,7 +119,7 @@ func (d *D) SaveEvent(c context.T, ev *event.E) (kc, vc int, err error) {
// really there can only be one of these; the chances of an idhash
// collision are basically zero in practice, at least, one in a
// billion or more anyway, more than a human is going to create.
err = errorf.E("blocked: %0x was deleted by event Id", ev.Id)
err = errorf.E("blocked: event %0x deleted by event ID", ev.ID)
return
}
}

View File

@@ -64,7 +64,7 @@ func TestSaveEvents(t *testing.T) {
// Save the event to the database
var k, v int
if k, v, err = db.SaveEvent(ctx, ev); err != nil {
if k, v, err = db.SaveEvent(ctx, ev, false, nil); err != nil {
t.Fatalf("Failed to save event #%d: %v", eventCount+1, err)
}
kc += k
@@ -125,7 +125,7 @@ func TestDeletionEventWithETagRejection(t *testing.T) {
regularEvent.Sign(sign)
// Save the regular event
if _, _, err := db.SaveEvent(ctx, regularEvent); err != nil {
if _, _, err := db.SaveEvent(ctx, regularEvent, false, nil); err != nil {
t.Fatalf("Failed to save regular event: %v", err)
}
@@ -140,13 +140,13 @@ func TestDeletionEventWithETagRejection(t *testing.T) {
// Add an e-tag referencing the regular event
deletionEvent.Tags = deletionEvent.Tags.AppendTags(
tag.New([]byte{'e'}, []byte(hex.Enc(regularEvent.Id))),
tag.New([]byte{'e'}, []byte(hex.Enc(regularEvent.ID))),
)
deletionEvent.Sign(sign)
// Try to save the deletion event, it should be rejected
_, _, err = db.SaveEvent(ctx, deletionEvent)
_, _, err = db.SaveEvent(ctx, deletionEvent, false, nil)
if err == nil {
t.Fatal("Expected deletion event with e-tag to be rejected, but it was accepted")
}
@@ -154,6 +154,66 @@ func TestDeletionEventWithETagRejection(t *testing.T) {
// Verify the error message
expectedError := "deletion events referencing other events with 'e' tag are not allowed"
if err.Error() != expectedError {
t.Fatalf("Expected error message '%s', got '%s'", expectedError, err.Error())
t.Fatalf(
"Expected error message '%s', got '%s'", expectedError, err.Error(),
)
}
}
// TestSaveExistingEvent tests that attempting to save an event that already exists
// returns an error.
func TestSaveExistingEvent(t *testing.T) {
// Create a temporary directory for the database
tempDir, err := os.MkdirTemp("", "test-db-*")
if err != nil {
t.Fatalf("Failed to create temporary directory: %v", err)
}
defer os.RemoveAll(tempDir) // Clean up after the test
// Create a context and cancel function for the database
ctx, cancel := context.Cancel(context.Bg())
defer cancel()
// Initialize the database
db, err := New(ctx, cancel, tempDir, "info")
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer db.Close()
// Create a signer
sign := new(p256k.Signer)
if err := sign.Generate(); chk.E(err) {
t.Fatal(err)
}
// Create an event
ev := event.New()
ev.Kind = kind.TextNote // Kind 1 is a text note
ev.Pubkey = sign.Pub()
ev.CreatedAt = new(timestamp.T)
ev.CreatedAt.V = timestamp.Now().V
ev.Content = []byte("Test event")
ev.Tags = tags.New()
ev.Sign(sign)
// Save the event for the first time
if _, _, err := db.SaveEvent(ctx, ev, false, nil); err != nil {
t.Fatalf("Failed to save event: %v", err)
}
// Try to save the same event again, it should be rejected
_, _, err = db.SaveEvent(ctx, ev, false, nil)
if err == nil {
t.Fatal("Expected error when saving an existing event, but got nil")
}
// Verify the error message
expectedErrorPrefix := "event already exists: "
if !bytes.HasPrefix([]byte(err.Error()), []byte(expectedErrorPrefix)) {
t.Fatalf(
"Expected error message to start with '%s', got '%s'",
expectedErrorPrefix, err.Error(),
)
}
}

View File

@@ -232,7 +232,7 @@ func EncodeEvent(
return bech32.Encode(NeventHRP, bits5)
}
// EncodeEntity encodes a pubkey, kind, event Id, and relay hints.
// EncodeEntity encodes a pubkey, kind, event ID, and relay hints.
func EncodeEntity(pk []byte, k *kind.T, id []byte, relays [][]byte) (
s []byte, err error,
) {

View File

@@ -13,7 +13,7 @@ type Profile struct {
Relays [][]byte `json:"relays,omitempty"`
}
// Event pointer is the combination of an event Id, relay hints, author, pubkey,
// Event pointer is the combination of an event ID, relay hints, author, pubkey,
// and kind.
type Event struct {
ID *eventid.T `json:"id"`

View File

@@ -1,59 +0,0 @@
# Codecbuf - Concurrent-Safe Bytes Buffer Pool
This package provides a concurrent-safe pool of `bytes.Buffer` objects for encoding data. It helps reduce memory allocations and improve performance by reusing buffers instead of creating new ones for each operation.
## Usage
### Basic Usage
```go
// Get a buffer from the default pool
buf := codecbuf.Get()
// Use the buffer
buf.WriteString("Hello, World!")
// ... do more operations with the buffer ...
// Return the buffer to the pool when done
codecbuf.Put(buf)
```
### Using with defer
```go
func ProcessData() {
// Get a buffer from the default pool
buf := codecbuf.Get()
// Return the buffer to the pool when the function exits
defer codecbuf.Put(buf)
// Use the buffer
buf.WriteString("Hello, World!")
// ... do more operations with the buffer ...
}
```
### Creating a Custom Pool
```go
// Create a new buffer pool
pool := codecbuf.NewPool()
// Get a buffer from the custom pool
buf := pool.Get()
// Use the buffer
buf.WriteString("Hello, World!")
// Return the buffer to the custom pool
pool.Put(buf)
```
## Performance
Using a buffer pool can significantly improve performance in applications that frequently create and use byte buffers, especially in high-throughput scenarios. The pool reduces garbage collection pressure by reusing buffers instead of allocating new ones.
## Thread Safety
The buffer pool is safe for concurrent use by multiple goroutines. However, individual buffers obtained from the pool should not be used concurrently by multiple goroutines without additional synchronization.

View File

@@ -1,53 +0,0 @@
// Package codecbuf provides a concurrent-safe bytes buffer pool for encoding
// data.
package codecbuf
import (
"bytes"
"sync"
)
// Pool is a concurrent-safe pool of bytes.Buffer objects.
type Pool struct {
pool sync.Pool
}
// NewPool creates a new buffer pool.
func NewPool() *Pool {
return &Pool{
pool: sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
},
}
}
// Get returns a buffer from the pool or creates a new one if the pool is empty.
func (p *Pool) Get() *bytes.Buffer {
return p.pool.Get().(*bytes.Buffer)
}
// Put returns a buffer to the pool after zeroing its bytes for security and resetting it.
func (p *Pool) Put(buf *bytes.Buffer) {
// Zero out the bytes for security
data := buf.Bytes()
for i := range data {
data[i] = 0
}
buf.Reset()
p.pool.Put(buf)
}
// DefaultPool is the default buffer pool for the application.
var DefaultPool = NewPool()
// Get returns a buffer from the default pool.
func Get() *bytes.Buffer {
return DefaultPool.Get()
}
// Put returns a buffer to the default pool after zeroing its bytes for security.
func Put(buf *bytes.Buffer) {
DefaultPool.Put(buf)
}

View File

@@ -1,234 +0,0 @@
package codecbuf
import (
"bytes"
"testing"
)
func TestPool(t *testing.T) {
// Create a new pool
pool := NewPool()
// Get a buffer from the pool
buf := pool.Get()
if buf == nil {
t.Fatal("Expected non-nil buffer from pool")
}
// Write some data to the buffer
testData := "test data"
_, err := buf.WriteString(testData)
if err != nil {
t.Fatalf("Failed to write to buffer: %v", err)
}
// Verify the buffer contains the expected data
if buf.String() != testData {
t.Fatalf(
"Expected buffer to contain %q, got %q", testData, buf.String(),
)
}
// Put the buffer back in the pool
pool.Put(buf)
// Get another buffer from the pool (should be the same one, reset)
buf2 := pool.Get()
if buf2 == nil {
t.Fatal("Expected non-nil buffer from pool")
}
// Verify the buffer is empty (was reset)
if buf2.Len() != 0 {
t.Fatalf("Expected empty buffer, got buffer with length %d", buf2.Len())
}
// Write different data to the buffer
testData2 := "different data"
_, err = buf2.WriteString(testData2)
if err != nil {
t.Fatalf("Failed to write to buffer: %v", err)
}
// Verify the buffer contains the new data
if buf2.String() != testData2 {
t.Fatalf(
"Expected buffer to contain %q, got %q", testData2, buf2.String(),
)
}
}
func TestDefaultPool(t *testing.T) {
// Get a buffer from the default pool
buf := Get()
if buf == nil {
t.Fatal("Expected non-nil buffer from default pool")
}
// Write some data to the buffer
testData := "test data for default pool"
_, err := buf.WriteString(testData)
if err != nil {
t.Fatalf("Failed to write to buffer: %v", err)
}
// Verify the buffer contains the expected data
if buf.String() != testData {
t.Fatalf(
"Expected buffer to contain %q, got %q", testData, buf.String(),
)
}
// Put the buffer back in the pool
Put(buf)
// Get another buffer from the pool (should be reset)
buf2 := Get()
if buf2 == nil {
t.Fatal("Expected non-nil buffer from default pool")
}
// Verify the buffer is empty (was reset)
if buf2.Len() != 0 {
t.Fatalf("Expected empty buffer, got buffer with length %d", buf2.Len())
}
}
func TestZeroBytes(t *testing.T) {
// Create a new pool
pool := NewPool()
// Get a buffer from the pool
buf := pool.Get()
if buf == nil {
t.Fatal("Expected non-nil buffer from pool")
}
// Write some sensitive data to the buffer
sensitiveData := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
_, err := buf.Write(sensitiveData)
if err != nil {
t.Fatalf("Failed to write to buffer: %v", err)
}
// Get the capacity before putting it back
capacity := buf.Cap()
// Put the buffer back in the pool
pool.Put(buf)
// Get another buffer from the pool (should be the same one, reset)
buf2 := pool.Get()
if buf2 == nil {
t.Fatal("Expected non-nil buffer from pool")
}
// Verify the buffer is empty (was reset)
if buf2.Len() != 0 {
t.Fatalf("Expected empty buffer, got buffer with length %d", buf2.Len())
}
// Verify the capacity is the same (should be the same buffer)
if buf2.Cap() != capacity {
t.Fatalf("Expected capacity %d, got %d", capacity, buf2.Cap())
}
// Get the underlying bytes directly
// We need to grow the buffer to the same size as before to access the same memory
buf2.Grow(len(sensitiveData))
// Write some new data to the buffer to expose the underlying memory
newData := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF}
_, err = buf2.Write(newData)
if err != nil {
t.Fatalf("Failed to write to buffer: %v", err)
}
// Read the buffer bytes
bufBytes := buf2.Bytes()
// Verify that the sensitive data was zeroed out
// The new data should be there, but no trace of the old data
for i, b := range bufBytes[:len(newData)] {
if b != newData[i] {
t.Fatalf("Expected byte %d to be %d, got %d", i, newData[i], b)
}
}
}
func TestDefaultPoolZeroBytes(t *testing.T) {
// Get a buffer from the default pool
buf := Get()
if buf == nil {
t.Fatal("Expected non-nil buffer from default pool")
}
// Write some sensitive data to the buffer
sensitiveData := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
_, err := buf.Write(sensitiveData)
if err != nil {
t.Fatalf("Failed to write to buffer: %v", err)
}
// Get the capacity before putting it back
capacity := buf.Cap()
// Put the buffer back in the pool
Put(buf)
// Get another buffer from the pool (should be the same one, reset)
buf2 := Get()
if buf2 == nil {
t.Fatal("Expected non-nil buffer from default pool")
}
// Verify the buffer is empty (was reset)
if buf2.Len() != 0 {
t.Fatalf("Expected empty buffer, got buffer with length %d", buf2.Len())
}
// Verify the capacity is the same (should be the same buffer)
if buf2.Cap() != capacity {
t.Fatalf("Expected capacity %d, got %d", capacity, buf2.Cap())
}
// Get the underlying bytes directly
// We need to grow the buffer to the same size as before to access the same memory
buf2.Grow(len(sensitiveData))
// Write some new data to the buffer to expose the underlying memory
newData := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF}
_, err = buf2.Write(newData)
if err != nil {
t.Fatalf("Failed to write to buffer: %v", err)
}
// Read the buffer bytes
bufBytes := buf2.Bytes()
// Verify that the sensitive data was zeroed out
// The new data should be there, but no trace of the old data
for i, b := range bufBytes[:len(newData)] {
if b != newData[i] {
t.Fatalf("Expected byte %d to be %d, got %d", i, newData[i], b)
}
}
}
func BenchmarkWithPool(b *testing.B) {
pool := NewPool()
b.ResetTimer()
for i := 0; i < b.N; i++ {
buf := pool.Get()
buf.WriteString("benchmark test data")
pool.Put(buf)
}
}
func BenchmarkWithoutPool(b *testing.B) {
for i := 0; i < b.N; i++ {
buf := new(bytes.Buffer)
buf.WriteString("benchmark test data")
}
}

View File

@@ -171,7 +171,7 @@ func NewResponseWith(event *event.E) *Response { return &Response{Event: event}
// Label returns the label of a auth Response envelope.
func (en *Response) Label() string { return L }
func (en *Response) Id() []byte { return en.Event.Id }
func (en *Response) Id() []byte { return en.Event.ID }
// Write the Response to a provided io.Writer.
func (en *Response) Write(w io.Writer) (err error) {

View File

@@ -30,7 +30,7 @@ func New() *T {
return &T{Subscription: subscription.NewStd()}
}
// NewFrom creates a new closedenvelope.T populated with subscription Id and Reason.
// NewFrom creates a new closedenvelope.T populated with subscription ID and Reason.
func NewFrom(id *subscription.Id, msg []byte) *T {
return &T{
Subscription: id, Reason: msg,

View File

@@ -52,7 +52,7 @@ func TestMarshalUnmarshal(t *testing.T) {
if rem, err = req2.Unmarshal(rb); chk.E(err) {
t.Fatal(err)
}
// log.I.Ln(req2.Id)
// log.I.Ln(req2.ID)
if len(rem) > 0 {
t.Fatalf(
"unmarshal failed, remainder\n%d %s",

View File

@@ -24,7 +24,7 @@ var _ codec.Envelope = (*T)(nil)
// New creates an empty new standard formatted closeenvelope.T.
func New() *T { return &T{ID: subscription.NewStd()} }
// NewFrom creates a new closeenvelope.T populated with subscription Id.
// NewFrom creates a new closeenvelope.T populated with subscription ID.
func NewFrom(id *subscription.Id) *T { return &T{ID: id} }
// Label returns the label of a closeenvelope.T.

View File

@@ -34,7 +34,7 @@ func TestMarshalUnmarshal(t *testing.T) {
if rem, err = req2.Unmarshal(rb); chk.E(err) {
t.Fatal(err)
}
// log.I.Ln(req2.Id)
// log.I.Ln(req2.ID)
if len(rem) > 0 {
t.Fatalf(
"unmarshal failed, remainder\n%d %s",

View File

@@ -177,7 +177,7 @@ func (en *Response) Unmarshal(b []byte) (r []byte, err error) {
r = b
var inID, inCount bool
for ; len(r) > 0; r = r[1:] {
// first we should be finding a subscription Id
// first we should be finding a subscription ID
if !inID && r[0] == '"' {
r = r[1:]
// so we don't do this twice

View File

@@ -20,7 +20,7 @@ func TestMarshalUnmarshal(t *testing.T) {
}
req := NewFrom(s)
rb = req.Marshal(rb)
// log.I.Ln(req.Id)
// log.I.Ln(req.ID)
rb1 = rb1[:len(rb)]
copy(rb1, rb)
var rem []byte
@@ -35,7 +35,7 @@ func TestMarshalUnmarshal(t *testing.T) {
if rem, err = req2.Unmarshal(rb); chk.E(err) {
t.Fatal(err)
}
// log.I.Ln(req2.Id)
// log.I.Ln(req2.ID)
if len(rem) > 0 {
t.Fatalf(
"unmarshal failed, remainder\n%d %s",

Some files were not shown because too many files have changed in this diff Show More