feat: WebShell真实SSH终端 + 移除日志能力菜单
This commit is contained in:
1 parent
925348e583
commit
3be53b89fa
6 files changed
+338
-50
No files matched your search
@@ -4,10 +4,13 @@ import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"ezops/internal/models"
|
||||
@@ -15,13 +18,13 @@ import (
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true // allow all origins in dev
|
||||
return true
|
||||
},
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
// WSMessage WebSocket消息结构
|
||||
// WSMessage WebSocket消息结构 // Logan.Li
|
||||
type WSMessage struct {
|
||||
Status int `json:"status"` // 0=data, 1=resize
|
||||
Data string `json:"data"`
|
||||
@@ -32,7 +35,7 @@ type WSMessage struct {
|
||||
// WebShellHandler WebShell处理器
|
||||
type WebShellHandler struct {
|
||||
db *gorm.DB
|
||||
clients sync.Map // serverID -> []*websocket.Conn
|
||||
clients sync.Map
|
||||
}
|
||||
|
||||
// NewWebShellHandler 创建WebShell处理器
|
||||
@@ -40,14 +43,7 @@ func NewWebShellHandler(db *gorm.DB) *WebShellHandler {
|
||||
return &WebShellHandler{db: db}
|
||||
}
|
||||
|
||||
// Terminal WebSocket终端连接
|
||||
// @Summary WebShell终端
|
||||
// @Description 通过WebSocket连接到服务器的SSH终端
|
||||
// @Tags WebShell
|
||||
// @Param id query int true "服务器ID"
|
||||
// @Param cols query int false "终端列数"
|
||||
// @Param rows query int false "终端行数"
|
||||
// @Router /ws/terminal [get]
|
||||
// Terminal WebSocket终端连接 — 真实SSH // Logan.Li
|
||||
func (h *WebShellHandler) Terminal(c *gin.Context) {
|
||||
serverID := c.Query("id")
|
||||
if serverID == "" {
|
||||
@@ -55,36 +51,127 @@ func (h *WebShellHandler) Terminal(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify server exists
|
||||
var server models.Server
|
||||
if err := h.db.First(&server, serverID).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "server not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Upgrade to WebSocket
|
||||
// 先升级 WebSocket — 这样前端能看到连接过程
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
log.Printf("[WebShell] upgrade error: %v", err)
|
||||
log.Printf("[WebShell] WebSocket升级失败: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
log.Printf("[WebShell] connected to server %s (%s:%d)", server.Hostname, server.IP, server.Port)
|
||||
|
||||
// Send welcome message
|
||||
welcome := WSMessage{
|
||||
conn.WriteJSON(WSMessage{
|
||||
Status: 0,
|
||||
Data: "\r\n\x1b[32mEZOps WebShell\x1b[0m - Connected to " + server.Hostname + " (" + server.IP + ")\r\n",
|
||||
}
|
||||
conn.WriteJSON(welcome)
|
||||
Data: "\r\n\x1b[36m正在连接 " + server.Hostname + " (" + server.IP + ")...\x1b[0m\r\n",
|
||||
})
|
||||
|
||||
// Demo mode: echo input back with a prompt
|
||||
// In production this would connect to actual SSH via crypto/ssh or a PTY bridge
|
||||
// 构建 SSH 配置
|
||||
sshConfig := &ssh.ClientConfig{
|
||||
User: server.Username,
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
switch server.AuthType {
|
||||
case "key":
|
||||
signer, err := ssh.ParsePrivateKey([]byte(server.SSHKey))
|
||||
if err != nil {
|
||||
sshConfig.Auth = []ssh.AuthMethod{ssh.Password(server.Password)}
|
||||
} else {
|
||||
sshConfig.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
|
||||
}
|
||||
default:
|
||||
sshConfig.Auth = []ssh.AuthMethod{
|
||||
ssh.Password(server.Password),
|
||||
}
|
||||
}
|
||||
|
||||
// 连接 SSH(带10秒超时)
|
||||
addr := server.IP + ":" + strconv.Itoa(server.Port)
|
||||
sshClient, err := ssh.Dial("tcp", addr, sshConfig)
|
||||
if err != nil {
|
||||
log.Printf("[WebShell] SSH连接失败 %s: %v", addr, err)
|
||||
conn.WriteJSON(WSMessage{
|
||||
Status: 0,
|
||||
Data: "\r\n\x1b[31mSSH连接失败: " + err.Error() + "\x1b[0m\r\n\x1b[33m提示: 请检查服务器IP/端口/密码是否正确,目标主机是否在线\x1b[0m\r\n",
|
||||
})
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
defer sshClient.Close()
|
||||
|
||||
log.Printf("[WebShell] SSH已连接 %s (%s)", server.Hostname, addr)
|
||||
|
||||
// 创建 SSH session
|
||||
session, err := sshClient.NewSession()
|
||||
if err != nil {
|
||||
log.Printf("[WebShell] 创建session失败: %v", err)
|
||||
conn.WriteJSON(WSMessage{Status: 0, Data: "\r\n\x1b[31m创建SSH会话失败\x1b[0m\r\n"})
|
||||
return
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
// 请求 PTY
|
||||
modes := ssh.TerminalModes{
|
||||
ssh.ECHO: 1,
|
||||
ssh.TTY_OP_ISPEED: 14400,
|
||||
ssh.TTY_OP_OSPEED: 14400,
|
||||
}
|
||||
if err := session.RequestPty("xterm-256color", 24, 80, modes); err != nil {
|
||||
log.Printf("[WebShell] PTY请求失败: %v", err)
|
||||
conn.WriteJSON(WSMessage{Status: 0, Data: "\r\n\x1b[31mPTY请求失败\x1b[0m\r\n"})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取管道
|
||||
stdinPipe, err := session.StdinPipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
stdoutPipe, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 启动 shell
|
||||
if err := session.Shell(); err != nil {
|
||||
log.Printf("[WebShell] Shell启动失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 发送欢迎消息
|
||||
conn.WriteJSON(WSMessage{
|
||||
Status: 0,
|
||||
Data: "\r\n\x1b[32mEZOps WebShell\x1b[0m — 已连接 " + server.Hostname + " (" + server.IP + ")\r\n\r\n",
|
||||
})
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
// goroutine: SSH stdout -> WebSocket
|
||||
go func() {
|
||||
defer close(done)
|
||||
buf := make([]byte, 8192)
|
||||
for {
|
||||
n, err := stdoutPipe.Read(buf)
|
||||
if err != nil {
|
||||
conn.WriteJSON(WSMessage{Status: 0, Data: "\r\n\x1b[31m[远程连接已断开]\x1b[0m\r\n"})
|
||||
break
|
||||
}
|
||||
if n > 0 {
|
||||
conn.WriteJSON(WSMessage{Status: 0, Data: string(buf[:n])})
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 主循环: WebSocket -> SSH stdin
|
||||
for {
|
||||
msgType, data, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("[WebShell] read error: %v", err)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -95,29 +182,18 @@ func (h *WebShellHandler) Terminal(c *gin.Context) {
|
||||
}
|
||||
|
||||
switch msg.Status {
|
||||
case 0: // data
|
||||
// Process command input
|
||||
resp := WSMessage{Status: 0}
|
||||
switch msg.Data {
|
||||
case "\r", "\n":
|
||||
resp.Data = "\r\n[" + server.Hostname + " ~]$ "
|
||||
case "\u0003": // Ctrl+C
|
||||
resp.Data = "^C\r\n[" + server.Hostname + " ~]$ "
|
||||
case "\u0004": // Ctrl+D
|
||||
resp.Data = "\r\n\x1b[33mConnection closed.\x1b[0m\r\n"
|
||||
conn.WriteJSON(resp)
|
||||
goto done
|
||||
default:
|
||||
resp.Data = msg.Data // echo back
|
||||
}
|
||||
conn.WriteJSON(resp)
|
||||
|
||||
case 0: // 数据输入
|
||||
stdinPipe.Write([]byte(msg.Data))
|
||||
case 1: // resize
|
||||
log.Printf("[WebShell] resize to cols=%d rows=%d", msg.Cols, msg.Rows)
|
||||
if msg.Cols > 0 && msg.Rows > 0 {
|
||||
session.WindowChange(msg.Rows, msg.Cols)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
done:
|
||||
log.Printf("[WebShell] disconnected from server %s", server.Hostname)
|
||||
// 等待 stdout goroutine 结束
|
||||
<-done
|
||||
stdinPipe.Close()
|
||||
log.Printf("[WebShell] 断开 %s (%s)", server.Hostname, addr)
|
||||
}
|
||||
Reference in new issue
Block a user