Compare commits
4 Commits
481f8c431d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
736922ca77 | ||
|
|
8e473a5bae | ||
|
|
310d7e086c | ||
|
|
b5f6f42311 |
@@ -35,6 +35,8 @@ func main() {
|
||||
localPort := flag.String("local", "8080", "Local port to expose")
|
||||
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
|
||||
@@ -56,7 +58,7 @@ func main() {
|
||||
serverAddr = "localhost:2222"
|
||||
}
|
||||
|
||||
m := tui.InitialModel(*localPort, serverAddr, authToken)
|
||||
m := tui.InitialModel(*localPort, serverAddr, authToken, *hostHeaderFlag, *localHttpsFlag)
|
||||
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||
|
||||
if _, err := p.Run(); err != nil {
|
||||
|
||||
@@ -76,9 +76,8 @@ func (tm *TunnelManager) FindSlugByConn(conn *gossh.ServerConn) (string, bool) {
|
||||
// HTTP Proxy to forward requests to the SSH tunnel
|
||||
func startHttpProxy(port string) {
|
||||
log.Printf("Starting HTTP Proxy on %s", port)
|
||||
// Serve static files for 404 page
|
||||
|
||||
http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("./cmd/server/static/assets"))))
|
||||
// Serve static files for 404 page (namespaced to avoid collision with user apps)
|
||||
http.Handle("/_gw/", http.StripPrefix("/_gw/", http.FileServer(http.Dir("./cmd/server/static/assets"))))
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract subdomain to identify the tunnel
|
||||
@@ -126,7 +125,7 @@ func startHttpProxy(port string) {
|
||||
// But typically `gliderlabs/ssh` is for allowing the server to be a jump host.
|
||||
// We want to be an HTTP Gateway.
|
||||
|
||||
// 1. Hijack the connection to handle bidirectional traffic (WebSockets)
|
||||
// 1. Hijack the connection to handle bidirectional traffic (WebSockets)
|
||||
hijacker, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
<link
|
||||
rel="stylesheet"
|
||||
id="a3a3_lazy_load-css"
|
||||
href="./assets/a3_lazy_load.min.css"
|
||||
href="/_gw/a3_lazy_load.min.css"
|
||||
type="text/css"
|
||||
media="all"
|
||||
/>
|
||||
<link rel="stylesheet" href="assets/style.css">
|
||||
<link rel="stylesheet" href="/_gw/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<section class="error_404">
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
<!-- Astronaut -->
|
||||
<div class="error_404_astro">
|
||||
<object data="assets/astro.svg"></object>
|
||||
<object data="/_gw/astro.svg"></object>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -68,8 +68,8 @@ type LogMsg string
|
||||
type MetricMsg int64
|
||||
type ClearCopiedMsg struct{}
|
||||
|
||||
func InitialModel(localPort, serverAddr, authToken string) Model {
|
||||
c := tunnel.NewClient(serverAddr, localPort, authToken)
|
||||
func InitialModel(localPort, serverAddr, authToken, hostHeader string, localHTTPS bool) Model {
|
||||
c := tunnel.NewClient(serverAddr, localPort, authToken, hostHeader, localHTTPS)
|
||||
return Model{
|
||||
Client: c,
|
||||
LogLines: []string{},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -23,6 +24,8 @@ type Client struct {
|
||||
ServerAddr string
|
||||
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
|
||||
@@ -30,11 +33,13 @@ type Client struct {
|
||||
PublicURL string // PublicURL is the URL accessible from the internet
|
||||
}
|
||||
|
||||
func NewClient(serverAddr, localPort, authToken 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),
|
||||
}
|
||||
@@ -110,37 +115,104 @@ 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.
|
||||
|
||||
// Create a buffer to read the first few bytes
|
||||
buf := make([]byte, 1024)
|
||||
// Read first chunk to inspect headers
|
||||
buf := make([]byte, 8192) // Increased buffer for headers
|
||||
remoteConn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
|
||||
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))
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// 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")
|
||||
if len(lines) > 0 {
|
||||
firstLine := lines[0]
|
||||
@@ -149,19 +221,14 @@ func (c *Client) handleConnection(remoteConn net.Conn) {
|
||||
method := parts[0]
|
||||
path := parts[1]
|
||||
// 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 {
|
||||
c.Events <- "TCP|||Connection"
|
||||
}
|
||||
}
|
||||
|
||||
// We need to write the bytes we read to the local connection first
|
||||
localConn.Write(buf[:n])
|
||||
|
||||
// Bidirectional copy
|
||||
// Calculate bytes?
|
||||
|
||||
// We can use a custom copy to track metrics
|
||||
go func() {
|
||||
n, _ := io.Copy(remoteConn, localConn)
|
||||
c.Metrics <- n
|
||||
|
||||
Reference in New Issue
Block a user