Compare commits

...

50 Commits

Author SHA1 Message Date
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
9b7e8d28de remove redundant return 2025-07-22 15:02:46 +01:00
c16ee76638 add error message for failed fetch 2025-07-22 14:55:09 +01:00
132fdc9f36 Add test for NostrEscape and NostrUnescape handling in JSON tags
- Introduced a new test case `ExplicitlyEscapedJSON` in `json_tags_test.go`
- Validates the behavior of `NostrEscape` and `NostrUnescape` with explicitly escaped JSON
- Ensures proper marshaling, unmarshaling, and verification of nested and special characters in JSON tags
2025-07-22 13:16:09 +01:00
4f1d48c247 Refactor privilege check logs, enhance export functionality, and add new test coverage
- Updated privilege check log messages in `publisher.go` and `handleReq.go` for better clarity and consistency
- Improved event serialization by replacing `WriteTo` with `Serialize` in `export.go`
- Added unit tests for `Export` functionality in `export_test.go`
- Introduced tests for JSON tag handling in `json_tags_test.go`
- Simplified condition in `spider-fetch.go` by removing redundant checks
2025-07-22 12:47:03 +01:00
651791aec1 Add export functionality and fix privilege checks
- Added `Export` method in `database/database.go` to export events to an io.Writer
- Implemented detailed logic for exporting all or specific pubkeys' events
- Removed placeholder `Export` function with TODO comment from `database/database.go`
- Updated error handling in `handleReq.go` and `publisher.go` by using `err != nil` instead of `chk.E(err)`
- Added more detailed logging in privilege check conditions in both `publisher.go` and `handleReq.go`
- Introduced new imports such as `"fmt"` in `connection.go` for improved error message formatting
- Created a new file `export.go` under the `database` package with complete implementation of export functionality
2025-07-22 11:34:57 +01:00
53d649c64e Merge remote-tracking branch 'origin/main' 2025-07-21 15:05:19 +01:00
4dafab3fd6 Fix privilege check logic by inverting conditionals
- Inverted conditional in `publisher.go` to correctly skip unprivileged events
- Inverted conditional in `handleReq.go` to filter out unprivileged events properly
- Added fallback logic in `check-privilege.go` to mark as privileged if no conditions match
2025-07-21 15:05:07 +01:00
f2475c48b7 Fix privilege check logic in publisher and handleReq files
- Changed `auth.CheckPrivilege` condition from allowing to denying access if not privileged in `publisher.go`
- Updated `handleReq.go` to deny access if `auth.CheckPrivilege` returns false, improving consistency and security checks
2025-07-21 14:29:46 +01:00
b5448f4153 Add remote address to log and fix NIP20 prefix handling
- Updated `handleMessage.go` to include the real remote address in the log message when a message is received
- Removed `regexp` import and `NIP20prefixmatcher` variable from `publisher.go`
- Modified `server.go` to remove an unused parameter from the `AddEvent` method
- Added `NIP20prefixmatcher` variable and used it for checking error messages in `addEvent.go`
2025-07-21 13:35:10 +01:00
11d318d4e3 Update Challenge methods with detailed comments and improved functionality
- Added comprehensive documentation for Write, Marshal, Unmarshal, and ParseChallenge methods
- Improved method descriptions with parameters, return values, and expected behavior sections
- Enhanced clarity of implementation details in comments
- Standardized comment formatting across the file
2025-07-21 13:14:59 +01:00
53e8e160dd Fix check-privilege logic and remove redundant condition
- Removed unused `authedIsAuthor` variable and simplified privilege check logic in `check-privilege.go`
- Replaced conditional return based on `authedIsAuthor` with direct use of `privileged` flag
- Simplified the logic for checking if authed pubkey is mentioned in event tags
2025-07-21 11:36:57 +01:00
143 changed files with 17368 additions and 1408 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
}

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/"`
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,18 +1,47 @@
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"
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/interfaces/relay"
"orly.dev/pkg/interfaces/store"
"orly.dev/pkg/protocol/socketapi"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/normalize"
)
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.
//
@@ -50,9 +79,8 @@ import (
// - Returns a boolean indicating whether the event was accepted and any
// relevant message.
func (s *Server) AddEvent(
c context.T, rl relay.I, ev *event.E,
hr *http.Request, origin string,
authedPubkey []byte,
c context.T, rl relay.I, ev *event.E, hr *http.Request, origin string,
pubkeys [][]byte,
) (accepted bool, message []byte) {
if ev == nil {
@@ -65,9 +93,12 @@ func (s *Server) AddEvent(
return false, []byte(saveErr.Error())
}
errmsg := saveErr.Error()
if socketapi.NIP20prefixmatcher.MatchString(errmsg) {
if NIP20prefixmatcher.MatchString(errmsg) {
if strings.Contains(errmsg, "tombstone") {
return false, normalize.Error.F("event was deleted, not storing it again")
return false, normalize.Error.F(
"%s event was deleted, not storing it again",
origin,
)
}
if strings.HasPrefix(errmsg, string(normalize.Blocked)) {
return false, []byte(errmsg)
@@ -80,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
}

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

@@ -0,0 +1,69 @@
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)
}
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,
); 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,
); 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,15 @@ 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); 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

@@ -1,136 +1,252 @@
package relay
import (
"fmt"
"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/kind"
"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"
"orly.dev/pkg/utils/lol"
"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 *kind.T, noFetch bool, pubkeys ...[]byte,
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: kinds.New(k),
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
}
if len(evs) < len(pubkeys) && !noFetch {
// 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{}{}
}
}
}
// Nil the event to free memory
ev = nil
}
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 512
for i := 0; i < len(pubkeys); i += 512 {
end := i + 512
// Break up pubkeys into batches of 128
for i := 0; i < len(pubkeys); i += 128 {
end := i + 128
if end > len(pubkeys) {
end = len(pubkeys)
}
batchPubkeys := pubkeys[i:end]
log.I.F(
"processing batch %d to %d of %d for kind %s",
i, end, len(pubkeys), k.Name(),
i, end, len(pubkeys), kindsList,
)
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: kinds.New(k),
Kinds: k,
Authors: batchPkList,
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:
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
}
log.I.F("event %0x is valid", ev.ID)
}
// Save the event to the database
if _, _, err = s.Storage().SaveEvent(
s.Ctx, ev, true, // already verified
); chk.E(err) {
err = nil
continue
}
if lol.Level.Load() == lol.Trace {
log.T.C(
func() string {
return fmt.Sprintf(
"saved event:\n%s", ev.Marshal(nil),
)
},
)
} else {
log.I.F("saved event: %0x", ev.ID)
}
// 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{}{}
}
}
}
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()
for _, ev := range evss {
evs = append(evs, ev)
}
mx.Unlock()
}()
}
wg.Wait()
}
// save the events to the database
for _, ev := range evs {
if _, _, err = s.Storage().SaveEvent(s.Ctx, ev); chk.E(err) {
err = nil
continue
// Nil the event in the slice to free memory
evss[i] = nil
}
}
}
}
if !k.Equal(kind.FollowList) {
chk.E(s.Storage().Sync())
debug.FreeOSMemory()
// If we're in noExtract mode, just return
if noExtract {
return
}
// deduplicate and take the newest
var tmp event.S
evMap := make(map[string]event.S)
for _, ev := range evs {
evMap[ev.PubKeyString()] = append(evMap[ev.PubKeyString()], ev)
}
for _, evm := range evMap {
if len(evm) < 1 {
continue
}
if len(evm) > 1 {
sort.Sort(evm)
}
tmp = append(tmp, evm[0])
}
evs = tmp
// we have all we're going to get now
pkMap := make(map[string]struct{})
for _, ev := range evs {
t := ev.Tags.GetAll(tag.New("p"))
for _, tt := range t.ToSliceOfTags() {
pkh := tt.Value()
if len(pkh) != 2*schnorr.PubKeyBytesLen {
continue
}
pk := make([]byte, schnorr.PubKeyBytesLen)
if _, err = hex.DecBytes(pk, pkh); chk.E(err) {
err = nil
continue
}
pkMap[string(pk)] = struct{}{}
}
}
// Convert the collected pubkeys to the return format
for pk := range pkMap {
pks = append(pks, []byte(pk))
}
log.I.F("found %d pks", len(pks))
return
}

View File

@@ -2,40 +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)
@@ -52,21 +31,22 @@ func (s *Server) Spider(noFetch ...bool) (err error) {
log.I.F("getting ownersFollowed")
var ownersFollowed [][]byte
if ownersFollowed, err = s.SpiderFetch(
kind.FollowList, dontFetch, ownersPubkeys...,
kinds.New(kind.FollowList), dontFetch, false, ownersPubkeys...,
); chk.E(err) {
return
}
// log.I.S(ownersFollowed)
log.I.F("getting followedFollows")
var followedFollows [][]byte
if followedFollows, err = s.SpiderFetch(
kind.FollowList, dontFetch, ownersFollowed...,
kinds.New(kind.FollowList), dontFetch, false, ownersFollowed...,
); chk.E(err) {
return
}
log.I.F("getting ownersMuted")
var ownersMuted [][]byte
if ownersMuted, err = s.SpiderFetch(
kind.MuteList, dontFetch, ownersPubkeys...,
kinds.New(kind.MuteList), dontFetch, false, ownersPubkeys...,
); chk.E(err) {
return
}
@@ -74,22 +54,17 @@ func (s *Server) Spider(noFetch ...bool) (err error) {
// list
filteredFollows := make([][]byte, 0, len(followedFollows))
for _, follow := range followedFollows {
found := false
for _, owner := range ownersFollowed {
if bytes.Equal(follow, owner) {
found = true
break
}
}
for _, owner := range ownersMuted {
if bytes.Equal(follow, owner) {
found = true
break
}
}
if !found {
filteredFollows = append(filteredFollows, follow)
}
filteredFollows = append(filteredFollows, follow)
}
followedFollows = filteredFollows
own := "owner"
@@ -115,19 +90,26 @@ func (s *Server) Spider(noFetch ...bool) (err error) {
len(followedFollows), folfol,
len(ownersMuted), mut,
)
// add the owners
// add the owners to the ownersFollowed
ownersFollowed = append(ownersFollowed, ownersPubkeys...)
s.SetOwnersPubkeys(ownersPubkeys)
s.SetOwnersFollowed(ownersFollowed)
s.SetFollowedFollows(followedFollows)
s.SetOwnersMuted(ownersMuted)
// lastly, update users profile metadata and relay lists in the background
if !dontFetch {
// lastly, update all followed users new events in the background
if !dontFetch && s.C.SpiderType != "none" {
go func() {
var k *kinds.T
if s.C.SpiderType == "directory" {
k = kinds.New(
kind.ProfileMetadata, kind.RelayListMetadata,
kind.DMRelaysList,
)
}
everyone := append(ownersFollowed, followedFollows...)
s.SpiderFetch(kind.ProfileMetadata, false, everyone...)
s.SpiderFetch(kind.RelayListMetadata, false, everyone...)
s.SpiderFetch(kind.DMRelaysList, false, everyone...)
_, _ = 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

@@ -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,16 +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) Export(c context.T, w io.Writer, pubkeys ...[]byte) {
// TODO implement me
panic("implement me")
}
func (d *D) SetLogLevel(level string) {
d.Logger.SetLogLevel(lol.GetLogLevel(level))
}
@@ -106,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

105
pkg/database/export.go Normal file
View File

@@ -0,0 +1,105 @@
package database
import (
"bytes"
"github.com/dgraph-io/badger/v4"
"io"
"orly.dev/pkg/database/indexes"
"orly.dev/pkg/database/indexes/types"
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/units"
)
// Export the complete database of stored events to an io.Writer in line structured minified
// 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 := new(bytes.Buffer)
if err = indexes.EventEnc(nil).MarshalWrite(buf); chk.E(err) {
return
}
it := txn.NewIterator(badger.IteratorOptions{Prefix: buf.Bytes()})
defer it.Close()
for it.Rewind(); it.Valid(); it.Next() {
item := it.Item()
if err = item.Value(
func(val []byte) (err error) {
evBuf.Write(val)
return
},
); chk.E(err) {
continue
}
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) {
return
}
if _, err = w.Write([]byte{'\n'}); chk.E(err) {
return
}
evBuf.Reset()
}
return
},
); err != nil {
return
}
} else {
for _, pubkey := range pubkeys {
if err = d.View(
func(txn *badger.Txn) (err error) {
pkBuf := new(bytes.Buffer)
ph := &types.PubHash{}
if err = ph.FromPubkey(pubkey); chk.E(err) {
return
}
if err = indexes.PubkeyEnc(
ph, nil, nil,
).MarshalWrite(pkBuf); chk.E(err) {
return
}
it := txn.NewIterator(badger.IteratorOptions{Prefix: pkBuf.Bytes()})
defer it.Close()
for it.Rewind(); it.Valid(); it.Next() {
item := it.Item()
if err = item.Value(
func(val []byte) (err error) {
evBuf.Write(val)
return
},
); chk.E(err) {
continue
}
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
}
if _, err = w.Write([]byte{'\n'}); chk.E(err) {
continue
}
evBuf.Reset()
}
return
},
); err != nil {
return
}
}
}
return
}

111
pkg/database/export_test.go Normal file
View File

@@ -0,0 +1,111 @@
package database
import (
"bufio"
"bytes"
"os"
"testing"
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/encoders/event/examples"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/context"
)
// TestExport tests the Export function by:
// 1. Creating a new database with events from examples.Cache
// 2. Checking that all event IDs in the cache are found in the export
// 3. Verifying this also works when only a few pubkeys are requested
func TestExport(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 scanner to read events from examples.Cache
scanner := bufio.NewScanner(bytes.NewBuffer(examples.Cache))
scanner.Buffer(make([]byte, 0, 1_000_000_000), 1_000_000_000)
// Maps to store event IDs and their associated pubkeys
eventIDs := make(map[string]bool)
pubkeyToEventIDs := make(map[string][]string)
// Process each event
for scanner.Scan() {
chk.E(scanner.Err())
b := scanner.Bytes()
ev := event.New()
// Unmarshal the event
if _, err = ev.Unmarshal(b); chk.E(err) {
t.Fatal(err)
}
// Save the event to the database
if _, _, err = db.SaveEvent(ctx, ev, false); err != nil {
t.Fatalf("Failed to save event: %v", err)
}
// Store the event ID
eventID := ev.IdString()
eventIDs[eventID] = true
// Store the event ID by pubkey
pubkey := ev.PubKeyString()
pubkeyToEventIDs[pubkey] = append(pubkeyToEventIDs[pubkey], eventID)
}
// Check for scanner errors
if err = scanner.Err(); err != nil {
t.Fatalf("Scanner error: %v", err)
}
t.Logf("Saved %d events to the database", len(eventIDs))
// Test 1: Export all events and verify all IDs are in the export
var exportBuffer bytes.Buffer
db.Export(ctx, &exportBuffer)
// Parse the exported events and check that all IDs are present
exportedIDs := make(map[string]bool)
exportScanner := bufio.NewScanner(&exportBuffer)
exportScanner.Buffer(make([]byte, 0, 1_000_000_000), 1_000_000_000)
exportCount := 0
for exportScanner.Scan() {
b := exportScanner.Bytes()
ev := event.New()
if _, err = ev.Unmarshal(b); chk.E(err) {
t.Fatal(err)
}
exportedIDs[ev.IdString()] = true
exportCount++
}
// Check for scanner errors
if err = exportScanner.Err(); err != nil {
t.Fatalf("Scanner error: %v", err)
}
t.Logf("Found %d events in the export", exportCount)
// Check that all original event IDs are in the export
for id := range eventIDs {
if !exportedIDs[id] {
t.Errorf("Event ID %s not found in export", id)
}
}
t.Logf("All %d event IDs found in export", len(eventIDs))
}

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); 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,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/interfaces/store"
"orly.dev/pkg/utils/chk"
)
@@ -15,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) {

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); 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); 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); 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); 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); 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); 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

@@ -32,7 +32,7 @@ func (d *D) QueryEvents(c context.T, f *filter.F) (evs event.S, err error) {
}
// fetch the events
var ev *event.E
if ev, err = d.FetchEventBySerial(ser); chk.E(err) {
if ev, err = d.FetchEventBySerial(ser); err != nil {
continue
}
evs = append(evs, ev)
@@ -218,7 +218,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); 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,7 @@ 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); err != nil {
t.Fatalf("Failed to save replaceable event: %v", err)
}
@@ -216,14 +216,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); 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 +239,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 +269,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 +289,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); err != nil {
t.Fatalf("Failed to save deletion event: %v", err)
}
@@ -319,17 +319,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 +345,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 +379,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); err != nil {
t.Fatalf("Failed to save parameterized replaceable event: %v", err)
}
@@ -405,7 +405,7 @@ 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); err != nil {
t.Fatalf("Failed to save parameterized deletion event: %v", err)
}
@@ -433,12 +433,12 @@ 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); err != nil {
t.Fatalf(
"Failed to save parameterized deletion event with e-tag: %v", err,
)
@@ -483,7 +483,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 +501,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); 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); 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); 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); 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); 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); 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); 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); 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); 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) (
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,15 @@ func (d *D) SaveEvent(c context.T, ev *event.E) (kc, vc int, err error) {
}
} else {
var idxs []Range
// log.I.S(ev.Pubkey)
if idxs, err = GetIndexesFromFilter(
&filter.F{
Authors: tag.New(ev.Pubkey),
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 +115,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: %0x was 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); 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); 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)
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); 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)
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

@@ -35,7 +35,20 @@ func NewChallengeWith[V string | []byte](challenge V) *Challenge {
// Label returns the label of a authenvelope.Challenge.
func (en *Challenge) Label() string { return L }
// Write the authenvelope.Challenge to a provided io.Writer.
// Write encodes and writes the Challenge instance to the provided writer.
//
// # Parameters
//
// - w (io.Writer): The destination where the encoded data will be written.
//
// # Return Values
//
// - err (error): An error if writing to the writer fails.
//
// # Expected behaviour
//
// Encodes the Challenge instance into a byte slice using Marshal, logs the
// encoded challenge, and writes it to the provided io.Writer.
func (en *Challenge) Write(w io.Writer) (err error) {
var b []byte
b = en.Marshal(b)
@@ -44,8 +57,26 @@ func (en *Challenge) Write(w io.Writer) (err error) {
return
}
// Marshal a authenvelope.Challenge to minified JSON, appending to a provided destination
// slice. Note that this ensures correct string escaping on the challenge field.
// Marshal encodes the Challenge instance into a byte slice, formatting it as
// a JSON-like structure with a specific label and escaping rules applied to
// its content.
//
// # Parameters
//
// - dst ([]byte): The destination buffer where the encoded data will be written.
//
// # Return Values
//
// - b ([]byte): The byte slice containing the encoded Challenge data.
//
// # Expected behaviour
//
// - Prepares the destination buffer and applies a label to it.
//
// - Escapes the challenge content according to Nostr-specific rules before
// appending it to the output.
//
// - Returns the resulting byte slice with the complete encoded structure.
func (en *Challenge) Marshal(dst []byte) (b []byte) {
b = dst
var err error
@@ -63,9 +94,24 @@ func (en *Challenge) Marshal(dst []byte) (b []byte) {
return
}
// Unmarshal a authenvelope.Challenge from minified JSON, returning the remainder after the
// end of the envelope. Note that this ensures the challenge string was
// correctly escaped by NIP-01 escaping rules.
// Unmarshal parses the provided byte slice and extracts the challenge value,
// leaving any remaining bytes after parsing.
//
// # Parameters
//
// - b ([]byte): The byte slice containing the encoded challenge data.
//
// # Return Values
//
// - r ([]byte): Any remaining bytes after parsing the challenge.
//
// - err (error): An error if parsing fails.
//
// # Expected behaviour
//
// - Extracts the quoted challenge string from the input byte slice.
//
// - Trims any trailing characters following the closing quote.
func (en *Challenge) Unmarshal(b []byte) (r []byte, err error) {
r = b
if en.Challenge, r, err = text2.UnmarshalQuoted(r); chk.E(err) {
@@ -80,8 +126,26 @@ func (en *Challenge) Unmarshal(b []byte) (r []byte, err error) {
return
}
// ParseChallenge reads a authenvelope.Challenge encoded in minified JSON and unpacks it to
// the runtime format.
// ParseChallenge parses the provided byte slice into a new Challenge instance,
// extracting the challenge value and returning any remaining bytes after parsing.
//
// # Parameters
//
// - b ([]byte): The byte slice containing the encoded challenge data.
//
// # Return Values
//
// - t (*Challenge): A pointer to the newly created and populated Challenge
// instance.
//
// - rem ([]byte): Any remaining bytes in the input slice after parsing.
//
// - err (error): An error if parsing fails.
//
// # Expected behaviour
//
// Parses the byte slice into a new Challenge instance using Unmarshal,
// returning any remaining bytes and an error if parsing fails.
func ParseChallenge(b []byte) (t *Challenge, rem []byte, err error) {
t = NewChallenge()
if rem, err = t.Unmarshal(b); chk.E(err) {
@@ -107,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",

View File

@@ -31,7 +31,7 @@ func NewSubmissionWith(ev *event.E) *Submission { return &Submission{E: ev} }
// Label returns the label of a event eventenvelope.Submission envelope.
func (en *Submission) Label() string { return L }
func (en *Submission) Id() []byte { return en.E.Id }
func (en *Submission) Id() []byte { return en.E.ID }
// Write the Submission to a provided io.Writer.
func (en *Submission) Write(w io.Writer) (err error) {
@@ -104,7 +104,7 @@ func NewResultWith[V string | []byte](s V, ev *event.E) (
return &Result{subscription.MustNew(s), ev}, nil
}
func (en *Result) Id() []byte { return en.Event.Id }
func (en *Result) Id() []byte { return en.Event.ID }
// Label returns the label of a event eventenvelope.Result envelope.
func (en *Result) Label() string { return L }
@@ -145,7 +145,7 @@ func (en *Result) Unmarshal(b []byte) (r []byte, err error) {
return
}
en.Event = event.New()
if r, err = en.Event.Unmarshal(r); chk.E(err) {
if r, err = en.Event.Unmarshal(r); err != nil {
return
}
if r, err = envelopes.SkipToTheEnd(r); chk.E(err) {
@@ -158,7 +158,7 @@ func (en *Result) Unmarshal(b []byte) (r []byte, err error) {
// envelope into it.
func ParseResult(b []byte) (t *Result, rem []byte, err error) {
t = NewResult()
if rem, err = t.Unmarshal(b); chk.T(err) {
if rem, err = t.Unmarshal(b); err != nil {
return
}
return

View File

@@ -30,7 +30,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

@@ -40,7 +40,7 @@ func NewFrom[V string | []byte](eid V, ok bool, msg ...V) *T {
}
if len(eid) != sha256.Size {
log.W.F(
"event Id unexpected length, expect %d got %d",
"event ID unexpected length, expect %d got %d",
len(eid), sha256.Size,
)
}
@@ -96,7 +96,7 @@ func (en *T) Unmarshal(b []byte) (r []byte, err error) {
}
if len(idHex) != sha256.Size {
err = errorf.E(
"invalid size for Id, require %d got %d",
"invalid size for ID, require %d got %d",
len(idHex), sha256.Size,
)
}

View File

@@ -13,7 +13,7 @@ import (
// MarshalBinary writes a binary encoding of an event.
//
// [ 32 bytes Id ]
// [ 32 bytes ID ]
// [ 32 bytes Pubkey ]
// [ varint CreatedAt ]
// [ 2 bytes Kind ]
@@ -27,7 +27,7 @@ import (
// [ varint Content length ]
// [ 64 bytes Sig ]
func (ev *E) MarshalBinary(w io.Writer) {
_, _ = w.Write(ev.Id)
_, _ = w.Write(ev.ID)
_, _ = w.Write(ev.Pubkey)
varint.Encode(w, uint64(ev.CreatedAt.V))
varint.Encode(w, uint64(ev.Kind.K))
@@ -46,8 +46,8 @@ func (ev *E) MarshalBinary(w io.Writer) {
}
func (ev *E) UnmarshalBinary(r io.Reader) (err error) {
ev.Id = make([]byte, 32)
if _, err = r.Read(ev.Id); chk.E(err) {
ev.ID = make([]byte, 32)
if _, err = r.Read(ev.ID); chk.E(err) {
return
}
ev.Pubkey = make([]byte, 32)

View File

@@ -15,36 +15,53 @@ func TestTMarshalBinary_UnmarshalBinary(t *testing.T) {
scanner.Buffer(make([]byte, 0, 1_000_000_000), 1_000_000_000)
var rem, out []byte
var err error
buf := codecbuf.Get()
ea, eb := New(), New()
now := time.Now()
var counter int
for scanner.Scan() {
// Create new event objects and buffer for each iteration
buf := codecbuf.Get()
ea, eb := New(), New()
chk.E(scanner.Err())
b := scanner.Bytes()
// log.I.F("%s", b)
c := make([]byte, 0, len(b))
c = append(c, b...)
if rem, err = ea.Unmarshal(c); chk.E(err) {
t.Fatal(err)
}
// log.I.F("len %d\n%s\n", len(b), ea.SerializeIndented())
if len(rem) != 0 {
t.Fatalf(
"some of input remaining after marshal/unmarshal: '%s'",
rem,
)
}
// Reset buffer before marshaling
buf.Reset()
ea.MarshalBinary(buf)
// log.I.S(buf.Bytes())
// Create a new buffer for unmarshaling
buf2 := bytes.NewBuffer(buf.Bytes())
if err = eb.UnmarshalBinary(buf2); chk.E(err) {
codecbuf.Put(buf)
t.Fatal(err)
}
// log.I.F("len %d\n%s\n", len(b), eb.SerializeIndented())
// Marshal unmarshaled binary event back to JSON
unmarshaledJSON := eb.Serialize()
// Compare the two JSON representations
if !bytes.Equal(b, unmarshaledJSON) {
t.Fatalf(
"JSON representations don't match after binary marshaling/unmarshaling:\nOriginal: %s\nUnmarshaled: %s",
b, unmarshaledJSON,
)
}
// Return buffer to pool
codecbuf.Put(buf)
counter++
out = out[:0]
// break
}
chk.E(scanner.Err())
t.Logf(

View File

@@ -15,7 +15,7 @@ import (
)
// ToCanonical converts the event to the canonical encoding used to derive the
// event Id.
// event ID.
func (ev *E) ToCanonical(dst []byte) (b []byte) {
b = dst
b = append(b, "[0,\""...)
@@ -88,8 +88,8 @@ func (ev *E) FromCanonical(b []byte) (rem []byte, err error) {
return
}
}
// create the event, use the Id hash to populate the Id
ev.Id = id
// create the event, use the ID hash to populate the ID
ev.ID = id
// unwrap the pubkey
if v, ok := x[1].(*json.Hex); !ok {
err = errorf.E(

View File

@@ -114,7 +114,7 @@ func main() {
}
can := ev.ToCanonical(nil)
eh := event.Hash(can)
eq := bytes.Equal(ev.Id, eh)
eq := bytes.Equal(ev.ID, eh)
if !eq {
_, err = fmt.Fprintf(ids, "%s\n", ev.Serialize())
if chk.E(err) {

View File

@@ -1,5 +1,5 @@
// Package event provides a codec for nostr events, for the wire format (with Id
// and signature), for the canonical form, that is hashed to generate the Id,
// Package event provides a codec for nostr events, for the wire format (with ID
// and signature), for the canonical form, that is hashed to generate the ID,
// and a fast binary form that uses io.Reader/io.Writer.
package event
@@ -14,7 +14,6 @@ import (
"orly.dev/pkg/encoders/tags"
"orly.dev/pkg/encoders/text"
"orly.dev/pkg/encoders/timestamp"
"orly.dev/pkg/encoders/unix"
"orly.dev/pkg/interfaces/signer"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/errorf"
@@ -24,8 +23,8 @@ import (
// defines its JSON string-based format.
type E struct {
// Id is the SHA256 hash of the canonical encoding of the event in binary format
Id []byte
// ID is the SHA256 hash of the canonical encoding of the event in binary format
ID []byte
// Pubkey is the public key of the event creator in binary format
Pubkey []byte
@@ -38,14 +37,14 @@ type E struct {
Kind *kind.T
// Tags are a list of tags, which are a list of strings usually structured
// as a 3 layer scheme indicating specific features of an event.
// as a 3-layer scheme indicating specific features of an event.
Tags *tags.T
// Content is an arbitrary string that can contain anything, but usually
// conforming to a specification relating to the Kind and the Tags.
Content []byte
// Sig is the signature on the Id hash that validates as coming from the
// Sig is the signature on the ID hash that validates as coming from the
// Pubkey in binary format.
Sig []byte
}
@@ -77,19 +76,24 @@ func (ev *E) SerializeIndented() (b []byte) {
return ev.MarshalWithWhitespace(nil, true)
}
// EventId returns the event.E Id as an eventid.T.
// EventId returns the event.E ID as an eventid.T.
func (ev *E) EventId() (eid *eventid.T) {
return eventid.NewWith(ev.Id)
return eventid.NewWith(ev.ID)
}
// stringy/numbery functions for retarded other libraries
// IdString returns the event Id as a hex-encoded string.
func (ev *E) IdString() (s string) { return hex.Enc(ev.Id) }
// IdString returns the event ID as a hex-encoded string.
func (ev *E) IdString() (s string) { return hex.Enc(ev.ID) }
func (ev *E) Id() []byte { return ev.ID }
// CreatedAtInt64 returns the created_at timestamp as a standard int64.
func (ev *E) CreatedAtInt64() (i int64) { return ev.CreatedAt.I64() }
// KindInt returns the kind as an int, as is often needed for JSON.
func (ev *E) KindInt() (i int) { return int(ev.Kind.K) }
// KindInt32 returns the kind as an int32, as is often needed for JSON.
func (ev *E) KindInt32() (i int32) { return int32(ev.Kind.K) }
@@ -107,13 +111,13 @@ func (ev *E) ContentString() (s string) { return string(ev.Content) }
// J is an event.E encoded in more basic types than used in this library.
type J struct {
Id string `json:"id"`
Pubkey string `json:"pubkey"`
CreatedAt unix.Time `json:"created_at"`
Kind int32 `json:"kind"`
Tags [][]string `json:"tags"`
Content string `json:"content"`
Sig string `json:"sig"`
Id string `json:"id" doc:"event id (SHA256 hash of canonical form of event, 64 characters hex)"`
Pubkey string `json:"pubkey" doc:"public key of author of event, required to verify signature (BIP-340 Schnorr public key, 64 characters hex)"`
CreatedAt int64 `json:"created_at" doc:"unix timestamp of time when event was created"`
Kind int `json:"kind" doc:"kind number of event"`
Tags [][]string `json:"tags" doc:"tags that add metadata to the event"`
Content string `json:"content" doc:"content of event"`
Sig string `json:"sig" doc:"signature of event (BIP-340 schnorr signature, 128 characters hex)"`
}
// ToEventJ converts an event.E into an event.J.
@@ -121,17 +125,17 @@ func (ev *E) ToEventJ() (j *J) {
j = &J{}
j.Id = ev.IdString()
j.Pubkey = ev.PubKeyString()
j.CreatedAt = unix.Time{ev.CreatedAt.Time()}
j.Kind = ev.KindInt32()
j.CreatedAt = ev.CreatedAt.I64()
j.Kind = ev.KindInt()
j.Content = ev.ContentString()
j.Tags = ev.Tags.ToStringsSlice()
j.Sig = ev.SigString()
return
}
// IdFromString decodes an event ID and loads it into an event.E Id.
// IdFromString decodes an event ID and loads it into an event.E ID.
func (ev *E) IdFromString(s string) (err error) {
ev.Id, err = hex.Dec(s)
ev.ID, err = hex.Dec(s)
return
}
@@ -149,6 +153,13 @@ func (ev *E) KindFromInt32(i int32) {
return
}
// KindFromInt encodes an int representation of a kind.T into an event.E.
func (ev *E) KindFromInt(i int) {
ev.Kind = &kind.T{}
ev.Kind.K = uint16(i)
return
}
// PubKeyFromString decodes a hex-encoded string into the event.E Pubkey field.
func (ev *E) PubKeyFromString(s string) (err error) {
if len(s) != 2*schnorr.PubKeyBytesLen {
@@ -198,8 +209,8 @@ func (e J) ToEvent() (ev *E, err error) {
if err = ev.IdFromString(e.Id); chk.E(err) {
return
}
ev.CreatedAtFromInt64(e.CreatedAt.Unix())
ev.KindFromInt32(e.Kind)
ev.CreatedAtFromInt64(e.CreatedAt)
ev.KindFromInt(e.Kind)
if err = ev.PubKeyFromString(e.Pubkey); chk.E(err) {
return
}

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,7 @@ package event
import (
"bytes"
"fmt"
"github.com/minio/sha256-simd"
"io"
"orly.dev/pkg/crypto/ec/schnorr"
@@ -35,7 +36,7 @@ func (ev *E) Marshal(dst []byte) (b []byte) {
func (ev *E) MarshalWithWhitespace(dst []byte, on bool) (b []byte) {
// open parentheses
dst = append(dst, '{')
// Id
// ID
if on {
dst = append(dst, '\n', '\t')
}
@@ -43,7 +44,7 @@ func (ev *E) MarshalWithWhitespace(dst []byte, on bool) (b []byte) {
if on {
dst = append(dst, ' ')
}
dst = text2.AppendQuote(dst, ev.Id, hex.EncAppend)
dst = text2.AppendQuote(dst, ev.ID, hex.EncAppend)
dst = append(dst, ',')
// Pubkey
if on {
@@ -187,12 +188,12 @@ InVal:
}
if len(id) != sha256.Size {
err = errorf.E(
"invalid Id, require %d got %d", sha256.Size,
"invalid ID, require %d got %d", sha256.Size,
len(id),
)
return
}
ev.Id = id
ev.ID = id
goto BetweenKV
case jPubkey[0]:
if !bytes.Equal(jPubkey, key) {
@@ -300,7 +301,7 @@ AfterClose:
}
return
invalid:
err = errorf.E(
err = fmt.Errorf(
"invalid key,\n'%s'\n'%s'\n'%s'", string(b), string(b[:len(r)]),
string(r),
)

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