feat: Implement initial client application with TUI for SSH tunnel management and monitoring.

This commit is contained in:
Jose Luis Montañes Ojados
2026-01-27 03:03:21 +01:00
parent 481f8c431d
commit b5f6f42311
3 changed files with 67 additions and 14 deletions

View File

@@ -35,6 +35,7 @@ func main() {
localPort := flag.String("local", "8080", "Local port to expose") localPort := flag.String("local", "8080", "Local port to expose")
serverAddrFlag := flag.String("server", "", "Grokway server address") serverAddrFlag := flag.String("server", "", "Grokway server address")
tokenFlag := flag.String("token", "", "Authentication token (overrides config)") tokenFlag := flag.String("token", "", "Authentication token (overrides config)")
hostHeaderFlag := flag.String("host-header", "", "Custom Host header to send to local service")
flag.Parse() flag.Parse()
// Load config // Load config
@@ -56,7 +57,7 @@ func main() {
serverAddr = "localhost:2222" serverAddr = "localhost:2222"
} }
m := tui.InitialModel(*localPort, serverAddr, authToken) m := tui.InitialModel(*localPort, serverAddr, authToken, *hostHeaderFlag)
p := tea.NewProgram(m, tea.WithAltScreen()) p := tea.NewProgram(m, tea.WithAltScreen())
if _, err := p.Run(); err != nil { if _, err := p.Run(); err != nil {

View File

@@ -68,8 +68,8 @@ type LogMsg string
type MetricMsg int64 type MetricMsg int64
type ClearCopiedMsg struct{} type ClearCopiedMsg struct{}
func InitialModel(localPort, serverAddr, authToken string) Model { func InitialModel(localPort, serverAddr, authToken, hostHeader string) Model {
c := tunnel.NewClient(serverAddr, localPort, authToken) c := tunnel.NewClient(serverAddr, localPort, authToken, hostHeader)
return Model{ return Model{
Client: c, Client: c,
LogLines: []string{}, LogLines: []string{},

View File

@@ -23,6 +23,7 @@ type Client struct {
ServerAddr string ServerAddr string
LocalPort string LocalPort string
AuthToken string AuthToken string
HostHeader string // New field for custom Host header
SSHClient *ssh.Client SSHClient *ssh.Client
Listener net.Listener Listener net.Listener
Events chan string // Channel to send logs/events to TUI Events chan string // Channel to send logs/events to TUI
@@ -30,11 +31,12 @@ type Client struct {
PublicURL string // PublicURL is the URL accessible from the internet PublicURL string // PublicURL is the URL accessible from the internet
} }
func NewClient(serverAddr, localPort, authToken string) *Client { func NewClient(serverAddr, localPort, authToken, hostHeader string) *Client {
return &Client{ return &Client{
ServerAddr: serverAddr, ServerAddr: serverAddr,
LocalPort: localPort, LocalPort: localPort,
AuthToken: authToken, AuthToken: authToken,
HostHeader: hostHeader,
Events: make(chan string, 10), Events: make(chan string, 10),
Metrics: make(chan int64, 10), Metrics: make(chan int64, 10),
} }
@@ -129,18 +131,73 @@ func (c *Client) handleConnection(remoteConn net.Conn) {
// or use a bufio reader if we weren't doing a raw Copy. // or use a bufio reader if we weren't doing a raw Copy.
// But io.Copy needs a reader. // But io.Copy needs a reader.
// Create a buffer to read the first few bytes // Read first chunk to inspect headers
buf := make([]byte, 1024) buf := make([]byte, 8192) // Increased buffer for headers
remoteConn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
n, err := remoteConn.Read(buf) n, err := remoteConn.Read(buf)
if err != nil { remoteConn.SetReadDeadline(time.Time{}) // Reset deadline
if err != nil && err != io.EOF && !os.IsTimeout(err) {
logToFile(fmt.Sprintf("Error reading from remote: %v", err)) logToFile(fmt.Sprintf("Error reading from remote: %v", err))
return return
} }
if n == 0 {
return
}
payload := string(buf[:n]) payload := string(buf[:n])
// Try to parse rudimentary HTTP // If it looks like HTTP, rewrite Host and add Proto headers
if strings.Contains(payload, "HTTP/") {
lines := strings.Split(payload, "\r\n")
var newLines []string
for _, line := range lines {
if strings.HasPrefix(strings.ToLower(line), "host:") {
if c.HostHeader != "" {
newLines = append(newLines, "Host: "+c.HostHeader)
} else {
// Default to localhost handling: keep original or set to localhost?
// Usually localhost:port is safest if user didn't specify.
newLines = append(newLines, "Host: localhost:"+c.LocalPort)
}
continue
}
newLines = append(newLines, line)
}
// Insert X-Forwarded-Proto if missing (prevents redirects loop)
if !strings.Contains(strings.ToLower(payload), "x-forwarded-proto:") {
// Find end of headers (empty line)
for i, line := range newLines {
if line == "" { // End of headers
// Insert before the empty line
finalHeaders := append(newLines[:i], "X-Forwarded-Proto: https")
finalHeaders = append(finalHeaders, newLines[i:]...)
newLines = finalHeaders
break
}
}
}
modifiedPayload := strings.Join(newLines, "\r\n")
// Log for debug
logToFile("Rewritten Headers:\n" + modifiedPayload)
// Send modified headers
localConn.Write([]byte(modifiedPayload))
} else {
// Not HTTP or couldn't parse, just forward as comes
localConn.Write(buf[:n])
}
// Try to parse rudimentary HTTP for TUI logs
// Format: METHOD PATH PROTOCOL // Format: METHOD PATH PROTOCOL
// We use the payload we just processed (or the original if not modified? No, logs should show original request usually,
// but here we have the payload string variable already.)
lines := strings.Split(payload, "\n") lines := strings.Split(payload, "\n")
if len(lines) > 0 { if len(lines) > 0 {
firstLine := lines[0] firstLine := lines[0]
@@ -149,19 +206,14 @@ func (c *Client) handleConnection(remoteConn net.Conn) {
method := parts[0] method := parts[0]
path := parts[1] path := parts[1]
// Send structured log // Send structured log
c.Events <- fmt.Sprintf("HTTP|%s|%s|%d", method, path, 200) // Status is fake for now, acts as connection open c.Events <- fmt.Sprintf("HTTP|%s|%s|%d", method, path, 200)
} else { } else {
c.Events <- "TCP|||Connection" c.Events <- "TCP|||Connection"
} }
} }
// We need to write the bytes we read to the local connection first
localConn.Write(buf[:n])
// Bidirectional copy // Bidirectional copy
// Calculate bytes? // Calculate bytes?
// We can use a custom copy to track metrics
go func() { go func() {
n, _ := io.Copy(remoteConn, localConn) n, _ := io.Copy(remoteConn, localConn)
c.Metrics <- n c.Metrics <- n