1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
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
|