diff options
| author | Gab Virebent <gabriel1@virebent.art> | 2026-06-20 01:07:45 +0000 |
|---|---|---|
| committer | Gab Virebent <gabriel1@virebent.art> | 2026-06-20 01:07:45 +0000 |
| commit | aa459e3b84cc4c5d908f3781c9253848c18640c3 (patch) | |
| tree | 8454b956cc0fa027034c8291f39d58ece469e10c /internal/nym/client.go | |
| download | nymdrop-aa459e3b84cc4c5d908f3781c9253848c18640c3.tar.gz nymdrop-aa459e3b84cc4c5d908f3781c9253848c18640c3.tar.xz nymdrop-aa459e3b84cc4c5d908f3781c9253848c18640c3.zip | |
Initial public release: Nym-native anonymous submission system
End-to-end verified pipeline (browser -> HTTP relay -> Nym mixnet -> reader).
Client-side X25519+HKDF+AES-GCM-256, no-log blind relay, AGPL-3.0.
Diffstat (limited to 'internal/nym/client.go')
| -rw-r--r-- | internal/nym/client.go | 141 |
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 |
