feat: Implement initial client application with TUI for SSH tunnel management and monitoring.
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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{},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user