173 lines
4.3 KiB
Go
173 lines
4.3 KiB
Go
package tunnel
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
|
|
"os"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
func logToFile(msg string) {
|
|
f, _ := os.OpenFile("client_debug.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
defer f.Close()
|
|
f.WriteString(time.Now().Format(time.RFC3339) + " " + msg + "\n")
|
|
}
|
|
|
|
// Client handles the SSH connection and forwarding
|
|
type Client struct {
|
|
ServerAddr string
|
|
LocalPort string
|
|
AuthToken string
|
|
SSHClient *ssh.Client
|
|
Listener net.Listener
|
|
Events chan string // Channel to send logs/events to TUI
|
|
Metrics chan int64 // Channel to send bytes transferred
|
|
PublicURL string // PublicURL is the URL accessible from the internet
|
|
}
|
|
|
|
func NewClient(serverAddr, localPort, authToken string) *Client {
|
|
return &Client{
|
|
ServerAddr: serverAddr,
|
|
LocalPort: localPort,
|
|
AuthToken: authToken,
|
|
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,
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
func (c *Client) handleConnection(remoteConn net.Conn) {
|
|
defer remoteConn.Close()
|
|
|
|
// Dial local service
|
|
localConn, err := net.Dial("tcp", "localhost:"+c.LocalPort)
|
|
if err != nil {
|
|
errMsg := fmt.Sprintf("Failed to dial local: %s", err)
|
|
c.Events <- errMsg
|
|
logToFile(errMsg)
|
|
return
|
|
}
|
|
defer localConn.Close()
|
|
logToFile("Dialed local service successfully")
|
|
|
|
// 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)
|
|
n, err := remoteConn.Read(buf)
|
|
if err != nil {
|
|
logToFile(fmt.Sprintf("Error reading from remote: %v", err))
|
|
return
|
|
}
|
|
|
|
payload := string(buf[:n])
|
|
|
|
// Try to parse rudimentary HTTP
|
|
// Format: METHOD PATH PROTOCOL
|
|
lines := strings.Split(payload, "\n")
|
|
if len(lines) > 0 {
|
|
firstLine := lines[0]
|
|
parts := strings.Fields(firstLine)
|
|
if len(parts) >= 3 {
|
|
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
|
|
} 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
|
|
}()
|
|
|
|
n2, _ := io.Copy(localConn, remoteConn)
|
|
c.Metrics <- n2
|
|
}
|