mirror of
https://github.com/veops/oneterm.git
synced 2025-09-27 03:36:02 +08:00
391 lines
10 KiB
Go
391 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
|
|
}
|
|
|
|
// 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 {
|
|
conn.Close()
|
|
return nil
|
|
}
|
|
if err.Error() == "io: read/write on closed pipe" {
|
|
return nil
|
|
}
|
|
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().Info("write to telnet failed, connection likely closed", 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 {
|
|
defer func() {
|
|
conn.Close()
|
|
if chs.Rin != nil {
|
|
chs.Rin.Close()
|
|
}
|
|
// Close AwayChan to signal connection end
|
|
sess.Once.Do(func() {
|
|
close(chs.AwayChan)
|
|
})
|
|
}()
|
|
|
|
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 {
|
|
return fmt.Errorf("telnet connection closed")
|
|
}
|
|
if strings.Contains(err.Error(), "use of closed network connection") {
|
|
return nil
|
|
}
|
|
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
|
|
|
|
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
|
|
}
|