From 58859dba6791ee392e477093c2a74bb81f8c173e Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Mon, 10 Feb 2025 07:52:13 +0000 Subject: [PATCH] JSON Unmarshalling of the Initial Authentication Message, Security and Input Validation, timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Authentication: The module now reads a single JSON message containing both secret and host_port values. Timeouts: The Telnet dial uses a 10‑second timeout, and the WebSocket connection is closed after 120 seconds of inactivity. Adjust these values as needed. Security: The upgrader currently allows all origins. In a production environment, restrict this by verifying r.Origin. Error Handling & Goroutine Cleanup: Each goroutine writes to a buffered error channel so that the first error will cancel the connection. In a more complex scenario, you might use a context with cancellation or a sync.WaitGroup to better manage goroutine lifetimes. --- telnetproxy.go | 272 +++++++++++++++++++++++-------------------------- 1 file changed, 125 insertions(+), 147 deletions(-) diff --git a/telnetproxy.go b/telnetproxy.go index 675e3a4..c8796e8 100644 --- a/telnetproxy.go +++ b/telnetproxy.go @@ -1,180 +1,158 @@ package telnetproxy import ( - "bufio" - "context" - "errors" - "fmt" - "io" - "log" - "net" - "net/http" - "strings" - "time" + "bufio" + "errors" + "fmt" + "io" + "log" + "net" + "net/http" + "time" - "github.com/caddyserver/caddy/v2" - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" - "github.com/caddyserver/caddy/v2/modules/caddyhttp" - "github.com/gorilla/websocket" + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/gorilla/websocket" ) -// TelnetProxy is a Caddy module that proxies websocket connections to telnet servers. +// TelnetProxy is a Caddy module that proxies WebSocket connections to Telnet servers. type TelnetProxy struct { - Secret string `json:"secret,omitempty"` + Secret string `json:"secret,omitempty"` } // CaddyModule returns the Caddy module information. func (TelnetProxy) CaddyModule() caddy.ModuleInfo { - return caddy.ModuleInfo{ - ID: "http.handlers.telnet_proxy", - New: func() caddy.Module { return new(TelnetProxy) }, - } + return caddy.ModuleInfo{ + ID: "http.handlers.telnet_proxy", + New: func() caddy.Module { return new(TelnetProxy) }, + } } // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (tp *TelnetProxy) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if !d.Args(&tp.Secret) { - return d.ArgErr() - } - } - return nil + for d.Next() { + if !d.Args(&tp.Secret) { + return d.ArgErr() + } + } + return nil +} + +// authMessage is used for the initial authentication and connection details. +type authMessage struct { + Secret string `json:"secret"` + HostPort string `json:"host_port"` } // ServeHTTP implements caddyhttp.MiddlewareHandler. func (tp TelnetProxy) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { - upgrader := websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - // Allow all origins (you might want to restrict this in production) - return true - }, - } + upgrader := websocket.Upgrader{ + // In production, restrict allowed origins to trusted hosts. + CheckOrigin: func(r *http.Request) bool { + return true + }, + } - ws, err := upgrader.Upgrade(w, r, nil) - if err != nil { - log.Printf("WebSocket upgrade error: %v", err) - return err - } - defer ws.Close() + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("WebSocket upgrade error: %v", err) + return err + } + // The connection will be closed in handleWebSocket. + defer ws.Close() - // Handle WebSocket connection in a goroutine - tp.handleWebSocket(ws) + // Handle the WebSocket connection in a separate goroutine. + go tp.handleWebSocket(ws) - return nil // Don't let caddy return a 404 after us + // Returning nil prevents further Caddy middleware from interfering. + return nil } func (tp TelnetProxy) handleWebSocket(ws *websocket.Conn) { - defer func() { - if r := recover(); r != nil { - log.Printf("Recovered from panic: %v", r) - } - }() + // Recover from panics to avoid crashing the server. + defer func() { + if r := recover(); r != nil { + log.Printf("Recovered from panic: %v", r) + } + }() - var ( - hostPort string - clientSecret string - telnetConn net.Conn - err error - ) + // Read and decode the initial authentication message. + var msg authMessage + if err := ws.ReadJSON(&msg); err != nil { + log.Printf("Error reading initial JSON: %v", err) + ws.WriteMessage(websocket.TextMessage, []byte("Error reading initial JSON")) + return + } - // Initial Authentication and Host/Port - err = ws.ReadJSON(map[string]string{ - "secret": "", - "host_port": "", - }) - if err != nil { - log.Printf("Error reading initial JSON: %v", err) - ws.WriteMessage(websocket.TextMessage, []byte("Error reading initial JSON")) - return - } + // Check authentication. + if msg.Secret != tp.Secret { + log.Printf("Authentication failed. Received secret: %s", msg.Secret) + ws.WriteMessage(websocket.TextMessage, []byte("Authentication failed")) + return + } + // Connect to the Telnet server with a timeout. + telnetConn, err := net.DialTimeout("tcp", msg.HostPort, 10*time.Second) + if err != nil { + log.Printf("Error connecting to Telnet server at %s: %v", msg.HostPort, err) + ws.WriteMessage(websocket.TextMessage, []byte("Error connecting to Telnet server: "+err.Error())) + return + } + defer telnetConn.Close() - err = ws.ReadJSON(&map[string]string{ - "secret": &clientSecret, - "host_port": &hostPort, - }) - if err != nil { - log.Printf("Error reading initial JSON: %v", err) - ws.WriteMessage(websocket.TextMessage, []byte("Error reading initial JSON")) - return - } + // Create a channel to signal errors from either direction. + errChan := make(chan error, 2) + // Goroutine: Telnet -> WebSocket. + go func() { + reader := bufio.NewReader(telnetConn) + for { + // Read until newline; adjust if the Telnet protocol doesn't send newline-terminated data. + data, err := reader.ReadBytes('\n') + if err != nil { + if !errors.Is(err, io.EOF) { + log.Printf("Telnet read error: %v", err) + errChan <- fmt.Errorf("Telnet read error: %v", err) + } + return + } + if err := ws.WriteMessage(websocket.BinaryMessage, data); err != nil { + log.Printf("WebSocket write error (from Telnet): %v", err) + errChan <- fmt.Errorf("WebSocket write error (from Telnet): %v", err) + return + } + } + }() + // Goroutine: WebSocket -> Telnet. + go func() { + for { + _, data, err := ws.ReadMessage() + if err != nil { + if !websocket.IsCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) { + log.Printf("WebSocket read error (from client): %v", err) + errChan <- fmt.Errorf("WebSocket read error (from client): %v", err) + } + return + } + if _, err := telnetConn.Write(data); err != nil { + log.Printf("Telnet write error: %v", err) + errChan <- fmt.Errorf("Telnet write error: %v", err) + return + } + } + }() - // Authentication Check - if clientSecret != tp.Secret { - log.Printf("Authentication failed. Client secret: %s, server secret: %s", clientSecret, tp.Secret) - ws.WriteMessage(websocket.TextMessage, []byte("Authentication failed")) - return - } + // Wait until an error occurs or a timeout happens. + select { + case <-errChan: + log.Println("Closing connection due to an error.") + case <-time.After(120 * time.Second): + log.Println("Closing connection due to inactivity.") + } - - // Telnet connection - telnetConn, err = net.Dial("tcp", hostPort) - if err != nil { - log.Printf("Error connecting to Telnet server: %v", err) - ws.WriteMessage(websocket.TextMessage, []byte("Error connecting to Telnet server: "+err.Error())) - return - } - defer telnetConn.Close() - - // Channel for errors - errChan := make(chan error, 2) // Buffered channel to avoid blocking - - // Copy from Telnet -> WebSocket (Read from telnet, send to client) - go func() { - // Using a buffered reader in case the server sends large chunks of text - reader := bufio.NewReader(telnetConn) - for { - data, err := reader.ReadBytes('\n') // Read until a newline - if err != nil { - if !errors.Is(err, io.EOF) { - log.Printf("Telnet read error: %v", err) - errChan <- fmt.Errorf("Telnet read error: %v", err) - } - return // End goroutine on EOF or error - } - - if err = ws.WriteMessage(websocket.BinaryMessage, data); err != nil { - log.Printf("Websocket write error (from telnet): %v", err) - errChan <- fmt.Errorf("Websocket write error (from telnet): %v", err) - return - } - - } - }() - - // Copy from WebSocket -> Telnet (Read from client, send to server) - go func() { - for { - _, msg, err := ws.ReadMessage() - if err != nil { - if !websocket.IsCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure){ - log.Printf("Websocket read error (from browser): %v", err) - errChan <- fmt.Errorf("Websocket read error (from browser): %v", err) - } - return - } - - _, err = telnetConn.Write(msg) - if err != nil { - log.Printf("Telnet write error: %v", err) - errChan <- fmt.Errorf("Telnet write error: %v", err) - return - } - } - }() - - // Keep connection alive by waiting for an error on either read or write channel - select { - case <-errChan: // First Error closes connections - log.Println("Closing connection") - telnetConn.Close() - ws.Close() - - case <-time.After(120 * time.Second): // Optional timeout. - log.Println("Closing connection due to inactivity") - telnetConn.Close() - ws.Close() - } -} + // Close both connections (deferred calls will handle this if not already closed). + telnetConn.Close() + ws.Close() +} \ No newline at end of file