Skip to content

Commit

Permalink
linted stuff in exit package
Browse files Browse the repository at this point in the history
  • Loading branch information
asmogo committed Sep 1, 2024
1 parent 59ff503 commit a8d3de5
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 85 deletions.
45 changes: 24 additions & 21 deletions exit/exit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -173,29 +173,29 @@ 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.
// It sets up the incoming event channel and starts a goroutine to handle the events.
// 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},
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}

Expand Down
146 changes: 90 additions & 56 deletions exit/https.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"log/slog"
"math/big"
Expand All @@ -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()

Expand All @@ -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
}
Expand Down
6 changes: 3 additions & 3 deletions exit/mutex.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package exit

import (
"fmt"
"log/slog"
"sync"
)

Expand Down Expand Up @@ -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()
}
Loading

0 comments on commit a8d3de5

Please sign in to comment.