summaryrefslogtreecommitdiffstats
path: root/internal/nym
diff options
context:
space:
mode:
Diffstat (limited to 'internal/nym')
-rw-r--r--internal/nym/client.go141
1 files changed, 141 insertions, 0 deletions
diff --git a/internal/nym/client.go b/internal/nym/client.go
new file mode 100644
index 0000000..89fcf52
--- /dev/null
+++ b/internal/nym/client.go
@@ -0,0 +1,141 @@
+package nym
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "time"
+
+ "golang.org/x/net/websocket"
+)
+
+// Client sends payloads through the Nym mixnet via an embedded nym-client subprocess.
+type Client struct {
+ journalistAddr string
+ wsPort int
+ dryRun bool
+ process *exec.Cmd
+ ws *websocket.Conn
+ binFS fs.FS
+ configDir string
+}
+
+func New(journalistAddr string, binFS fs.FS) *Client {
+ return &Client{journalistAddr: journalistAddr, wsPort: 1978, binFS: binFS}
+}
+
+func NewDryRun(journalistAddr string) *Client {
+ return &Client{journalistAddr: journalistAddr, dryRun: true}
+}
+
+// Start extracts the nym-client binary, inits config, starts the subprocess,
+// and connects to its WebSocket. No-op in dry-run mode.
+func (c *Client) Start() error {
+ if c.dryRun {
+ fmt.Printf("[dry-run] Nym client skipped — journalist: %s\n", c.journalistAddr)
+ return nil
+ }
+
+ binPath, tmpDir, err := extractBin(c.binFS)
+ if err != nil {
+ return fmt.Errorf("extract nym-client: %w", err)
+ }
+
+ homeDir, _ := os.UserHomeDir()
+ c.configDir = filepath.Join(homeDir, ".nymdrop-server")
+
+ // Init (idempotent — ignore error if already initialised).
+ exec.Command(binPath, "init", "--id", "nymdrop-server").Run()
+
+ cmd := exec.Command(binPath, "run", "--id", "nymdrop-server",
+ "--port", fmt.Sprintf("%d", c.wsPort))
+ cmd.Stdout = os.Stderr // route nym-client noise to our stderr
+ cmd.Stderr = os.Stderr
+ if err := cmd.Start(); err != nil {
+ os.RemoveAll(tmpDir)
+ return fmt.Errorf("nym-client start: %w", err)
+ }
+ c.process = cmd
+
+ wsURL := fmt.Sprintf("ws://127.0.0.1:%d", c.wsPort)
+ for i := 0; i < 120; i++ {
+ ws, err := websocket.Dial(wsURL, "", "http://localhost/")
+ if err == nil {
+ c.ws = ws
+ return nil
+ }
+ time.Sleep(500 * time.Millisecond)
+ }
+ return fmt.Errorf("nym-client websocket did not start in time")
+}
+
+type sendMsg struct {
+ Type string `json:"type"`
+ Message string `json:"message"`
+ Recipient string `json:"recipient"`
+ WithReplySurb bool `json:"withReplySurb"`
+}
+
+// Send delivers payload to the journalist's Nym address through the mixnet.
+func (c *Client) Send(payload []byte) error {
+ if c.dryRun {
+ fmt.Printf("[dry-run] payload received: %d bytes — would send to %s\n",
+ len(payload), c.journalistAddr)
+ return nil
+ }
+ msg := sendMsg{
+ Type: "send",
+ Message: base64.StdEncoding.EncodeToString(payload),
+ Recipient: c.journalistAddr,
+ WithReplySurb: false,
+ }
+ raw, err := json.Marshal(msg)
+ if err != nil {
+ return fmt.Errorf("marshal: %w", err)
+ }
+ if err := websocket.Message.Send(c.ws, string(raw)); err != nil {
+ return fmt.Errorf("ws send: %w", err)
+ }
+ return nil
+}
+
+// Stop terminates the subprocess.
+func (c *Client) Stop() {
+ if c.ws != nil {
+ c.ws.Close()
+ }
+ if c.process != nil && c.process.Process != nil {
+ c.process.Process.Kill()
+ }
+}
+
+func extractBin(binFS fs.FS) (string, string, error) {
+ tmpDir, err := os.MkdirTemp("", "nymdrop-server-*")
+ if err != nil {
+ return "", "", err
+ }
+ name := "nym-client"
+ if runtime.GOOS == "windows" {
+ name += ".exe"
+ }
+ binPath := filepath.Join(tmpDir, name)
+ data, err := fs.ReadFile(binFS, "bin/nym-client-linux-amd64")
+ if err != nil {
+ os.RemoveAll(tmpDir)
+ return "", "", err
+ }
+ if err := os.WriteFile(binPath, data, 0700); err != nil {
+ os.RemoveAll(tmpDir)
+ return "", "", err
+ }
+ return binPath, tmpDir, nil
+}
+
+// Ensure unused import io is not flagged.
+var _ = io.EOF