package handler import ( "bufio" "encoding/json" "fmt" "io" "log" "strings" "time" "github.com/gofiber/contrib/websocket" "github.com/gofiber/fiber/v2" "git.tukangketik.id/swanadiva/hostkeeper/v2/internal/sshconn" ) func Terminal(c *fiber.Ctx) error { hosts := hostStore.All() type hostItem struct { ID string `json:"id"` Name string `json:"name"` IP string `json:"ip"` } var available []hostItem for _, h := range hosts { if h.Status == "active" { available = append(available, hostItem{h.ID, h.Name, h.IP}) } } hostsJSON, _ := json.Marshal(available) return c.Render("terminal", fiber.Map{ "View": "terminal", "Title": "Terminal", "NavItems": navItems, "Hosts": available, "HostsJSON": string(hostsJSON), }) } func TerminalWS(c *websocket.Conn) { hostID := c.Params("host", "unknown") log.Printf("WS connected: host=%s", hostID) host, found := hostStore.Get(hostID) if !found || host.Hostname == "" { mockTerminal(c, hostID) return } client, err := sshconn.Dial(host) if err != nil { log.Printf("SSH failed for %s: %v, falling back to mock", hostID, err) mockTerminal(c, hostID) return } defer client.Close() welcome := fmt.Sprintf("\x1b[1;32mConnected to %s (%s)\x1b[0m\r\n", host.Name, host.IP) c.WriteMessage(websocket.TextMessage, []byte(welcome)) stdinR, stdinW := io.Pipe() stdoutR, stdoutW := io.Pipe() go func() { scanner := bufio.NewScanner(stdoutR) for scanner.Scan() { c.WriteMessage(websocket.TextMessage, []byte(scanner.Text()+"\r\n")) } }() go func() { defer stdinW.Close() for { _, msg, err := c.ReadMessage() if err != nil { return } input := string(msg) if input == "\x03" { client.Close() return } stdinW.Write([]byte(input)) } }() if err := client.Shell(stdinR, stdoutW, stdoutW, 24, 80); err != nil { log.Printf("SSH session ended: %v", err) } log.Printf("WS disconnected: host=%s", hostID) } func mockTerminal(c *websocket.Conn, host string) { welcome := fmt.Sprintf("\x1b[1;33m%s (mock mode)\x1b[0m\r\n\x1b[2mConnected at %s\x1b[0m\r\n\r\n", host, time.Now().Format(time.RFC822)) c.WriteMessage(websocket.TextMessage, []byte(welcome)) prompt := fmt.Sprintf("\x1b[1;34m%s:~$\x1b[0m ", host) cwd := "~" buf := "" for { _, msg, err := c.ReadMessage() if err != nil { break } input := string(msg) switch { case input == "\r": output := handleCommand(buf, host, &cwd) c.WriteMessage(websocket.TextMessage, []byte(output+prompt)) buf = "" case input == "\x7f": if len(buf) > 0 { buf = buf[:len(buf)-1] c.WriteMessage(websocket.TextMessage, []byte("\b \b")) } case input == "\x03": buf = "" c.WriteMessage(websocket.TextMessage, []byte("^C\r\n"+prompt)) default: if input >= " " && input <= "~" { buf += input c.WriteMessage(websocket.TextMessage, []byte(input)) } } } log.Printf("Mock WS disconnected: host=%s", host) } func handleCommand(cmd, host string, cwd *string) string { cmd = strings.TrimSpace(cmd) if cmd == "" { return "" } parts := strings.Fields(cmd) if len(parts) == 0 { return "" } switch parts[0] { case "clear": return "\x1b[2J\x1b[H" case "exit", "logout": return "logout\r\n\x1b[2J\x1b[H\x1b[1;31mConnection closed.\x1b[0m\r\n" case "pwd": return *cwd + "\r\n" case "whoami": return "deploy\r\n" case "hostname": return host + "\r\n" case "date": return time.Now().Format("Mon Jan 2 15:04:05 MST 2006") + "\r\n" case "uptime": return fmt.Sprintf(" 13:42:17 up %d day, 1 user, load average: 0.08, 0.12, 0.10\r\n", 1+time.Now().Day()%30) case "uname": if len(parts) > 1 && parts[1] == "-a" { return fmt.Sprintf("Linux %s 6.8.0-45-generic #47-Ubuntu SMP PREEMPT x86_64 x86_64 x86_64 GNU/Linux\r\n", host) } return "Linux\r\n" case "ls": if *cwd == "~" || *cwd == "/home/deploy" { return "Documents Downloads projects config.yml README.md .env\r\n" } return "total 0\r\n" case "cd": if len(parts) > 1 { *cwd = parts[1] } else { *cwd = "~" } return "" case "cat": if len(parts) > 1 { return fmt.Sprintf("\x1b[1;37m# Content of %s\x1b[0m\r\n(not implemented in mock shell)\r\n", parts[1]) } return "" case "echo": return strings.Join(parts[1:], " ") + "\r\n" case "ssh": return "\x1b[1;33mSSH forwarding not available in mock mode\x1b[0m\r\n" case "help": return "Available commands: clear, exit, pwd, whoami, hostname, date, uptime, uname, ls, cd, cat, echo, ssh, help\r\n" default: return fmt.Sprintf("\x1b[1;31m%s: command not found\x1b[0m\r\n", parts[0]) } }