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
|
package nym
import (
"encoding/binary"
"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
recipient []byte // parsed 96-byte wire form of journalistAddr
wsPort int
dryRun bool
process *exec.Cmd
ws *websocket.Conn
binFS fs.FS
configDir string
}
func New(journalistAddr string, binFS fs.FS) (*Client, error) {
recipient, err := ParseRecipient(journalistAddr)
if err != nil {
return nil, fmt.Errorf("parse journalist nym address: %w", err)
}
return &Client{journalistAddr: journalistAddr, recipient: recipient, wsPort: 1978, binFS: binFS}, nil
}
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")
}
// Send delivers payload to the journalist's Nym address through the mixnet
// using nym-client's binary websocket protocol (tag || recipient || conn_id ||
// data_len || data). This avoids the JSON+base64 wrapping of the old text
// protocol, which inflated every payload by ~4/3 before it ever reached the
// nym-client's own 16MB hardcoded websocket message limit.
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
}
req := make([]byte, 0, 1+len(c.recipient)+8+8+len(payload))
req = append(req, ReqSend)
req = append(req, c.recipient...)
req = binary.BigEndian.AppendUint64(req, 0) // connection_id: none
req = binary.BigEndian.AppendUint64(req, uint64(len(payload)))
req = append(req, payload...)
if err := websocket.Message.Send(c.ws, req); 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
|