Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lint exit package #40

Merged
merged 1 commit into from
Sep 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading