Merge remote-tracking branch 'upstream/main' into kwsantiago/nwc-mock-client

This commit is contained in:
2025-08-18 18:09:23 -04:00
16 changed files with 61 additions and 163 deletions

0
cmd/lerproxy/LICENSE Normal file → Executable file
View File

21
cmd/lerproxy/README.md Normal file → Executable file
View File

@@ -6,12 +6,12 @@ DNS verification [NIP-05](https://github.com/nostr-protocol/nips/blob/master/05.
## Install
go install lerproxy.mleku.dev@latest
go install mleku.dev/lerproxy@latest
## Run
```
Usage: lerproxy.mleku.dev [--listen LISTEN] [--map MAP] [--rewrites REWRITES] [--cachedir CACHEDIR] [--hsts] [--email EMAIL] [--http HTTP] [--rto RTO] [--wto WTO] [--idle IDLE] [--cert CERT]
Usage: mleku.dev/lerproxy [--listen LISTEN] [--map MAP] [--rewrites REWRITES] [--cachedir CACHEDIR] [--hsts] [--email EMAIL] [--http HTTP] [--rto RTO] [--wto WTO] [--idle IDLE] [--cert CERT]
Options:
--listen LISTEN, -l LISTEN
@@ -49,7 +49,7 @@ as:
* in the launch parameters for `lerproxy` you can now add any number of `--cert` parameters with
the domain (including for wildcards), and the path to the `.crt`/`.key` files:
lerproxy.mleku.dev --cert <domain>:/path/to/TLS_cert
mleku.dev/lerproxy --cert <domain>:/path/to/TLS_cert
this will then, if found, load and parse the TLS certificate and secret key if the suffix of
the domain matches. The certificate path is expanded to two files with the above filename
@@ -58,17 +58,6 @@ as:
> 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.
# 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
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
may be the problem.
## example mapping.txt
nostr.example.com: /path/to/nostr.json
@@ -96,7 +85,7 @@ Description=lerproxy
[Service]
Type=simple
User=username
ExecStart=/usr/local/bin/lerproxy.mleku.dev -m /path/to/mapping.txt -l xxx.xxx.xxx.xxx:443 --http xxx.xxx.xxx.6:80 -m /path/to/mapping.txt -e email@example.com -c /path/to/letsencrypt/cache --cert example.com:/path/to/tls/certs
ExecStart=/usr/local/bin/mleku.dev/lerproxy -m /path/to/mapping.txt -l xxx.xxx.xxx.xxx:443 --http xxx.xxx.xxx.6:80 -m /path/to/mapping.txt -e email@example.com -c /path/to/letsencrypt/cache --cert example.com:/path/to/tls/certs
Restart=on-failure
Wants=network-online.target
After=network.target network-online.target wg-quick@wg0.service
@@ -114,7 +103,7 @@ a tunnel, such as your dev machine (something I do for nostr relay development)
The simplest way to allow `lerproxy` to bind to port 80 and 443 is as follows:
setcap 'cap_net_bind_service=+ep' /path/to/lerproxy.mleku.dev
setcap 'cap_net_bind_service=+ep' /path/to/mleku.dev/lerproxy
## todo

View File

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

View File

@@ -1,4 +1,3 @@
// Package hsts implements a HTTP handler that enforces HSTS.
package hsts
import "net/http"

View File

@@ -1,10 +1,10 @@
// 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.
// usage for multiple hostnames/backends, and URL rewriting capability.
package main
import (
"bufio"
"context"
"crypto/tls"
_ "embed"
"encoding/json"
@@ -15,14 +15,6 @@ import (
"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/pkg/utils/chk"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/log"
"os"
"os/signal"
"path/filepath"
@@ -34,14 +26,22 @@ import (
"github.com/alexflint/go-arg"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/sync/errgroup"
"orly.dev/cmd/lerproxy/buf"
"orly.dev/cmd/lerproxy/hsts"
"orly.dev/cmd/lerproxy/reverse"
"orly.dev/cmd/lerproxy/tcpkeepalive"
"orly.dev/cmd/lerproxy/util"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/log"
)
//go:embed favicon.ico
var defaultFavicon []byte
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"`
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"`
// Rewrites string `arg:"-r,--rewrites" default:"rewrites.txt"`
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"`
@@ -49,22 +49,21 @@ type runArgs struct {
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"`
Certs []string `arg:"--cert,separate" help:"certificates and the domain they match: eg: mleku.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"`
}
var args runArgs
func main() {
arg.MustParse(&args)
ctx, cancel := signal.NotifyContext(context.Bg(), os.Interrupt)
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
if err := run(ctx, args); chk.T(err) {
if err := run(ctx, args); err != nil {
log.F.Ln(err)
}
}
func run(c context.T, args runArgs) (err error) {
func run(ctx context.Context, args runArgs) (err error) {
if args.Cache == "" {
err = log.E.Err("no cache specified")
@@ -83,7 +82,7 @@ func run(c context.T, args runArgs) (err error) {
if args.WTO > 0 {
srv.WriteTimeout = args.WTO
}
group, ctx := errgroup.WithContext(c)
group, ctx := errgroup.WithContext(ctx)
if args.HTTP != "" {
httpServer := http.Server{
Addr: args.HTTP,
@@ -100,8 +99,8 @@ func run(c context.T, args runArgs) (err error) {
group.Go(
func() error {
<-ctx.Done()
ctx, cancel := context.Timeout(
context.Bg(),
ctx, cancel := context.WithTimeout(
context.Background(),
time.Second,
)
defer cancel()
@@ -137,7 +136,9 @@ func run(c context.T, args runArgs) (err error) {
group.Go(
func() error {
<-ctx.Done()
ctx, cancel := context.Timeout(context.Bg(), time.Second)
ctx, cancel := context.WithTimeout(
context.Background(), time.Second,
)
defer cancel()
return srv.Shutdown(ctx)
},
@@ -328,13 +329,12 @@ func setProxy(mapping map[string]string) (h http.Handler, err error) {
)
fin := hn + "/favicon.ico"
var fi []byte
if fi, err = os.ReadFile(fin); chk.E(err) {
if fi, err = os.ReadFile(fin); !chk.E(err) {
fi = defaultFavicon
}
mux.HandleFunc(
hn+"/favicon.ico",
func(writer http.ResponseWriter, request *http.Request) {
log.T.F("serving favicon to %s", hn)
if _, err = writer.Write(fi); chk.E(err) {
return
}
@@ -376,12 +376,12 @@ func setProxy(mapping map[string]string) (h http.Handler, err error) {
)
// req.Header.Set("Access-Control-Allow-Credentials", "true")
req.Header.Set("Access-Control-Allow-Origin", "*")
log.I.Ln(req.URL, req.RemoteAddr)
log.D.Ln(req.URL, req.RemoteAddr)
},
Transport: &http.Transport{
DialContext: func(c context.T, n, addr string) (
net.Conn, error,
) {
DialContext: func(
ctx context.Context, n, addr string,
) (net.Conn, error) {
return net.DialTimeout(network, ba, 5*time.Second)
},
},

View File

@@ -1,13 +1,12 @@
// 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"
"orly.dev/cmd/lerproxy/util"
)
// NewSingleHostReverseProxy is a copy of httputil.NewSingleHostReverseProxy

View File

@@ -1,12 +1,11 @@
// Package tcpkeepalive implements a net.TCPListener with a singleton set period
// for a default 3 minute keep-aline.
package tcpkeepalive
import (
"net"
"orly.dev/cmd/lerproxy/timeout"
"orly.dev/pkg/utils/chk"
"time"
"orly.dev/cmd/lerproxy/timeout"
)
// Period can be changed prior to opening a Listener to alter its'

View File

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

View File

@@ -1,6 +1,3 @@
// 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"

View File

@@ -16,7 +16,7 @@ import (
// separate from the ownersFollowed list, but there could be reasons for this
// distinction, such as rate limiting applying to the former and not the latter.
type Lists struct {
sync.Mutex
sync.RWMutex
ownersPubkeys [][]byte
ownersFollowed [][]byte
followedFollows [][]byte
@@ -24,15 +24,15 @@ type Lists struct {
}
func (l *Lists) LenOwnersPubkeys() (ll int) {
l.Lock()
defer l.Unlock()
l.RLock()
defer l.RUnlock()
ll = len(l.ownersPubkeys)
return
}
func (l *Lists) OwnersPubkeys() (pks [][]byte) {
l.Lock()
defer l.Unlock()
l.RLock()
defer l.RUnlock()
pks = append(pks, l.ownersPubkeys...)
return
}
@@ -45,15 +45,15 @@ func (l *Lists) SetOwnersPubkeys(pks [][]byte) {
}
func (l *Lists) LenOwnersFollowed() (ll int) {
l.Lock()
defer l.Unlock()
l.RLock()
defer l.RUnlock()
ll = len(l.ownersFollowed)
return
}
func (l *Lists) OwnersFollowed() (pks [][]byte) {
l.Lock()
defer l.Unlock()
l.RLock()
defer l.RUnlock()
pks = append(pks, l.ownersFollowed...)
return
}
@@ -66,15 +66,15 @@ func (l *Lists) SetOwnersFollowed(pks [][]byte) {
}
func (l *Lists) LenFollowedFollows() (ll int) {
l.Lock()
defer l.Unlock()
l.RLock()
defer l.RUnlock()
ll = len(l.followedFollows)
return
}
func (l *Lists) FollowedFollows() (pks [][]byte) {
l.Lock()
defer l.Unlock()
l.RLock()
defer l.RUnlock()
pks = append(pks, l.followedFollows...)
return
}
@@ -87,15 +87,15 @@ func (l *Lists) SetFollowedFollows(pks [][]byte) {
}
func (l *Lists) LenOwnersMuted() (ll int) {
l.Lock()
defer l.Unlock()
l.RLock()
defer l.RUnlock()
ll = len(l.ownersMuted)
return
}
func (l *Lists) OwnersMuted() (pks [][]byte) {
l.Lock()
defer l.Unlock()
l.RLock()
defer l.RUnlock()
pks = append(pks, l.ownersMuted...)
return
}

View File

@@ -2,7 +2,6 @@ package database
import (
"bytes"
"fmt"
"orly.dev/pkg/crypto/sha256"
"orly.dev/pkg/database/indexes/types"
"orly.dev/pkg/encoders/event"
@@ -105,14 +104,6 @@ func (d *D) QueryEvents(c context.T, f *filter.F) (evs event.S, err error) {
idPkTs = append(idPkTs, deletionIdPkTs...)
}
// First pass: collect all deletion events
log.T.C(
func() string {
return fmt.Sprintf(
"Debug: Starting first pass - processing %d events\n",
len(idPkTs),
)
},
)
for _, idpk := range idPkTs {
var ev *event.E
ser := new(types.Uint40)
@@ -130,14 +121,6 @@ func (d *D) QueryEvents(c context.T, f *filter.F) (evs event.S, err error) {
}
// Process deletion events to build our deletion maps
if ev.Kind.Equal(kind.Deletion) {
log.T.C(
func() string {
return fmt.Sprintf(
"found deletion event with ID: %s\n",
hex.Enc(ev.ID),
)
},
)
// Check for 'e' tags that directly reference event IDs
eTags := ev.Tags.GetAll(tag.New([]byte{'e'}))
for _, eTag := range eTags.ToSliceOfTags() {
@@ -149,22 +132,7 @@ func (d *D) QueryEvents(c context.T, f *filter.F) (evs event.S, err error) {
}
// Check for 'a' tags that reference parameterized replaceable
// events
log.T.C(
func() string {
return fmt.Sprintf(
"processing deletion event with ID: %s\n",
hex.Enc(ev.ID),
)
},
)
aTags := ev.Tags.GetAll(tag.New([]byte{'a'}))
log.D.C(
func() string {
return fmt.Sprintf(
"Found %d a-tags\n", aTags.Len(),
)
},
)
for _, aTag := range aTags.ToSliceOfTags() {
if aTag.Len() < 2 {
continue
@@ -205,21 +173,6 @@ func (d *D) QueryEvents(c context.T, f *filter.F) (evs event.S, err error) {
dValue := string(split[2])
deletionsByKindPubkeyDTag[key][dValue] = true
// Debug logging
log.D.C(
func() string {
return fmt.Sprintf(
"processing a-tag: %s\n", string(aTag.Value()),
)
},
)
log.D.C(
func() string {
return fmt.Sprintf(
"adding to deletion map - key: %s, d-tag: %s\n",
key, dValue,
)
},
)
}
// For replaceable events, we need to check if there are any
// e-tags that reference events with the same kind and pubkey
@@ -353,23 +306,6 @@ func (d *D) QueryEvents(c context.T, f *filter.F) (evs event.S, err error) {
// Check if this event has been deleted via an a-tag
if deletionMap, exists := deletionsByKindPubkeyDTag[key]; exists {
// Debug logging
log.T.C(
func() string {
return fmt.Sprintf(
"Checking deletion map - key: %s, d-tag: %s",
key, dValue,
)
},
)
log.T.C(
func() string {
return fmt.Sprintf(
"Deletion map contains key: %v, d-tag in map: %v",
exists, deletionMap[dValue],
)
},
)
// If the d-tag value is in the deletion map and this event
// is not specifically requested by ID, skip it
if deletionMap[dValue] && !isIdInFilter {

View File

@@ -248,7 +248,7 @@ func (f *F) Unmarshal(b []byte) (r []byte, err error) {
r = b[:]
var key []byte
var state int
for ; len(r) >= 0; r = r[1:] {
for ; len(r) > 0; r = r[1:] {
// log.I.ToSliceOfBytes("%c", rem[0])
switch state {
case beforeOpen:

View File

@@ -140,7 +140,7 @@ func (p *S) Receive(msg typer.T) {
// # Expected behaviour
//
// Delivers the event to all subscribers whose filters match the event. It
// applies authentication checks if required by the server, and skips delivery
// applies authentication checks if required by the server and skips delivery
// for unauthenticated users when events are privileged.
func (p *S) Deliver(ev *event.E) {
var err error
@@ -163,29 +163,11 @@ func (p *S) Deliver(ev *event.E) {
},
)
for id, subscriber := range subs {
log.T.F(
"subscriber %s\n%s", w.RealRemote(),
subscriber.Marshal(nil),
)
if !subscriber.Match(ev) {
log.T.C(
func() string {
return fmt.Sprintf(
"subscriber %s filter %s not match", id,
subscriber.Marshal(nil),
)
},
)
continue
}
if p.Server.AuthRequired() {
if !auth.CheckPrivilege(w.AuthedPubkey(), ev) {
log.W.F(
"not privileged %0x ev pubkey %0x ev pubkey %0x kind %s privileged: %v",
w.AuthedPubkey(), ev.Pubkey,
w.AuthedPubkey(), ev.Kind.Name(),
ev.Kind.IsPrivileged(),
)
continue
}
var res *eventenvelope.Result

View File

@@ -273,7 +273,7 @@ func (r *Client) ConnectWithTLS(
); err != nil {
r.ConnectionError = err
r.Close()
break
return
}
message := buf.Bytes()
var t string

View File

@@ -3,10 +3,10 @@ package ws
import (
"crypto/tls"
"errors"
"fmt"
"io"
"net/http"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/errorf"
"orly.dev/pkg/utils/units"
"time"
@@ -40,7 +40,7 @@ func (c *Connection) WriteMessage(
ctx context.T, data []byte,
) (err error) {
if err = c.conn.Write(ctx, ws.MessageText, data); err != nil {
err = fmt.Errorf("failed to write message: %w", err)
err = errorf.E("failed to write message: %w", err)
return
}
return nil
@@ -52,11 +52,11 @@ func (c *Connection) ReadMessage(
) (err error) {
var reader io.Reader
if _, reader, err = c.conn.Reader(ctx); err != nil {
err = fmt.Errorf("failed to get reader: %w", err)
err = errorf.E("failed to get reader: %w", err)
return
}
if _, err = io.Copy(buf, reader); err != nil {
err = fmt.Errorf("failed to read message: %w", err)
err = errorf.E("failed to read message: %w", err)
return
}
return

View File

@@ -1 +1 @@
v0.8.5
v0.8.7