Compare commits
1 Commits
c83fa3530d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
803b4049a6 |
@@ -37,6 +37,7 @@ func main() {
|
|||||||
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")
|
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)")
|
localHttpsFlag := flag.Bool("local-https", false, "Use HTTPS to connect to local service (implied if port is 443)")
|
||||||
|
slugFlag := flag.String("slug", "", "Request a specific subdomain slug (e.g., myapp)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// Load config
|
// Load config
|
||||||
@@ -58,7 +59,7 @@ func main() {
|
|||||||
serverAddr = "localhost:2222"
|
serverAddr = "localhost:2222"
|
||||||
}
|
}
|
||||||
|
|
||||||
m := tui.InitialModel(*localPort, serverAddr, authToken, *hostHeaderFlag, *localHttpsFlag)
|
m := tui.InitialModel(*localPort, serverAddr, authToken, *hostHeaderFlag, *localHttpsFlag, *slugFlag)
|
||||||
p := tea.NewProgram(m, tea.WithAltScreen())
|
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||||
|
|
||||||
if _, err := p.Run(); err != nil {
|
if _, err := p.Run(); err != nil {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -44,6 +45,13 @@ var manager = &TunnelManager{
|
|||||||
tunnels: make(map[string]*Tunnel),
|
tunnels: make(map[string]*Tunnel),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// requestedSlugs stores slugs requested by clients before tcpip-forward
|
||||||
|
var (
|
||||||
|
requestedSlugs = make(map[*gossh.ServerConn]string)
|
||||||
|
requestedSlugsMu sync.Mutex
|
||||||
|
slugPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$`)
|
||||||
|
)
|
||||||
|
|
||||||
func (tm *TunnelManager) Register(id string, t *Tunnel) {
|
func (tm *TunnelManager) Register(id string, t *Tunnel) {
|
||||||
tm.mu.Lock()
|
tm.mu.Lock()
|
||||||
defer tm.mu.Unlock()
|
defer tm.mu.Unlock()
|
||||||
@@ -305,28 +313,22 @@ func main() {
|
|||||||
// This is where clients say "Please listen on port X"
|
// This is where clients say "Please listen on port X"
|
||||||
sshServer.RequestHandlers = map[string]ssh.RequestHandler{
|
sshServer.RequestHandlers = map[string]ssh.RequestHandler{
|
||||||
"tcpip-forward": func(ctx ssh.Context, srv *ssh.Server, req *gossh.Request) (bool, []byte) {
|
"tcpip-forward": func(ctx ssh.Context, srv *ssh.Server, req *gossh.Request) (bool, []byte) {
|
||||||
// Parse payload
|
conn := ctx.Value(ssh.ContextKeyConn).(*gossh.ServerConn)
|
||||||
// string address to bind (usually empty or 0.0.0.0 or 127.0.0.1)
|
|
||||||
// uint32 port number to bind
|
|
||||||
|
|
||||||
// For Grokway, we ignore the requested port and assign a random subdomain/slug
|
// Check if client requested a specific slug
|
||||||
// Or we use the requested port if valid?
|
requestedSlugsMu.Lock()
|
||||||
// Let's assume we ignore it and generate a slug,
|
slug, hasRequested := requestedSlugs[conn]
|
||||||
// OR we use the port as the "slug" if it's special.
|
if hasRequested {
|
||||||
|
delete(requestedSlugs, conn)
|
||||||
|
}
|
||||||
|
requestedSlugsMu.Unlock()
|
||||||
|
|
||||||
// But wait, the client needs to know what we assigned!
|
if !hasRequested {
|
||||||
// Standard SSH response to tcpip-forward contains the BOUND PORT.
|
slug = generateSlug(8)
|
||||||
// If we return a port, the client knows.
|
}
|
||||||
// If we want to return a URL, standard SSH doesn't have a field for that.
|
|
||||||
// But we can write it to the Session (stdout).
|
|
||||||
|
|
||||||
// Let's accept any request and map it to a random slug.
|
|
||||||
slug := generateSlug(8)
|
|
||||||
|
|
||||||
log.Printf("Client requested forwarding. Assigning slug: %s", slug)
|
log.Printf("Client requested forwarding. Assigning slug: %s", slug)
|
||||||
|
|
||||||
conn := ctx.Value(ssh.ContextKeyConn).(*gossh.ServerConn)
|
|
||||||
|
|
||||||
manager.Register(slug, &Tunnel{
|
manager.Register(slug, &Tunnel{
|
||||||
ID: slug,
|
ID: slug,
|
||||||
LocalPort: 80, // We assume client forwards to port 80 locally? No,
|
LocalPort: 80, // We assume client forwards to port 80 locally? No,
|
||||||
@@ -351,6 +353,24 @@ func main() {
|
|||||||
"cancel-tcpip-forward": func(ctx ssh.Context, srv *ssh.Server, req *gossh.Request) (bool, []byte) {
|
"cancel-tcpip-forward": func(ctx ssh.Context, srv *ssh.Server, req *gossh.Request) (bool, []byte) {
|
||||||
return true, nil
|
return true, nil
|
||||||
},
|
},
|
||||||
|
"grokway-request-slug": func(ctx ssh.Context, srv *ssh.Server, req *gossh.Request) (bool, []byte) {
|
||||||
|
slug := string(req.Payload)
|
||||||
|
if !slugPattern.MatchString(slug) {
|
||||||
|
log.Printf("Invalid slug format requested: %q", slug)
|
||||||
|
return false, []byte("invalid slug format (lowercase alphanumeric and hyphens, 3-32 chars)")
|
||||||
|
}
|
||||||
|
// Check if slug is already in use
|
||||||
|
if _, taken := manager.Get(slug); taken {
|
||||||
|
log.Printf("Slug %q already in use", slug)
|
||||||
|
return false, []byte("slug already in use")
|
||||||
|
}
|
||||||
|
conn := ctx.Value(ssh.ContextKeyConn).(*gossh.ServerConn)
|
||||||
|
requestedSlugsMu.Lock()
|
||||||
|
requestedSlugs[conn] = slug
|
||||||
|
requestedSlugsMu.Unlock()
|
||||||
|
log.Printf("Client requested slug: %s", slug)
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
"grokway-whoami": func(ctx ssh.Context, srv *ssh.Server, req *gossh.Request) (bool, []byte) {
|
"grokway-whoami": func(ctx ssh.Context, srv *ssh.Server, req *gossh.Request) (bool, []byte) {
|
||||||
conn := ctx.Value(ssh.ContextKeyConn).(*gossh.ServerConn)
|
conn := ctx.Value(ssh.ContextKeyConn).(*gossh.ServerConn)
|
||||||
slug, ok := manager.FindSlugByConn(conn)
|
slug, ok := manager.FindSlugByConn(conn)
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ type LogMsg string
|
|||||||
type MetricMsg int64
|
type MetricMsg int64
|
||||||
type ClearCopiedMsg struct{}
|
type ClearCopiedMsg struct{}
|
||||||
|
|
||||||
func InitialModel(localPort, serverAddr, authToken, hostHeader string, localHTTPS bool) Model {
|
func InitialModel(localPort, serverAddr, authToken, hostHeader string, localHTTPS bool, slug string) Model {
|
||||||
c := tunnel.NewClient(serverAddr, localPort, authToken, hostHeader, localHTTPS)
|
c := tunnel.NewClient(serverAddr, localPort, authToken, hostHeader, localHTTPS, slug)
|
||||||
return Model{
|
return Model{
|
||||||
Client: c,
|
Client: c,
|
||||||
LogLines: []string{},
|
LogLines: []string{},
|
||||||
|
|||||||
@@ -21,26 +21,28 @@ func logToFile(msg string) {
|
|||||||
|
|
||||||
// Client handles the SSH connection and forwarding
|
// Client handles the SSH connection and forwarding
|
||||||
type Client struct {
|
type Client struct {
|
||||||
ServerAddr string
|
ServerAddr string
|
||||||
LocalPort string
|
LocalPort string
|
||||||
AuthToken string
|
AuthToken string
|
||||||
HostHeader string // New field for custom Host header
|
HostHeader string // Custom Host header
|
||||||
LocalHTTPS bool // New field: connect to local service with HTTPS
|
LocalHTTPS bool // Connect to local service with HTTPS
|
||||||
SSHClient *ssh.Client
|
Slug string // Requested slug (empty = server assigns random)
|
||||||
Listener net.Listener
|
SSHClient *ssh.Client
|
||||||
Events chan string // Channel to send logs/events to TUI
|
Listener net.Listener
|
||||||
Metrics chan int64 // Channel to send bytes transferred
|
Events chan string // Channel to send logs/events to TUI
|
||||||
PublicURL string // PublicURL is the URL accessible from the internet
|
Metrics chan int64 // Channel to send bytes transferred
|
||||||
|
PublicURL string // PublicURL is the URL accessible from the internet
|
||||||
stopKeepAlive chan struct{} // Signal to stop keepalive goroutine
|
stopKeepAlive chan struct{} // Signal to stop keepalive goroutine
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(serverAddr, localPort, authToken, hostHeader string, localHTTPS bool) *Client {
|
func NewClient(serverAddr, localPort, authToken, hostHeader string, localHTTPS bool, slug string) *Client {
|
||||||
return &Client{
|
return &Client{
|
||||||
ServerAddr: serverAddr,
|
ServerAddr: serverAddr,
|
||||||
LocalPort: localPort,
|
LocalPort: localPort,
|
||||||
AuthToken: authToken,
|
AuthToken: authToken,
|
||||||
HostHeader: hostHeader,
|
HostHeader: hostHeader,
|
||||||
LocalHTTPS: localHTTPS,
|
LocalHTTPS: localHTTPS,
|
||||||
|
Slug: slug,
|
||||||
Events: make(chan string, 10),
|
Events: make(chan string, 10),
|
||||||
Metrics: make(chan int64, 10),
|
Metrics: make(chan int64, 10),
|
||||||
}
|
}
|
||||||
@@ -64,6 +66,20 @@ func (c *Client) connect() error {
|
|||||||
c.SSHClient = client
|
c.SSHClient = client
|
||||||
c.Events <- "SSH Connected!"
|
c.Events <- "SSH Connected!"
|
||||||
|
|
||||||
|
// Request a specific slug if provided
|
||||||
|
if c.Slug != "" {
|
||||||
|
ok, reply, err := client.SendRequest("grokway-request-slug", true, []byte(c.Slug))
|
||||||
|
if err != nil || !ok {
|
||||||
|
reason := string(reply)
|
||||||
|
if reason == "" && err != nil {
|
||||||
|
reason = err.Error()
|
||||||
|
}
|
||||||
|
c.Events <- fmt.Sprintf("Slug %q not available (%s), server will assign one", c.Slug, reason)
|
||||||
|
} else {
|
||||||
|
c.Events <- fmt.Sprintf("Slug %q reserved", c.Slug)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Request remote listening (Reverse Forwarding)
|
// Request remote listening (Reverse Forwarding)
|
||||||
// Bind to 0.0.0.0 on server, random port (0)
|
// Bind to 0.0.0.0 on server, random port (0)
|
||||||
listener, err := client.Listen("tcp", "0.0.0.0:0")
|
listener, err := client.Listen("tcp", "0.0.0.0:0")
|
||||||
|
|||||||
Reference in New Issue
Block a user