mirror of
				https://github.com/veops/oneterm.git
				synced 2025-10-31 10:56:29 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			382 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			382 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package protocols
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"net"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/gin-gonic/gin"
 | |
| 	"go.uber.org/zap"
 | |
| 
 | |
| 	"github.com/veops/oneterm/internal/model"
 | |
| 	gsession "github.com/veops/oneterm/internal/session"
 | |
| 	"github.com/veops/oneterm/internal/tunneling"
 | |
| 	"github.com/veops/oneterm/pkg/logger"
 | |
| )
 | |
| 
 | |
| // Telnet protocol constants
 | |
| const (
 | |
| 	IAC  = byte(255) // Interpret As Command
 | |
| 	WILL = byte(251)
 | |
| 	WONT = byte(252)
 | |
| 	DO   = byte(253)
 | |
| 	DONT = byte(254)
 | |
| 	SB   = byte(250) // Sub-negotiation Begin
 | |
| 	SE   = byte(240) // Sub-negotiation End
 | |
| 	GA   = byte(249) // Go Ahead
 | |
| 
 | |
| 	// Telnet options
 | |
| 	OPT_ECHO           = byte(1)  // Echo
 | |
| 	OPT_SUPPRESS_GA    = byte(3)  // Suppress Go Ahead
 | |
| 	OPT_TERMINAL_TYPE  = byte(24) // Terminal Type
 | |
| 	OPT_NAWS           = byte(31) // Negotiate About Window Size
 | |
| 	OPT_TERMINAL_SPEED = byte(32) // Terminal Speed
 | |
| 	OPT_LINEMODE       = byte(34) // Linemode
 | |
| 	OPT_NEW_ENVIRON    = byte(39) // New Environment
 | |
| )
 | |
| 
 | |
| // ConnectTelnet establishes a connection to a Telnet server and handles the session
 | |
| // It performs authentication, sets up the environment, and manages data flow
 | |
| // between the client and server until the session ends.
 | |
| func ConnectTelnet(ctx *gin.Context, sess *gsession.Session, asset *model.Asset, account *model.Account, gateway *model.Gateway) (err error) {
 | |
| 	chs := sess.Chans
 | |
| 	defer func() {
 | |
| 		if err != nil {
 | |
| 			logger.L().Error("telnet connection error", zap.Error(err))
 | |
| 			chs.ErrChan <- err
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	logger.L().Info("starting telnet connection",
 | |
| 		zap.String("sessionId", sess.SessionId),
 | |
| 		zap.String("asset", asset.Name),
 | |
| 		zap.String("ip", asset.Ip))
 | |
| 
 | |
| 	// Establish connection through tunneling
 | |
| 	ip, port, err := tunneling.Proxy(false, sess.SessionId, "telnet", asset, gateway)
 | |
| 	if err != nil {
 | |
| 		logger.L().Error("telnet tunneling failed", zap.Error(err))
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Connect to the telnet server
 | |
| 	conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, port), 10*time.Second)
 | |
| 	if err != nil {
 | |
| 		logger.L().Error("telnet dial failed", zap.Error(err))
 | |
| 		return
 | |
| 	}
 | |
| 	defer conn.Close()
 | |
| 
 | |
| 	// Setup authentication control mechanisms
 | |
| 	authDone := make(chan bool, 1)
 | |
| 	authErr := make(chan error, 1)
 | |
| 	var prompt strings.Builder
 | |
| 	var promptMutex sync.Mutex
 | |
| 	loginSent := false
 | |
| 	passwordSent := false
 | |
| 	terminalTypeSent := false
 | |
| 
 | |
| 	// Authentication handler goroutine
 | |
| 	// Monitors server responses for login/password prompts and responds accordingly
 | |
| 	go func() {
 | |
| 		timeoutChan := time.After(5 * time.Second)
 | |
| 
 | |
| 		for {
 | |
| 			select {
 | |
| 			case <-timeoutChan:
 | |
| 				authDone <- true
 | |
| 				return
 | |
| 			default:
 | |
| 				buf := make([]byte, 1024)
 | |
| 				conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
 | |
| 				n, err := conn.Read(buf)
 | |
| 				conn.SetReadDeadline(time.Time{})
 | |
| 
 | |
| 				if err != nil {
 | |
| 					if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
 | |
| 						continue
 | |
| 					}
 | |
| 
 | |
| 					if err == io.EOF {
 | |
| 						authErr <- fmt.Errorf("connection closed during authentication")
 | |
| 						return
 | |
| 					}
 | |
| 
 | |
| 					logger.L().Error("read error during authentication", zap.Error(err))
 | |
| 					authErr <- err
 | |
| 					return
 | |
| 				}
 | |
| 
 | |
| 				if n > 0 {
 | |
| 					// Process telnet protocol commands and extract actual data
 | |
| 					processed := processTelnetData(buf[:n], conn)
 | |
| 					if len(processed) > 0 {
 | |
| 						chs.OutChan <- processed
 | |
| 
 | |
| 						// Update prompt buffer to detect login/password prompts
 | |
| 						promptMutex.Lock()
 | |
| 						prompt.Write(processed)
 | |
| 						if prompt.Len() > 200 {
 | |
| 							promptStr := prompt.String()
 | |
| 							prompt.Reset()
 | |
| 							prompt.WriteString(promptStr[len(promptStr)-200:])
 | |
| 						}
 | |
| 						promptStr := strings.ToLower(prompt.String())
 | |
| 						promptMutex.Unlock()
 | |
| 
 | |
| 						// Check for username prompt
 | |
| 						if !loginSent && (strings.Contains(promptStr, "login") ||
 | |
| 							strings.Contains(promptStr, "username") ||
 | |
| 							strings.Contains(promptStr, "account")) {
 | |
| 
 | |
| 							time.Sleep(300 * time.Millisecond)
 | |
| 
 | |
| 							_, err := conn.Write([]byte(account.Account + "\r\n"))
 | |
| 							if err != nil {
 | |
| 								logger.L().Error("send username failed", zap.Error(err))
 | |
| 								authErr <- err
 | |
| 								return
 | |
| 							}
 | |
| 
 | |
| 							loginSent = true
 | |
| 
 | |
| 							promptMutex.Lock()
 | |
| 							prompt.Reset()
 | |
| 							promptMutex.Unlock()
 | |
| 						}
 | |
| 
 | |
| 						// Check for password prompt
 | |
| 						if loginSent && !passwordSent && (strings.Contains(promptStr, "password") ||
 | |
| 							strings.Contains(promptStr, "pass:")) {
 | |
| 
 | |
| 							time.Sleep(300 * time.Millisecond)
 | |
| 
 | |
| 							_, err := conn.Write([]byte(account.Password + "\r\n"))
 | |
| 							if err != nil {
 | |
| 								logger.L().Error("send password failed", zap.Error(err))
 | |
| 								authErr <- err
 | |
| 								return
 | |
| 							}
 | |
| 
 | |
| 							passwordSent = true
 | |
| 
 | |
| 							// Give server time to process login before moving on
 | |
| 							go func() {
 | |
| 								time.Sleep(1 * time.Second)
 | |
| 								authDone <- true
 | |
| 							}()
 | |
| 						}
 | |
| 
 | |
| 						// Detect successful login by checking for command prompt characters
 | |
| 						if (loginSent && passwordSent) ||
 | |
| 							strings.Contains(promptStr, "$") ||
 | |
| 							strings.Contains(promptStr, "#") ||
 | |
| 							strings.Contains(promptStr, ">") {
 | |
| 
 | |
| 							if !terminalTypeSent {
 | |
| 								setTerminalType(conn)
 | |
| 								terminalTypeSent = true
 | |
| 							}
 | |
| 
 | |
| 							authDone <- true
 | |
| 							return
 | |
| 						}
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	// Wait for authentication to complete or timeout
 | |
| 	select {
 | |
| 	case <-authDone:
 | |
| 		setupEnvironment(conn)
 | |
| 	case err := <-authErr:
 | |
| 		logger.L().Error("telnet authentication failed", zap.Error(err))
 | |
| 		return err
 | |
| 	case <-time.After(10 * time.Second):
 | |
| 		// Fallback: proceed even if authentication times out
 | |
| 		setupEnvironment(conn)
 | |
| 	}
 | |
| 
 | |
| 	// Data flow from client to server
 | |
| 	// Reads from input pipe and writes to telnet connection
 | |
| 	sess.G.Go(func() error {
 | |
| 		buf := make([]byte, 1024)
 | |
| 		for {
 | |
| 			select {
 | |
| 			case <-sess.Gctx.Done():
 | |
| 				return nil
 | |
| 			default:
 | |
| 				n, err := chs.Rin.Read(buf)
 | |
| 				if err != nil {
 | |
| 					if err == io.EOF {
 | |
| 						continue
 | |
| 					}
 | |
| 					if err.Error() == "io: read/write on closed pipe" {
 | |
| 						return nil // Normal exit condition, not an error
 | |
| 					}
 | |
| 					logger.L().Error("read from input pipe failed", zap.Error(err))
 | |
| 					return err
 | |
| 				}
 | |
| 
 | |
| 				if n > 0 {
 | |
| 					_, err = conn.Write(buf[:n])
 | |
| 					if err != nil {
 | |
| 						logger.L().Error("write to telnet failed", zap.Error(err))
 | |
| 						return err
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	// Data flow from server to client
 | |
| 	// Reads from telnet connection, processes telnet protocol, and sends to output channel
 | |
| 	sess.G.Go(func() error {
 | |
| 		buf := make([]byte, 8192)
 | |
| 		for {
 | |
| 			select {
 | |
| 			case <-sess.Gctx.Done():
 | |
| 				return nil
 | |
| 			default:
 | |
| 				// Use deadline to avoid blocking indefinitely
 | |
| 				conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
 | |
| 				n, err := conn.Read(buf)
 | |
| 				conn.SetReadDeadline(time.Time{})
 | |
| 
 | |
| 				if err != nil {
 | |
| 					if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
 | |
| 						continue
 | |
| 					}
 | |
| 					if err == io.EOF {
 | |
| 						logger.L().Info("telnet connection closed by server")
 | |
| 						return fmt.Errorf("telnet connection closed")
 | |
| 					}
 | |
| 					if strings.Contains(err.Error(), "use of closed network connection") {
 | |
| 						return nil // Normal connection close, not an error
 | |
| 					}
 | |
| 					logger.L().Error("read from telnet failed", zap.Error(err))
 | |
| 					return err
 | |
| 				}
 | |
| 
 | |
| 				if n > 0 {
 | |
| 					data := processTelnetData(buf[:n], conn)
 | |
| 					if len(data) > 0 {
 | |
| 						chs.OutChan <- data
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	// Signal successful connection
 | |
| 	chs.ErrChan <- nil
 | |
| 	// Wait for all goroutines to complete
 | |
| 	err = sess.G.Wait()
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // setTerminalType configures the terminal type and dimensions
 | |
| // This helps ensure proper display of text and ANSI sequences
 | |
| func setTerminalType(conn net.Conn) {
 | |
| 	_, err := conn.Write([]byte("export TERM=xterm-256color\r\n"))
 | |
| 	if err != nil {
 | |
| 		logger.L().Error("failed to set TERM environment variable", zap.Error(err))
 | |
| 	}
 | |
| 
 | |
| 	_, err = conn.Write([]byte("export LINES=24 COLUMNS=80\r\n"))
 | |
| 	if err != nil {
 | |
| 		logger.L().Error("failed to set terminal dimensions", zap.Error(err))
 | |
| 	}
 | |
| 
 | |
| 	_, err = conn.Write([]byte("stty rows 24 columns 80\r\n"))
 | |
| 	if err != nil {
 | |
| 		logger.L().Error("failed to set stty configuration", zap.Error(err))
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // setupEnvironment configures the shell environment for optimal telnet operation
 | |
| // Disables features that could interfere with terminal display or functionality
 | |
| func setupEnvironment(conn net.Conn) {
 | |
| 	time.Sleep(500 * time.Millisecond)
 | |
| 	setTerminalType(conn)
 | |
| 
 | |
| 	_, err := conn.Write([]byte("set +o histappend 2>/dev/null || true\r\n"))
 | |
| 	if err != nil {
 | |
| 		logger.L().Error("failed to configure shell history", zap.Error(err))
 | |
| 	}
 | |
| 
 | |
| 	_, err = conn.Write([]byte("unalias -a 2>/dev/null || true\r\n"))
 | |
| 	if err != nil {
 | |
| 		logger.L().Error("failed to unalias commands", zap.Error(err))
 | |
| 	}
 | |
| 
 | |
| 	_, err = conn.Write([]byte("clear 2>/dev/null || echo -e '\\033c'\r\n"))
 | |
| 	if err != nil {
 | |
| 		logger.L().Error("failed to clear screen", zap.Error(err))
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // processTelnetData handles telnet protocol control sequences
 | |
| // It extracts actual data from the stream by filtering out protocol commands
 | |
| // and responding to negotiation requests appropriately
 | |
| func processTelnetData(data []byte, conn net.Conn) []byte {
 | |
| 	if len(data) == 0 {
 | |
| 		return data
 | |
| 	}
 | |
| 
 | |
| 	processed := make([]byte, 0, len(data))
 | |
| 	for i := 0; i < len(data); i++ {
 | |
| 		if data[i] == IAC && i+1 < len(data) {
 | |
| 			i++ // Skip IAC byte
 | |
| 			if i >= len(data) {
 | |
| 				break
 | |
| 			}
 | |
| 
 | |
| 			cmd := data[i]
 | |
| 
 | |
| 			if cmd >= WILL && cmd <= DONT && i+1 < len(data) {
 | |
| 				// Handle negotiation commands (WILL/WONT/DO/DONT)
 | |
| 				opt := data[i+1]
 | |
| 
 | |
| 				if cmd == WILL || cmd == DO {
 | |
| 					// Respond to capability negotiations
 | |
| 					var response []byte
 | |
| 					if cmd == WILL {
 | |
| 						response = []byte{IAC, DONT, opt} // Reject server's offer
 | |
| 					} else {
 | |
| 						response = []byte{IAC, WONT, opt} // Reject server's request
 | |
| 					}
 | |
| 
 | |
| 					if _, err := conn.Write(response); err != nil {
 | |
| 						logger.L().Error("failed to send negotiation response", zap.Error(err))
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				i++ // Skip option byte
 | |
| 			} else if cmd == SB {
 | |
| 				// Handle subnegotiation: skip until IAC SE
 | |
| 				i++
 | |
| 				for i < len(data)-1 {
 | |
| 					if data[i] == IAC && data[i+1] == SE {
 | |
| 						i++
 | |
| 						break
 | |
| 					}
 | |
| 					i++
 | |
| 				}
 | |
| 			}
 | |
| 			// Skip control sequence in output data
 | |
| 		} else {
 | |
| 			// Regular data byte, add to processed output
 | |
| 			processed = append(processed, data[i])
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return processed
 | |
| }
 | 
