diff --git a/exit/exit.go b/exit/exit.go index 0591169..3124c13 100644 --- a/exit/exit.go +++ b/exit/exit.go @@ -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. @@ -195,7 +194,8 @@ func (e *Exit) setSubscriptions(ctx context.Context) error { // It returns an error if there is any issue with the subscription. func (e *Exit) handleSubscription(ctx context.Context, pubKey string, since nostr.Timestamp) error { incomingEventChannel := e.pool.SubMany(ctx, e.config.NostrRelays, nostr.Filters{ - {Kinds: []int{protocol.KindEphemeralEvent}, + { + Kinds: []int{protocol.KindEphemeralEvent}, Since: &since, Tags: nostr.TagMap{ "p": []string{pubKey}, @@ -239,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) @@ -289,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 } diff --git a/exit/https.go b/exit/https.go index 343dfc9..d030668 100644 --- a/exit/https.go +++ b/exit/https.go @@ -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) - if err != nil { - return err - } - decodedMessage, err := nip04.Decrypt(privateKeyEvent.Content, sharedKey) - if err != nil { - return err - } - message, err := protocol.UnmarshalJSON([]byte(decodedMessage)) + cert, err = e.handleCertificateEvent(incomingEvent, ctx, cert) 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) - } + } + target, _ := url.Parse(httpTarget) - 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) - } + httpsConfig := &http.Server{ + ReadHeaderTimeout: headerTimeout, + Addr: fmt.Sprintf(":%d", port), + TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}}, + Handler: http.HandlerFunc(httputil.NewSingleHostReverseProxy(target).ServeHTTP), + } + return httpsConfig.ListenAndServeTLS("", "") - priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) +} + +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 err - } - certBlock, _ := pem.Decode([]byte(ev.Content)) - if certBlock == nil { - fmt.Fprintf(os.Stderr, "Failed to parse certificate PEM.") - os.Exit(1) + return tls.Certificate{}, fmt.Errorf("failed to write error: %w", err) } + os.Exit(1) + } - parsedCert, err := x509.ParseCertificate(certBlock.Bytes) + 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 err - } - cert = tls.Certificate{ - Certificate: [][]byte{certBlock.Bytes}, - PrivateKey: priv, - Leaf: parsedCert, + return tls.Certificate{}, fmt.Errorf("failed to write error: %w", err) } + os.Exit(1) } - 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), + 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) } - return httpsConfig.ListenAndServeTLS("", "") + 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 } diff --git a/exit/mutex.go b/exit/mutex.go index 3d111ae..03e87a3 100644 --- a/exit/mutex.go +++ b/exit/mutex.go @@ -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() } diff --git a/exit/nostr.go b/exit/nostr.go index 8864092..a819de1 100644 --- a/exit/nostr.go +++ b/exit/nostr.go @@ -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