diff --git a/cmd/client/main.go b/cmd/client/main.go index e621247..23554d0 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -36,6 +36,7 @@ func main() { serverAddrFlag := flag.String("server", "", "Grokway server address") tokenFlag := flag.String("token", "", "Authentication token (overrides config)") hostHeaderFlag := flag.String("host-header", "", "Custom Host header to send to local service") + localHttpsFlag := flag.Bool("local-https", false, "Use HTTPS to connect to local service (implied if port is 443)") flag.Parse() // Load config @@ -57,7 +58,7 @@ func main() { serverAddr = "localhost:2222" } - m := tui.InitialModel(*localPort, serverAddr, authToken, *hostHeaderFlag) + m := tui.InitialModel(*localPort, serverAddr, authToken, *hostHeaderFlag, *localHttpsFlag) p := tea.NewProgram(m, tea.WithAltScreen()) if _, err := p.Run(); err != nil { diff --git a/internal/tui/model.go b/internal/tui/model.go index c18ff35..59e67ba 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -68,8 +68,8 @@ type LogMsg string type MetricMsg int64 type ClearCopiedMsg struct{} -func InitialModel(localPort, serverAddr, authToken, hostHeader string) Model { - c := tunnel.NewClient(serverAddr, localPort, authToken, hostHeader) +func InitialModel(localPort, serverAddr, authToken, hostHeader string, localHTTPS bool) Model { + c := tunnel.NewClient(serverAddr, localPort, authToken, hostHeader, localHTTPS) return Model{ Client: c, LogLines: []string{}, diff --git a/internal/tunnel/client.go b/internal/tunnel/client.go index 4b846a5..f80ea5c 100644 --- a/internal/tunnel/client.go +++ b/internal/tunnel/client.go @@ -1,6 +1,7 @@ package tunnel import ( + "crypto/tls" "fmt" "io" "net" @@ -24,6 +25,7 @@ type Client struct { LocalPort string AuthToken string HostHeader string // New field for custom Host header + LocalHTTPS bool // New field: connect to local service with HTTPS SSHClient *ssh.Client Listener net.Listener Events chan string // Channel to send logs/events to TUI @@ -31,106 +33,57 @@ type Client struct { PublicURL string // PublicURL is the URL accessible from the internet } -func NewClient(serverAddr, localPort, authToken, hostHeader string) *Client { +func NewClient(serverAddr, localPort, authToken, hostHeader string, localHTTPS bool) *Client { return &Client{ ServerAddr: serverAddr, LocalPort: localPort, AuthToken: authToken, HostHeader: hostHeader, + LocalHTTPS: localHTTPS, Events: make(chan string, 10), Metrics: make(chan int64, 10), } } -func (c *Client) Start() error { - config := &ssh.ClientConfig{ - User: "grokway", - Auth: []ssh.AuthMethod{ - ssh.Password(c.AuthToken), - }, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), // Dev only - Timeout: 5 * time.Second, - } +// ... Start() code ... - c.Events <- fmt.Sprintf("Connecting to %s...", c.ServerAddr) - client, err := ssh.Dial("tcp", c.ServerAddr, config) - if err != nil { - return err - } - c.SSHClient = client - c.Events <- "SSH Connected!" - - // Request remote listening (Reverse Forwarding) - // Bind to 0.0.0.0 on server, random port (0) - listener, err := client.Listen("tcp", "0.0.0.0:0") - if err != nil { - return fmt.Errorf("failed to request port forwarding: %w", err) - } - c.Listener = listener - - // Query server for assigned slug - ok, slugBytes, err := client.SendRequest("grokway-whoami", true, nil) - slug := "test-slug" // Fallback - if err == nil && ok { - slug = string(slugBytes) - c.Events <- fmt.Sprintf("Server assigned domain: %s", slug) - } else { - c.Events <- "Failed to query domain from server, using fallback" - } - - hostname := "localhost" // This should match what the server is running on actually - - // Assuming HTTP proxy is on port 8080 of the same host as SSH server (but different port) - // We extract host from c.ServerAddr - host, _, _ := net.SplitHostPort(c.ServerAddr) - if host == "" { - host = hostname - } - - c.PublicURL = fmt.Sprintf("https://%s.%s", slug, host) - c.Events <- fmt.Sprintf("Tunnel established! Public URL: %s", c.PublicURL) - - go c.acceptLoop() - - return nil -} - -func (c *Client) acceptLoop() { - for { - remoteConn, err := c.Listener.Accept() - if err != nil { - c.Events <- fmt.Sprintf("Accept error: %s", err) - break - } - - c.Events <- "New Request received" - go c.handleConnection(remoteConn) - } -} +// ... acceptLoop() code ... func (c *Client) handleConnection(remoteConn net.Conn) { defer remoteConn.Close() // Dial local service - localConn, err := net.Dial("tcp", "localhost:"+c.LocalPort) + var localConn net.Conn + var err error + + // Check if we should use TLS (explicit flag or port 443) + // You might want to strip "443" from "localhost:443" if localPort includes hostname, but user input is just port usually? + // User input is --local :443 or 443. + useTLS := c.LocalHTTPS + if c.LocalPort == "443" || strings.HasSuffix(c.LocalPort, ":443") { + useTLS = true + } + + if useTLS { + conf := &tls.Config{InsecureSkipVerify: true} + localConn, err = tls.Dial("tcp", "localhost:"+c.LocalPort, conf) + } else { + localConn, err = net.Dial("tcp", "localhost:"+c.LocalPort) + } + if err != nil { - errMsg := fmt.Sprintf("Failed to dial local: %s", err) + errMsg := fmt.Sprintf("Failed to dial local (TLS=%v): %s", useTLS, err) c.Events <- errMsg logToFile(errMsg) return } defer localConn.Close() - logToFile("Dialed local service successfully") + logToFile(fmt.Sprintf("Dialed local service successfully (TLS=%v)", useTLS)) // We need to peek at the connection to see if it's HTTP // Wrap the connection to peek without consuming logToFile("Handling new connection") - // Check if we can peek - // Since net.Conn doesn't support Peek, we read specific bytes and reconstruct - // or use a bufio reader if we weren't doing a raw Copy. - // But io.Copy needs a reader. - // Read first chunk to inspect headers buf := make([]byte, 8192) // Increased buffer for headers remoteConn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))