package main import ( "embed" "fmt" "io" "io/fs" "net" "net/http" "os" "os/exec" "os/signal" "path/filepath" "runtime" "syscall" "time" "golang.org/x/net/proxy" ) //go:embed bin/nym-socks5-client-linux-amd64 var nymBin embed.FS // nymdropProviderAddr is the Nym network requester — set at build time via -ldflags var nymdropProviderAddr = "NYMDROP_PROVIDER_ADDR_PLACEHOLDER" // nymdropInboxAddr is the Nym address of the nymdrop-server SP — set at build time via -ldflags var nymdropInboxAddr = "NYMDROP_INBOX_ADDR_PLACEHOLDER" func main() { // Extract nym-socks5-client binary to temp dir tmpDir, err := os.MkdirTemp("", "nymdrop-*") if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } defer os.RemoveAll(tmpDir) nymBinName := "nym-socks5-client" if runtime.GOOS == "windows" { nymBinName += ".exe" } nymBinPath := filepath.Join(tmpDir, nymBinName) srcName := "bin/nym-socks5-client-linux-amd64" data, err := fs.ReadFile(nymBin, srcName) if err != nil { fmt.Fprintf(os.Stderr, "embedded binary not found: %v\n", err) os.Exit(1) } if err := os.WriteFile(nymBinPath, data, 0700); err != nil { fmt.Fprintf(os.Stderr, "extract binary: %v\n", err) os.Exit(1) } // Init nym client config if not already done homeDir, _ := os.UserHomeDir() configDir := filepath.Join(homeDir, ".nymdrop-source") initCmd := exec.Command(nymBinPath, "init", "--id", "nymdrop-source", "--provider", nymdropProviderAddr, "--home", configDir) initCmd.Stdout = os.Stdout initCmd.Stderr = os.Stderr _ = initCmd.Run() // ignore error if already initialised // Start nym-socks5-client nymCmd := exec.Command(nymBinPath, "run", "--id", "nymdrop-source", "--home", configDir, "--port", "11080") nymCmd.Stdout = os.Stdout nymCmd.Stderr = os.Stderr if err := nymCmd.Start(); err != nil { fmt.Fprintf(os.Stderr, "nym start: %v\n", err) os.Exit(1) } defer nymCmd.Process.Kill() // Wait for SOCKS5 to be ready fmt.Println("Connecting to Nym mixnet...") for i := 0; i < 30; i++ { conn, err := net.DialTimeout("tcp", "127.0.0.1:11080", 300*time.Millisecond) if err == nil { conn.Close() break } time.Sleep(500 * time.Millisecond) } // Serve submission form locally mux := http.NewServeMux() mux.HandleFunc("/", serveForm) mux.HandleFunc("/submit", handleSubmit) srv := &http.Server{Addr: "127.0.0.1:18080", Handler: mux} go srv.ListenAndServe() // Open browser openBrowser("http://127.0.0.1:18080") fmt.Println("NymDrop ready — http://127.0.0.1:18080") // Wait for interrupt sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) <-sig fmt.Println("\nShutting down.") } func openBrowser(url string) { switch runtime.GOOS { case "linux": exec.Command("xdg-open", url).Start() case "darwin": exec.Command("open", url).Start() case "windows": exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() } } func serveForm(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-store") fmt.Fprint(w, submissionForm) } func handleSubmit(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } r.Body = http.MaxBytesReader(w, r.Body, 10*1024*1024) defer r.Body.Close() payload, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "read error", http.StatusBadRequest) return } defer func() { for i := range payload { payload[i] = 0 } }() if len(payload) == 0 { http.Error(w, "empty payload", http.StatusBadRequest) return } dialer, err := proxy.SOCKS5("tcp", "127.0.0.1:11080", nil, proxy.Direct) if err != nil { fmt.Fprintf(os.Stderr, "socks5 dialer: %v\n", err) http.Error(w, "internal error", http.StatusInternalServerError) return } conn, err := dialer.Dial("tcp", nymdropInboxAddr) if err != nil { fmt.Fprintf(os.Stderr, "nym dial: %v\n", err) http.Error(w, "delivery failed", http.StatusBadGateway) return } defer conn.Close() if _, err := conn.Write(payload); err != nil { fmt.Fprintf(os.Stderr, "nym send: %v\n", err) http.Error(w, "delivery failed", http.StatusBadGateway) return } w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) } const submissionForm = `
Anonymous submission — encrypted in your browser, delivered over Nym mixnet.
Your submission is encrypted before leaving your device. The server keeps no logs. Once delivered over Nym, no record exists on this server.