lint exit package (#40)

This commit is contained in:
asmogo 2024-09-01 21:24:24 +02:00 committed by GitHub
parent b04b4f7e10
commit ce11bb963a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 131 additions and 88 deletions

View File

@ -23,7 +23,7 @@ import (
const (
startingReverseProxyMessage = "starting exit node with https reverse proxy"
generateKeyMessage = "Generated new private key. Please set your environment using the new key, otherwise your key will be lost."
generateKeyMessage = "Generated new private key. Please set your environment using the new key, otherwise your key will be lost." //nolint: lll
)
// Exit represents a structure that holds information related to an exit node.
@ -43,7 +43,7 @@ type Exit struct {
// It is used to establish and maintain connections between the Exit node and the backend host.
nostrConnectionMap *xsync.MapOf[string, *netstr.NostrConnection]
// mutexMap is a field in the Exit struct that represents a map used for synchronizing access to resources based on a string key.
// mutexMap is a field in the Exit struct used for synchronizing access to resources based on a string key.
mutexMap *MutexMap
// incomingChannel represents a channel used to receive incoming events from relays.
@ -87,26 +87,25 @@ func NewExit(ctx context.Context, exitNodeConfig *config.ExitConfig) *Exit {
// start reverse proxy if https port is set
if exitNodeConfig.HttpsPort != 0 {
exitNodeConfig.BackendHost = fmt.Sprintf(":%d", exitNodeConfig.HttpsPort)
go func(cfg *config.ExitConfig) {
go func(ctx context.Context, cfg *config.ExitConfig) {
slog.Info(startingReverseProxyMessage, "port", cfg.HttpsPort)
err := exit.StartReverseProxy(cfg.HttpsTarget, cfg.HttpsPort)
err := exit.StartReverseProxy(ctx, cfg.HttpsTarget, cfg.HttpsPort)
if err != nil {
panic(err)
}
}(exitNodeConfig)
}(ctx, exitNodeConfig)
}
// set config
exit.config = exitNodeConfig
// add relays to the pool
for _, relayUrl := range exitNodeConfig.NostrRelays {
relay, err := exit.pool.EnsureRelay(relayUrl)
for _, relayURL := range exitNodeConfig.NostrRelays {
relay, err := exit.pool.EnsureRelay(relayURL)
if err != nil {
fmt.Println(err)
slog.Error("failed to ensure relay", "url", relayURL, "error", err)
continue
}
exit.relays = append(exit.relays, relay)
fmt.Printf("added relay connection to %s\n", relayUrl)
slog.Info("added relay connection", "url", relayURL) //nolint:forbidigo
}
domain, err := exit.getDomain()
if err != nil {
@ -127,17 +126,18 @@ func NewExit(ctx context.Context, exitNodeConfig *config.ExitConfig) *Exit {
// getDomain returns the domain string used by the Exit node for communication with the Nostr relays.
// It concatenates the relay URLs using base32 encoding with no padding, separated by dots.
// The resulting domain is then appended with the base32 encoded public key obtained using the configured Nostr private key.
// The domain is then appended with the base32 encoded public key obtained using the configured Nostr private key.
// The final domain string is converted to lowercase and returned.
// If any errors occur during the process, they are returned along with an
func (e *Exit) getDomain() (string, error) {
var domain string
// first lets build the subdomains
for _, relayUrl := range e.config.NostrRelays {
for _, relayURL := range e.config.NostrRelays {
if domain == "" {
domain = base32.HexEncoding.WithPadding(base32.NoPadding).EncodeToString([]byte(relayUrl))
domain = base32.HexEncoding.WithPadding(base32.NoPadding).EncodeToString([]byte(relayURL))
} else {
domain = fmt.Sprintf("%s.%s", domain, base32.HexEncoding.WithPadding(base32.NoPadding).EncodeToString([]byte(relayUrl)))
domain = fmt.Sprintf("%s.%s",
domain, base32.HexEncoding.WithPadding(base32.NoPadding).EncodeToString([]byte(relayURL)))
}
}
// create base32 encoded public key
@ -173,21 +173,20 @@ func GetPublicKeyBase32(sk string) (string, error) {
// setSubscriptions sets up subscriptions for the Exit node to receive incoming events from the specified relays.
// It first obtains the public key using the configured Nostr private key.
// Then it calls the `handleSubscription` method to open a subscription to the relays with the specified filters.
// This method runs in a separate goroutine and continuously handles the incoming events by calling the `processMessage` method.
// This method runs in a separate goroutine and continuously handles the incoming events by calling `processMessage`
// If the context is canceled before the subscription is established, it returns the context error.
// If any errors occur during the process, they are returned.
// This method should be called once when starting the Exit node.
func (e *Exit) setSubscriptions(ctx context.Context) error {
pubKey, err := nostr.GetPublicKey(e.config.NostrPrivateKey)
if err != nil {
return err
return fmt.Errorf("failed to get public key: %w", err)
}
now := nostr.Now()
if err = e.handleSubscription(ctx, pubKey, now); err != nil {
return err
return fmt.Errorf("failed to handle subscription: %w", err)
}
return nil
}
// handleSubscription handles the subscription to incoming events from relays based on the provided filters.
@ -240,7 +239,7 @@ func (e *Exit) processMessage(ctx context.Context, msg nostr.IncomingEvent) {
}
protocolMessage, err := protocol.UnmarshalJSON([]byte(decodedMessage))
if err != nil {
slog.Error("could not unmarshal message")
slog.Error("could not unmarshal message", "error", err)
return
}
destination, err := protocol.Parse(protocolMessage.Destination)
@ -290,7 +289,10 @@ func (e *Exit) handleConnect(
dst, err = net.Dial("tcp", protocolMessage.Destination)
if err != nil {
slog.Error("could not connect to backend", "error", err)
connection.Close()
err = connection.Close()
if err != nil {
slog.Error("could not close connection", "error", err)
}
return
}

View File

@ -8,6 +8,7 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"log/slog"
"math/big"
@ -22,90 +23,123 @@ import (
"github.com/nbd-wtf/go-nostr/nip04"
)
func (e *Exit) StartReverseProxy(httpTarget string, port int32) error {
ctx := context.Background()
ev := e.pool.QuerySingle(ctx, e.config.NostrRelays, nostr.Filter{
const (
headerTimeout = 5 * time.Second
)
var (
errNoCertificateEvent = errors.New("failed to find encrypted direct message")
)
func (e *Exit) StartReverseProxy(ctx context.Context, httpTarget string, port int32) error {
incomingEvent := e.pool.QuerySingle(ctx, e.config.NostrRelays, nostr.Filter{
Authors: []string{e.publicKey},
Kinds: []int{protocol.KindCertificateEvent},
Tags: nostr.TagMap{"p": []string{e.publicKey}},
})
var cert tls.Certificate
if ev == nil {
var err error
if incomingEvent == nil {
certificate, err := e.createAndStoreCertificateData(ctx)
if err != nil {
return err
}
cert = *certificate
} else {
slog.Info("found certificate event", "certificate", ev.Content)
// load private key from file
privateKeyEvent := e.pool.QuerySingle(ctx, e.config.NostrRelays, nostr.Filter{
Authors: []string{e.publicKey},
Kinds: []int{protocol.KindPrivateKeyEvent},
Tags: nostr.TagMap{"p": []string{e.publicKey}},
})
if privateKeyEvent == nil {
return fmt.Errorf("failed to find encrypted direct message")
}
sharedKey, err := nip04.ComputeSharedSecret(privateKeyEvent.PubKey, e.config.NostrPrivateKey)
cert, err = e.handleCertificateEvent(incomingEvent, ctx, cert)
if err != nil {
return err
}
decodedMessage, err := nip04.Decrypt(privateKeyEvent.Content, sharedKey)
if err != nil {
return err
}
message, err := protocol.UnmarshalJSON([]byte(decodedMessage))
if err != nil {
return err
}
block, _ := pem.Decode(message.Data)
if block == nil {
fmt.Fprintf(os.Stderr, "error: failed to decode PEM block containing private key\n")
os.Exit(1)
}
if got, want := block.Type, "RSA PRIVATE KEY"; got != want {
fmt.Fprintf(os.Stderr, "error: decoded PEM block of type %s, but wanted %s", got, want)
os.Exit(1)
}
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return err
}
certBlock, _ := pem.Decode([]byte(ev.Content))
if certBlock == nil {
fmt.Fprintf(os.Stderr, "Failed to parse certificate PEM.")
os.Exit(1)
}
parsedCert, err := x509.ParseCertificate(certBlock.Bytes)
if err != nil {
return err
}
cert = tls.Certificate{
Certificate: [][]byte{certBlock.Bytes},
PrivateKey: priv,
Leaf: parsedCert,
}
}
target, _ := url.Parse(httpTarget)
httpsConfig := &http.Server{
Addr: fmt.Sprintf(":%d", port),
TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
Handler: http.HandlerFunc(httputil.NewSingleHostReverseProxy(target).ServeHTTP),
ReadHeaderTimeout: headerTimeout,
Addr: fmt.Sprintf(":%d", port),
TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
Handler: http.HandlerFunc(httputil.NewSingleHostReverseProxy(target).ServeHTTP),
}
return httpsConfig.ListenAndServeTLS("", "")
}
func (e *Exit) handleCertificateEvent(incomingEvent *nostr.IncomingEvent, ctx context.Context, cert tls.Certificate) (tls.Certificate, error) {
slog.Info("found certificate event", "certificate", incomingEvent.Content)
// load private key from file
privateKeyEvent := e.pool.QuerySingle(ctx, e.config.NostrRelays, nostr.Filter{
Authors: []string{e.publicKey},
Kinds: []int{protocol.KindPrivateKeyEvent},
Tags: nostr.TagMap{"p": []string{e.publicKey}},
})
if privateKeyEvent == nil {
return tls.Certificate{}, errNoCertificateEvent
}
sharedKey, err := nip04.ComputeSharedSecret(privateKeyEvent.PubKey, e.config.NostrPrivateKey)
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to compute shared key: %w", err)
}
decodedMessage, err := nip04.Decrypt(privateKeyEvent.Content, sharedKey)
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to decrypt private key: %w", err)
}
message, err := protocol.UnmarshalJSON([]byte(decodedMessage))
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to unmarshal message: %w", err)
}
block, _ := pem.Decode(message.Data)
if block == nil {
_, err = fmt.Fprintf(os.Stderr, "error: failed to decode PEM block containing private key\n")
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to write error: %w", err)
}
os.Exit(1)
}
if got, want := block.Type, "RSA PRIVATE KEY"; got != want {
_, err = fmt.Fprintf(os.Stderr, "error: decoded PEM block of type %s, but wanted %s", got, want)
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to write error: %w", err)
}
os.Exit(1)
}
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to parse private key: %w", err)
}
certBlock, _ := pem.Decode([]byte(incomingEvent.Content))
if certBlock == nil {
_, err = fmt.Fprintf(os.Stderr, "Failed to parse certificate PEM.")
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to write error: %w", err)
}
os.Exit(1)
}
parsedCert, err := x509.ParseCertificate(certBlock.Bytes)
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to parse certificate: %w", err)
}
cert = tls.Certificate{
Certificate: [][]byte{certBlock.Bytes},
PrivateKey: priv,
Leaf: parsedCert,
}
return cert, nil
}
const (
tenYears = 0 * 365 * 24 * time.Hour
keySize = 2048
limit = 128
chmod = 0644
)
func (e *Exit) createAndStoreCertificateData(ctx context.Context) (*tls.Certificate, error) {
priv, _ := rsa.GenerateKey(rand.Reader, 2048)
priv, _ := rsa.GenerateKey(rand.Reader, keySize)
notBefore := time.Now()
notAfter := notBefore.Add(10 * 365 * 24 * time.Hour)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
notAfter := notBefore.Add(tenYears)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), limit)
serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit)
domain, _ := e.getDomain()
@ -126,7 +160,7 @@ func (e *Exit) createAndStoreCertificateData(ctx context.Context) (*tls.Certific
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
// save key pem to file
err := os.WriteFile(fmt.Sprintf("%s.key", e.publicKey), keyPEM, 0644)
err := os.WriteFile(fmt.Sprintf("%s.key", e.publicKey), keyPEM, chmod)
if err != nil {
return nil, err
}

View File

@ -1,7 +1,7 @@
package exit
import (
"fmt"
"log/slog"
"sync"
)
@ -33,8 +33,8 @@ func (mm *MutexMap) Unlock(id string) {
mutex, ok := mm.m[id]
mm.mu.Unlock()
if !ok {
panic(fmt.Sprintf("tried to unlock mutex for non-existent id %s", id))
slog.Error("mutex not found", "id", id)
return
}
mutex.Unlock()
}

View File

@ -2,6 +2,8 @@ package exit
import (
"context"
"errors"
"fmt"
"log/slog"
"strconv"
"time"
@ -12,9 +14,11 @@ import (
const ten = 10
var errNoPublicKey = errors.New("no public key found")
func (e *Exit) announceExitNode(ctx context.Context) error {
if !e.config.Public {
return nil
return errNoPublicKey
}
go func() {
for {
@ -45,25 +49,28 @@ func (e *Exit) announceExitNode(ctx context.Context) error {
return nil
}
func (e *Exit) DeleteEvent(ctx context.Context, ev *nostr.Event) error {
func (e *Exit) DeleteEvent(ctx context.Context, event *nostr.Event) error {
for _, responseRelay := range e.config.NostrRelays {
var relay *nostr.Relay
relay, err := e.pool.EnsureRelay(responseRelay)
if err != nil {
return err
return fmt.Errorf("failed to ensure relay: %w", err)
}
event := nostr.Event{
CreatedAt: nostr.Now(),
PubKey: e.publicKey,
Kind: nostr.KindDeletion,
Tags: nostr.Tags{
nostr.Tag{"e", ev.ID},
nostr.Tag{"e", event.ID},
},
}
err = event.Sign(e.config.NostrPrivateKey)
if err != nil {
return fmt.Errorf("failed to sign event: %w", err)
}
err = relay.Publish(ctx, event)
if err != nil {
return err
return fmt.Errorf("failed to publish event: %w", err)
}
}
return nil