diff --git a/cmd/exit/exit.go b/cmd/exit/exit.go index 1c29a78..2700b02 100644 --- a/cmd/exit/exit.go +++ b/cmd/exit/exit.go @@ -3,11 +3,24 @@ package main import ( "github.com/asmogo/nws/config" "github.com/asmogo/nws/exit" - - "golang.org/x/net/context" + "github.com/spf13/cobra" + "log/slog" ) +var httpsPort int32 +var httpTarget string + func main() { + rootCmd := &cobra.Command{Use: "exit", Run: startExitNode} + rootCmd.Flags().Int32VarP(&httpsPort, "port", "p", 0, "port for the https reverse proxy") + rootCmd.Flags().StringVarP(&httpTarget, "target", "t", "", "target for the https reverse proxy (your local service)") + err := rootCmd.Execute() + if err != nil { + panic(err) + } +} +func startExitNode(cmd *cobra.Command, args []string) { + // load the configuration // from the environment cfg, err := config.LoadConfig[config.ExitConfig]() @@ -17,7 +30,17 @@ func main() { // create a new gw server // and start it - ctx := context.Background() + ctx := cmd.Context() exitNode := exit.NewExit(ctx, cfg) + if httpsPort != 0 { + slog.Info("starting exit node with https reverse proxy", "port", httpsPort) + go func() { + err = exitNode.StartReverseProxy(httpTarget, httpsPort) + if err != nil { + panic(err) + } + }() + + } exitNode.ListenAndServe(ctx) } diff --git a/docker-compose.yaml b/docker-compose.yaml index 5e27b72..ba9b5f8 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -34,6 +34,18 @@ services: - NOSTR_RELAYS=ws://nostr-relay:8080 - NOSTR_PRIVATE_KEY=003632642b6df1bb7f150c25aae079d590e6cfcceca924304154fbc2a3a938e3 - BACKEND_HOST=mint:3338 + exit-https: + build: + context: . + dockerfile: cmd/exit/Dockerfile + container_name: exit-https + command: ["./exit", "--port", "4443", "--target", "http://mint:3338"] + networks: + nostr: + environment: + - NOSTR_RELAYS=ws://nostr-relay:8080 + - NOSTR_PRIVATE_KEY=213632642b6df1bb7f150c25aae079d590e6cfcceca924304154fbc2a3a938e3 + - BACKEND_HOST=localhost:4443 proxy: build: context: . @@ -45,7 +57,6 @@ services: nostr: environment: - NOSTR_RELAYS=ws://nostr-relay:8080 - - NOSTR_PRIVATE_KEY=b0aceff311951aaa014c3296f4346f91f7fd4fc17396e060acbb48d2f42ef1fe nostr: image: scsibug/nostr-rs-relay:latest container_name: nostr-relay diff --git a/exit/exit.go b/exit/exit.go index dbd8a02..54ab42e 100644 --- a/exit/exit.go +++ b/exit/exit.go @@ -41,6 +41,9 @@ type Exit struct { // incomingChannel represents a channel used to receive incoming events from relays. incomingChannel chan nostr.IncomingEvent + + nprofile string + publicKey string } // NewExit creates a new Exit node with the provided context and config. @@ -76,6 +79,8 @@ func NewExit(ctx context.Context, config *config.ExitConfig) *Exit { if err != nil { panic(err) } + exit.nprofile = profile + exit.publicKey = pubKey slog.Info("created exit node", "profile", profile) err = exit.setSubscriptions(ctx) if err != nil { diff --git a/exit/https.go b/exit/https.go new file mode 100644 index 0000000..1d6abed --- /dev/null +++ b/exit/https.go @@ -0,0 +1,214 @@ +package exit + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "github.com/asmogo/nws/protocol" + "github.com/nbd-wtf/go-nostr" + "github.com/nbd-wtf/go-nostr/nip04" + "math/big" + "net/http" + "net/http/httputil" + "net/url" + "os" + "time" +) + +func (e *Exit) DeleteEvent(ctx context.Context, ev *nostr.Event) error { + for _, responseRelay := range e.config.NostrRelays { + var relay *nostr.Relay + relay, err := e.pool.EnsureRelay(responseRelay) + if err != nil { + return err + } + event := nostr.Event{ + CreatedAt: nostr.Now(), + PubKey: e.publicKey, + Kind: nostr.KindDeletion, + Tags: nostr.Tags{ + nostr.Tag{"e", ev.ID}, + }, + } + err = event.Sign(e.config.NostrPrivateKey) + err = relay.Publish(ctx, event) + if err != nil { + return err + } + } + return nil +} + +func (e *Exit) StartReverseProxy(httpTarget string, port int32) error { + ctx := context.Background() + ev := e.pool.QuerySingle(ctx, e.config.NostrRelays, nostr.Filter{ + Authors: []string{e.publicKey}, + Kinds: []int{nostr.KindTextNote}, + Tags: nostr.TagMap{"p": []string{e.nprofile}}, + }) + var cert tls.Certificate + if ev == nil { + certificate, err := e.createAndStoreCertificateData(ctx) + if err != nil { + return err + } + cert = *certificate + } else { + // load private key from file + privateKeyEvent := e.pool.QuerySingle(ctx, e.config.NostrRelays, nostr.Filter{ + Authors: []string{e.publicKey}, + Kinds: []int{nostr.KindEncryptedDirectMessage}, + Tags: nostr.TagMap{"p": []string{e.nprofile}}, + }) + 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)) + 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), + } + return httpsConfig.ListenAndServeTLS("", "") + +} + +func (e *Exit) createAndStoreCertificateData(ctx context.Context) (*tls.Certificate, error) { + priv, _ := rsa.GenerateKey(rand.Reader, 2048) + notBefore := time.Now() + notAfter := notBefore.Add(365 * 24 * time.Hour) + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit) + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"NWS"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + certBytes, _ := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + 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.nprofile), keyPEM, 0644) + if err != nil { + return nil, err + } + cert, _ := tls.X509KeyPair(certPEM, keyPEM) + certificate, err := e.storeCertificate(ctx, certPEM) + if err != nil { + return certificate, err + } + err = e.storePrivateKey(ctx, keyPEM) + if err != nil { + return certificate, err + } + return &cert, nil +} + +func (e *Exit) storePrivateKey(ctx context.Context, keyPEM []byte) error { + s, err := protocol.NewEventSigner(e.config.NostrPrivateKey) + if err != nil { + return err + } + event, err := s.CreateSignedEvent(e.publicKey, nostr.KindEncryptedDirectMessage, nostr.Tags{ + nostr.Tag{"p", e.nprofile}, + }, protocol.WithData(keyPEM)) + if err != nil { + return err + } + for _, responseRelay := range e.config.NostrRelays { + var relay *nostr.Relay + relay, err = e.pool.EnsureRelay(responseRelay) + if err != nil { + return err + } + err = relay.Publish(ctx, event) + if err != nil { + return err + } + } + return nil +} +func (e *Exit) storeCertificate(ctx context.Context, certPEM []byte) (*tls.Certificate, error) { + event := nostr.Event{ + CreatedAt: nostr.Now(), + PubKey: e.publicKey, + Kind: nostr.KindTextNote, + Content: string(certPEM), + Tags: nostr.Tags{ + nostr.Tag{"p", e.nprofile}, + }, + } + err := event.Sign(e.config.NostrPrivateKey) + if err != nil { + return nil, err + } + for _, responseRelay := range e.config.NostrRelays { + var relay *nostr.Relay + relay, err = e.pool.EnsureRelay(responseRelay) + if err != nil { + return nil, err + } + err = relay.Publish(ctx, event) + if err != nil { + return nil, err + } + } + return nil, nil +} diff --git a/netstr/conn.go b/netstr/conn.go index d2baf69..461995c 100644 --- a/netstr/conn.go +++ b/netstr/conn.go @@ -171,7 +171,7 @@ func (nc *NostrConnection) handleNostrWrite(b []byte, err error) (int, error) { protocol.WithType(protocol.MessageTypeSocks5), protocol.WithData(b), } - ev, err := signer.CreateSignedEvent(publicKey, nostr.Tags{nostr.Tag{"p", publicKey}}, opts...) + ev, err := signer.CreateSignedEvent(publicKey, protocol.KindEphemeralEvent, nostr.Tags{nostr.Tag{"p", publicKey}}, opts...) if err != nil { return 0, err } diff --git a/netstr/dial.go b/netstr/dial.go index 878d3c6..b7ffd58 100644 --- a/netstr/dial.go +++ b/netstr/dial.go @@ -43,7 +43,7 @@ func DialSocks(pool *nostr.SimplePool) func(ctx context.Context, net_, addr stri protocol.WithUUID(connectionID), protocol.WithDestination(addr), } - ev, err := signer.CreateSignedEvent(publicKey, + ev, err := signer.CreateSignedEvent(publicKey, protocol.KindEphemeralEvent, nostr.Tags{nostr.Tag{"p", publicKey}}, opts...) diff --git a/protocol/signer.go b/protocol/signer.go index 4323639..f696b97 100644 --- a/protocol/signer.go +++ b/protocol/signer.go @@ -34,11 +34,11 @@ func NewEventSigner(privateKey string) (*EventSigner, error) { // CreateEvent creates a new Event with the provided tags. The Public Key and the // current timestamp are set automatically. The Kind is set to KindEphemeralEvent. -func (s *EventSigner) CreateEvent(tags nostr.Tags) nostr.Event { +func (s *EventSigner) CreateEvent(kind int, tags nostr.Tags) nostr.Event { return nostr.Event{ PubKey: s.PublicKey, CreatedAt: nostr.Now(), - Kind: KindEphemeralEvent, + Kind: kind, Tags: tags, } } @@ -51,7 +51,7 @@ func (s *EventSigner) CreateEvent(tags nostr.Tags) nostr.Event { // The encrypted message is set as the content of the event. // Finally, the event is signed with the private key of the EventSigner, setting the event ID and event Sig fields. // The signed event is returned along with any error that occurs. -func (s *EventSigner) CreateSignedEvent(targetPublicKey string, tags nostr.Tags, opts ...MessageOption) (nostr.Event, error) { +func (s *EventSigner) CreateSignedEvent(targetPublicKey string, kind int, tags nostr.Tags, opts ...MessageOption) (nostr.Event, error) { sharedKey, err := nip04.ComputeSharedSecret(targetPublicKey, s.privateKey) if err != nil { return nostr.Event{}, err @@ -64,7 +64,7 @@ func (s *EventSigner) CreateSignedEvent(targetPublicKey string, tags nostr.Tags, return nostr.Event{}, err } encryptedMessage, err := nip04.Encrypt(string(messageJson), sharedKey) - ev := s.CreateEvent(tags) + ev := s.CreateEvent(kind, tags) ev.Content = encryptedMessage // calling Sign sets the event ID field and the event Sig field err = ev.Sign(s.privateKey)