Add client support for HTTPS forwarding and custom Host headers

This commit is contained in:
Jose Luis Montañes Ojados
2026-01-27 03:05:09 +01:00
parent b5f6f42311
commit 310d7e086c
3 changed files with 30 additions and 76 deletions

View File

@@ -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 {

View File

@@ -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{},

View File

@@ -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))