From 8b610b1bdbc2312aa8cfd3cebd5837a82cab59a2 Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 1 Aug 2026 14:54:08 +0000 Subject: [PATCH] fix: tunnel credentials now use plain terminal prompts Bubbletea and huh were fighting over terminal input. Fixed by: - TUI exits when user selects 'Remote Assistance' - Plain terminal prompts in main.go collect jumphost URL, user, pass - Starts tunnel directly, shows connection string for admin - No more conflicting rendering between TUI and forms --- cmd/wallarm/main.go | 75 ++++++++++++++++++++++++-- internal/ui/ui.go | 125 ++++---------------------------------------- 2 files changed, 79 insertions(+), 121 deletions(-) diff --git a/cmd/wallarm/main.go b/cmd/wallarm/main.go index 1c6d4ea..fbc6308 100644 --- a/cmd/wallarm/main.go +++ b/cmd/wallarm/main.go @@ -10,23 +10,84 @@ package main import ( + "bufio" "flag" "fmt" "os" + "strings" "git.sechpoint.app/customer-engineering/wallarm/internal/preflight" "git.sechpoint.app/customer-engineering/wallarm/internal/tunnel" "git.sechpoint.app/customer-engineering/wallarm/internal/ui" ) -// Embedded tunnel key — set at build time with: -// -// go build -ldflags "-X main.tunnelKey=$(cat ~/.wallarm/tunnel_key)" +// Embedded tunnel key for --tunnel flag (set at build time with -ldflags). var tunnelKey string -// Version is set at build time with -ldflags "-X main.version=1.0.0" +// Version set at build time. var version = "dev" +// runTunnelFlow prompts for tunnel credentials in plain terminal and starts the tunnel. +func runTunnelFlow() { + fmt.Println() + fmt.Println("🔗 Remote Assistance — Secure Tunnel Setup") + fmt.Println() + fmt.Println("The tunnel lets a remote admin connect to this server through your jumphost.") + fmt.Println("Enter the SSH endpoint where the admin will connect:") + fmt.Println() + + reader := bufio.NewReader(os.Stdin) + + fmt.Print("Jumphost URL [ssh.sechpoint.app:443]: ") + host, _ := reader.ReadString('\n') + host = strings.TrimSpace(host) + if host == "" { + host = "ssh.sechpoint.app:443" + } + + fmt.Print("Username [wallarm-tunnel]: ") + user, _ := reader.ReadString('\n') + user = strings.TrimSpace(user) + if user == "" { + user = "wallarm-tunnel" + } + + fmt.Print("Password or SSH key path: ") + pass, _ := reader.ReadString('\n') + pass = strings.TrimSpace(pass) + if pass == "" { + fmt.Println("No credentials provided. Aborting.") + return + } + + fmt.Println() + fmt.Println("Starting tunnel...") + + cfg := tunnel.Config{ + Jumphost: host, + User: user, + RemotePort: 9042, + LocalSSHPort: 22, + } + + if strings.HasPrefix(pass, "/") { + cfg.KeyPath = pass + } else { + cfg.Password = pass + } + + fmt.Println("Share this with your admin:") + fmt.Println() + fmt.Printf(" ssh -p 9042 %s@%s\n", user, host) + fmt.Println() + fmt.Println("Press Ctrl+C to close the tunnel.") + + if err := tunnel.Start(cfg); err != nil { + fmt.Fprintf(os.Stderr, "Tunnel error: %v\n", err) + os.Exit(1) + } +} + func main() { flag.Usage = func() { fmt.Fprintf(os.Stderr, `wallarm — Wallarm Deployment Manager @@ -95,8 +156,12 @@ deployment. On subsequent runs, it shows your existing deployments. fmt.Println() // 2. Launch bubbletea TUI (handles wizard vs dashboard routing) - if err := ui.Run(result); err != nil { + err := ui.Run(result) + if err != nil { fmt.Fprintf(os.Stderr, "UI error: %v\n", err) os.Exit(1) } + + // 3. If TUI exited cleanly (user chose Remote Assistance), start tunnel flow + runTunnelFlow() } diff --git a/internal/ui/ui.go b/internal/ui/ui.go index ed9be2d..1012fa9 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -15,7 +15,6 @@ import ( "git.sechpoint.app/customer-engineering/wallarm/internal/native" "git.sechpoint.app/customer-engineering/wallarm/internal/preflight" "git.sechpoint.app/customer-engineering/wallarm/internal/state" - "git.sechpoint.app/customer-engineering/wallarm/internal/tunnel" ) // Styles @@ -30,14 +29,11 @@ var ( // Model is the top-level bubbletea model. type Model struct { - state stateView - width int - height int - status string // feedback message during deployment - deploying bool // true while deployment is in progress - tunnelUser string // tunnel credentials (collected in remote config) - tunnelPass string - tunnelHost string + state stateView + width int + height int + status string // feedback message during deployment + deploying bool // true while deployment is in progress } type stateView int @@ -45,9 +41,7 @@ type stateView int const ( viewPreflight stateView = iota viewRoute - viewDeployChoice // new: local deploy or remote assist? - viewRemoteConfig // new: prompt for tunnel credentials - viewTunnelActive // new: tunnel is running, waiting for remote admin + viewDeployChoice // local deploy or remote assist? viewWizard viewDashboard viewDone @@ -90,15 +84,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case "2": if m.state == viewDeployChoice { - m.state = viewRemoteConfig - return m, nil - } - case "s": - if m.state == viewRemoteConfig && m.tunnelHost == "" { - return m, m.collectTunnelCreds() - } - if m.state == viewRemoteConfig && m.tunnelHost != "" { - return m, m.startTunnel() + return m, func() tea.Msg { return tea.Quit() } // exit TUI for tunnel flow } case "enter": if m.state == viewDone || m.state == viewError { @@ -115,14 +101,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.state = viewDone } return m, nil - case tunnelCredsMsg: - if msg.err != nil { - m.status = fmt.Sprintf("Could not collect credentials: %v", msg.err) - m.state = viewError - } else { - m.state = viewRemoteConfig // refresh to show collected creds - } - return m, nil case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height @@ -145,10 +123,6 @@ func (m Model) View() string { return "" case viewDeployChoice: return deployChoiceView(m) - case viewRemoteConfig: - return remoteConfigView(m) - case viewTunnelActive: - return tunnelActiveView(m) case viewWizard: return wizardView(m) case viewDashboard: @@ -161,57 +135,6 @@ func (m Model) View() string { return "" } -// collectTunnelCreds runs a huh form to collect tunnel endpoint details. -func (m Model) collectTunnelCreds() tea.Cmd { - return func() tea.Msg { - var host, user, pass string - err := huh.NewForm( - huh.NewGroup( - huh.NewInput(). - Title("Jumphost URL"). - Placeholder("ssh.sechpoint.app:443"). - Value(&host), - huh.NewInput(). - Title("Username"). - Placeholder("wallarm-tunnel"). - Value(&user), - huh.NewInput(). - Title("Password (or SSH key path)"). - Placeholder("password or /path/to/key"). - EchoMode(huh.EchoModePassword). - Value(&pass), - ), - ).WithTheme(huh.ThemeCharm()).Run() - - m.tunnelHost = host - m.tunnelUser = user - m.tunnelPass = pass - return tunnelCredsMsg{err: err} - } -} - -type tunnelCredsMsg struct{ err error } - -// startTunnel opens the reverse SSH tunnel with collected credentials. -func (m Model) startTunnel() tea.Cmd { - return func() tea.Msg { - cfg := tunnel.Config{ - Jumphost: m.tunnelHost, - User: m.tunnelUser, - Password: m.tunnelPass, - RemotePort: 9042, - LocalSSHPort: 22, - } - // If password looks like a file path, use key auth instead - if strings.HasPrefix(m.tunnelPass, "/") { - cfg.Password = "" - cfg.KeyPath = m.tunnelPass - } - - err := tunnel.Start(cfg) - return deployCompleteMsg{err: err} - } -} func (m Model) startDeploy() tea.Cmd { return func() tea.Msg { m.deploying = true @@ -246,36 +169,6 @@ func deployChoiceView(m Model) string { return s } -// ─── Remote Config ─────────────────────────────────────────────────── - -func remoteConfigView(m Model) string { - s := titleStyle.Render("🔗 Remote Assistance Setup") + "\n\n" - s += "Enter the tunnel endpoint where the remote admin will connect:\n\n" - - if m.tunnelHost == "" { - s += dimStyle.Render(" (Form will appear below — enter values and press Enter to continue)") + "\n" - s += dimStyle.Render(" Press 's' to start the tunnel after filling in credentials") + "\n" - } else { - s += fmt.Sprintf(" Jumphost: %s\n", goodStyle.Render(m.tunnelHost)) - s += fmt.Sprintf(" User: %s\n", m.tunnelUser) - s += dimStyle.Render("\n Tunnel credentials collected. Press 's' to start.") + "\n" - } - return s -} - -// ─── Tunnel Active ─────────────────────────────────────────────────── - -func tunnelActiveView(m Model) string { - s := titleStyle.Render("🔗 Tunnel Active") + "\n\n" - s += goodStyle.Render("Secure tunnel is running.") + "\n\n" - s += "Share this with your admin:\n" - s += dimStyle.Render(" ┌─────────────────────────────────────────┐") + "\n" - s += fmt.Sprintf(" │ %s", activeStyle.Render("ssh -p 9042 root@"+m.tunnelHost)) + dimStyle.Render(" │") + "\n" - s += dimStyle.Render(" └─────────────────────────────────────────┘") + "\n\n" - s += dimStyle.Render("The admin will see the deployment wizard on their terminal.") + "\n" - s += dimStyle.Render("Tunnel stays open until you press q.") + "\n" - return s -} func wizardView(m Model) string { s := titleStyle.Render("🛡️ Wallarm Setup Wizard") + "\n" @@ -289,7 +182,7 @@ func wizardView(m Model) string { s += activeStyle.Render("Wallarm Native Node Deployment") + "\n" s += " Press Enter to begin configuration" + "\n\n" - s += activeStyle.Render("Step 2: Cloud region") + "\n" + s += activeStyle.Render("Cloud region:") + "\n" r := preflightResult if r.USReachable { s += goodStyle.Render(" US") + " — us1.api.wallarm.com (reachable)\n" @@ -301,7 +194,7 @@ func wizardView(m Model) string { } else { s += dimStyle.Render(" EU — not reachable") + "\n" } - s += dimStyle.Render("\nPress q to quit") + s += dimStyle.Render("\nPress Enter to continue, q to quit") return s }