mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-03-31 08:19:54 +02:00
Add files via upload
This commit is contained in:
2
go.mod
2
go.mod
@@ -8,6 +8,7 @@ require (
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/google/uuid v1.5.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.4.22
|
||||
github.com/mattn/go-sqlite3 v1.14.18
|
||||
github.com/modelcontextprotocol/go-sdk v1.2.0
|
||||
@@ -29,7 +30,6 @@ require (
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/jsonschema-go v0.3.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
|
||||
@@ -634,6 +634,7 @@ func setupRoutes(
|
||||
// 系统设置 - 终端(执行命令,提高运维效率)
|
||||
protected.POST("/terminal/run", terminalHandler.RunCommand)
|
||||
protected.POST("/terminal/run/stream", terminalHandler.RunCommandStream)
|
||||
protected.GET("/terminal/ws", terminalHandler.RunCommandWS)
|
||||
|
||||
// 外部MCP管理
|
||||
protected.GET("/external-mcp", externalMCPHandler.GetExternalMCPs)
|
||||
|
||||
@@ -27,6 +27,19 @@ type TerminalHandler struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// maskTerminalCommand 对可能包含敏感信息的终端命令做脱敏,避免在日志中直接记录密码等内容
|
||||
func maskTerminalCommand(cmd string) string {
|
||||
trimmed := strings.TrimSpace(cmd)
|
||||
lower := strings.ToLower(trimmed)
|
||||
if strings.Contains(lower, "sudo") || strings.Contains(lower, "password") {
|
||||
return "[masked sensitive terminal command]"
|
||||
}
|
||||
if len(trimmed) > 256 {
|
||||
return trimmed[:256] + "..."
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
// NewTerminalHandler 创建终端处理器
|
||||
func NewTerminalHandler(logger *zap.Logger) *TerminalHandler {
|
||||
return &TerminalHandler{logger: logger}
|
||||
@@ -146,7 +159,7 @@ func (h *TerminalHandler) RunCommand(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, resp)
|
||||
return
|
||||
}
|
||||
h.logger.Debug("终端命令执行异常", zap.String("command", cmdStr), zap.Error(err))
|
||||
h.logger.Debug("终端命令执行异常", zap.String("command", maskTerminalCommand(cmdStr)), zap.Error(err))
|
||||
}
|
||||
|
||||
// 统一为 \n,避免前端因 \r 出现错位/对角线排版
|
||||
|
||||
95
internal/handler/terminal_ws_unix.go
Normal file
95
internal/handler/terminal_ws_unix.go
Normal file
@@ -0,0 +1,95 @@
|
||||
//go:build !windows
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// wsUpgrader 仅用于系统设置中的终端 WebSocket,会复用已有的登录保护(JWT 中间件在上层路由组)
|
||||
var wsUpgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
// 由于已在 Gin 路由层做了认证,这里放宽 Origin,方便在同一域名下通过 HTTPS/WSS 访问
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// RunCommandWS 提供真正交互式 Shell:基于 WebSocket + PTY 的长会话
|
||||
// 前端建立 WebSocket 连接后,所有键盘输入都会透传到 Shell,Shell 的输出也会实时写回前端。
|
||||
func (h *TerminalHandler) RunCommandWS(c *gin.Context) {
|
||||
conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 启动交互式 Shell,这里优先使用 bash,找不到则退回 sh
|
||||
shell := "bash"
|
||||
if _, err := exec.LookPath(shell); err != nil {
|
||||
shell = "sh"
|
||||
}
|
||||
cmd := exec.Command(shell)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"COLUMNS=120",
|
||||
"LINES=40",
|
||||
"TERM=xterm-256color",
|
||||
)
|
||||
|
||||
ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{Cols: ptyCols, Rows: ptyRows})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer ptmx.Close()
|
||||
|
||||
// Shell -> WebSocket:将 PTY 输出实时发给前端
|
||||
doneChan := make(chan struct{})
|
||||
go func() {
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, err := ptmx.Read(buf)
|
||||
if n > 0 {
|
||||
_ = conn.WriteMessage(websocket.TextMessage, buf[:n])
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
close(doneChan)
|
||||
}()
|
||||
|
||||
// WebSocket -> Shell:将前端输入写入 PTY(包括 sudo 密码、Ctrl+C 等)
|
||||
conn.SetReadLimit(64 * 1024)
|
||||
_ = conn.SetReadDeadline(time.Now().Add(terminalTimeout))
|
||||
conn.SetPongHandler(func(string) error {
|
||||
_ = conn.SetReadDeadline(time.Now().Add(terminalTimeout))
|
||||
return nil
|
||||
})
|
||||
|
||||
for {
|
||||
msgType, data, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
_ = cmd.Process.Kill()
|
||||
break
|
||||
}
|
||||
if msgType != websocket.TextMessage && msgType != websocket.BinaryMessage {
|
||||
continue
|
||||
}
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
if _, err := ptmx.Write(data); err != nil {
|
||||
_ = cmd.Process.Kill()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
<-doneChan
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
var currentTabId = 1;
|
||||
var inited = false;
|
||||
var tabIdCounter = 1;
|
||||
var PROMPT = '\x1b[32m$\x1b[0m ';
|
||||
var PROMPT = ''; // 真实 Shell 自己输出提示符,这里不再自定义
|
||||
var HISTORY_MAX = 100;
|
||||
var CANCEL_AFTER_MS = 125000;
|
||||
|
||||
@@ -26,20 +26,16 @@
|
||||
return terminals[0] || null;
|
||||
}
|
||||
|
||||
var WELCOME_LINE = 'CyberStrikeAI 终端 - 直接输入命令,Enter 执行;↑↓ 历史;Ctrl+L 清屏\r\n';
|
||||
var WELCOME_LINE = 'CyberStrikeAI 终端 - 真实 Shell 会话,直接输入命令;Ctrl+L 清屏\r\n';
|
||||
|
||||
function writePrompt(tab) {
|
||||
var t = tab || getCurrent();
|
||||
if (t && t.term) t.term.write(PROMPT);
|
||||
// 提示符交由后端 Shell 自行输出,这里仅保留占位函数,避免旧代码报错
|
||||
}
|
||||
|
||||
function redrawTabDisplay(t) {
|
||||
if (!t || !t.term) return;
|
||||
t.term.clear();
|
||||
t.lineBuffer = '';
|
||||
if (t.cursorIndex !== undefined) t.cursorIndex = 0;
|
||||
t.term.write(WELCOME_LINE);
|
||||
t.term.write(PROMPT);
|
||||
}
|
||||
|
||||
function writeln(tabOrS, s) {
|
||||
@@ -65,100 +61,66 @@
|
||||
t.term.write(suffix);
|
||||
}
|
||||
|
||||
function getAuthHeaders() {
|
||||
var h = new Headers();
|
||||
h.set('Content-Type', 'application/json');
|
||||
// 从本地存储中获取当前登录 token(与 auth.js 使用的结构保持一致)
|
||||
function getStoredAuthToken() {
|
||||
try {
|
||||
var auth = localStorage.getItem('cyberstrike-auth');
|
||||
if (auth) {
|
||||
var o = JSON.parse(auth);
|
||||
if (o && o.token) h.set('Authorization', 'Bearer ' + o.token);
|
||||
}
|
||||
var raw = localStorage.getItem('cyberstrike-auth');
|
||||
if (!raw) return null;
|
||||
var o = JSON.parse(raw);
|
||||
if (o && o.token) return o.token;
|
||||
} catch (e) {}
|
||||
return h;
|
||||
return null;
|
||||
}
|
||||
|
||||
function runCommand(cmd, tab) {
|
||||
var t = tab || getCurrent();
|
||||
if (!t) return;
|
||||
if (t.running) return;
|
||||
runCommandImpl(cmd, t);
|
||||
// WebSocket 地址构造(兼容 http/https,并通过 query 传递 token 以通过后端鉴权)
|
||||
function buildTerminalWSURL() {
|
||||
var proto = (window.location.protocol === 'https:') ? 'wss://' : 'ws://';
|
||||
var url = proto + window.location.host + '/api/terminal/ws';
|
||||
var token = getStoredAuthToken();
|
||||
if (token) {
|
||||
url += '?token=' + encodeURIComponent(token);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function runCommandImpl(cmd, t) {
|
||||
t.running = true;
|
||||
t.abortController = new AbortController();
|
||||
var cancelTimer = setTimeout(function () {
|
||||
if (!t.running) return;
|
||||
t.running = false;
|
||||
writeln(t, '\x1b[2m(已取消 可继续输入)\x1b[0m');
|
||||
writePrompt(t);
|
||||
}, CANCEL_AFTER_MS);
|
||||
function ensureTerminalWS(tab) {
|
||||
if (tab.ws && (tab.ws.readyState === WebSocket.OPEN || tab.ws.readyState === WebSocket.CONNECTING)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var ws = new WebSocket(buildTerminalWSURL());
|
||||
tab.ws = ws;
|
||||
tab.running = true;
|
||||
|
||||
var done = function () {
|
||||
clearTimeout(cancelTimer);
|
||||
t.running = false;
|
||||
t.abortController = null;
|
||||
writePrompt(t);
|
||||
};
|
||||
ws.onopen = function () {
|
||||
if (tab.term) {
|
||||
tab.term.focus();
|
||||
}
|
||||
};
|
||||
|
||||
fetch('/api/terminal/run/stream', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ command: cmd }),
|
||||
signal: t.abortController.signal
|
||||
}).then(function (res) {
|
||||
if (!res.ok) return res.json().then(function (d) { throw new Error(d.error || 'HTTP ' + res.status); });
|
||||
var ct = res.headers.get('Content-Type') || '';
|
||||
if (ct.indexOf('text/event-stream') !== -1 && res.body) {
|
||||
return readSSEStream(res.body, t).then(done).catch(function () { done(); });
|
||||
}
|
||||
return res.json().then(function (data) {
|
||||
if (data.stdout) writeOutput(t, data.stdout, false);
|
||||
if (data.stderr) writeOutput(t, data.stderr, true);
|
||||
done();
|
||||
});
|
||||
}).catch(function (err) {
|
||||
if (err.name === 'AbortError') {
|
||||
writeln(t, '\x1b[2m(已取消)\x1b[0m');
|
||||
} else {
|
||||
writeln(t, '\x1b[31m错误: ' + (err.message || String(err)) + '\x1b[0m');
|
||||
}
|
||||
done();
|
||||
});
|
||||
}
|
||||
ws.onmessage = function (ev) {
|
||||
if (!tab.term) return;
|
||||
tab.term.write(ev.data);
|
||||
};
|
||||
|
||||
function readSSEStream(body, t) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var reader = body.getReader();
|
||||
var decoder = new TextDecoder();
|
||||
var buf = '';
|
||||
function read() {
|
||||
reader.read().then(function (result) {
|
||||
if (result.done) { resolve(); return; }
|
||||
buf += decoder.decode(result.value, { stream: true });
|
||||
var i;
|
||||
while ((i = buf.indexOf('\n\n')) !== -1) {
|
||||
var block = buf.slice(0, i);
|
||||
buf = buf.slice(i + 2);
|
||||
var dataLine = block.match(/data:\s*(.+)/);
|
||||
if (dataLine) {
|
||||
try {
|
||||
var ev = JSON.parse(dataLine[1]);
|
||||
if (ev.t === 'out' && ev.d !== undefined) t.term.writeln(ev.d);
|
||||
else if (ev.t === 'err' && ev.d !== undefined) t.term.write('\x1b[31m' + ev.d + '\x1b[0m\n');
|
||||
else if (ev.t === 'exit') {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
read();
|
||||
}).catch(reject);
|
||||
ws.onclose = function () {
|
||||
tab.running = false;
|
||||
if (tab.term) {
|
||||
tab.term.writeln('\r\n\x1b[2m[会话已关闭]\x1b[0m');
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = function () {
|
||||
tab.running = false;
|
||||
if (tab.term) {
|
||||
tab.term.writeln('\r\n\x1b[31m[终端连接出错]\x1b[0m');
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
if (tab.term) {
|
||||
tab.term.writeln('\r\n\x1b[31m[无法连接终端服务: ' + String(e) + ']\x1b[0m');
|
||||
}
|
||||
read();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createTerminalInContainer(container, tab) {
|
||||
@@ -206,7 +168,6 @@
|
||||
}
|
||||
term.open(container);
|
||||
term.write(WELCOME_LINE);
|
||||
term.write(PROMPT);
|
||||
container.addEventListener('click', function () {
|
||||
switchTerminalTab(tab.id);
|
||||
if (term) term.focus();
|
||||
@@ -214,105 +175,23 @@
|
||||
container.setAttribute('tabindex', '0');
|
||||
container.title = '点击此处后输入命令';
|
||||
|
||||
function redrawLine(t) {
|
||||
if (!t || !t.term) return;
|
||||
var n = t.lineBuffer.length - t.cursorIndex;
|
||||
t.term.write('\r\x1b[K' + PROMPT + t.lineBuffer);
|
||||
if (n > 0) t.term.write('\x1b[' + n + 'D');
|
||||
function sendToWS(data) {
|
||||
ensureTerminalWS(tab);
|
||||
if (tab.ws && tab.ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
tab.ws.send(data);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
term.onData(function (data) {
|
||||
// Ctrl+L:本地清屏,同时把 ^L 也发给后端
|
||||
if (data === '\x0c') {
|
||||
term.clear();
|
||||
tab.lineBuffer = '';
|
||||
tab.cursorIndex = 0;
|
||||
writePrompt(tab);
|
||||
sendToWS(data);
|
||||
return;
|
||||
}
|
||||
if (data === '\x1b[A') {
|
||||
if (tab.history.length === 0) return;
|
||||
if (tab.historyIndex < 0) tab.historyIndex = tab.history.length;
|
||||
tab.historyIndex--;
|
||||
if (tab.historyIndex < 0) tab.historyIndex = 0;
|
||||
tab.lineBuffer = tab.history[tab.historyIndex];
|
||||
tab.cursorIndex = tab.lineBuffer.length;
|
||||
term.write('\r\x1b[K' + PROMPT + tab.lineBuffer);
|
||||
return;
|
||||
}
|
||||
if (data === '\x1b[B') {
|
||||
if (tab.history.length === 0) return;
|
||||
tab.historyIndex++;
|
||||
if (tab.historyIndex >= tab.history.length) {
|
||||
tab.historyIndex = -1;
|
||||
tab.lineBuffer = '';
|
||||
tab.cursorIndex = 0;
|
||||
term.write('\r\x1b[K' + PROMPT);
|
||||
} else {
|
||||
tab.lineBuffer = tab.history[tab.historyIndex];
|
||||
tab.cursorIndex = tab.lineBuffer.length;
|
||||
term.write('\r\x1b[K' + PROMPT + tab.lineBuffer);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (data === '\x1b[D') {
|
||||
if (tab.cursorIndex > 0) {
|
||||
tab.cursorIndex--;
|
||||
term.write('\x1b[D');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (data === '\x1b[C') {
|
||||
if (tab.cursorIndex < tab.lineBuffer.length) {
|
||||
tab.cursorIndex++;
|
||||
term.write('\x1b[C');
|
||||
}
|
||||
return;
|
||||
}
|
||||
var code = data.charCodeAt(0);
|
||||
if (code === 13 || code === 10) {
|
||||
var cmd = tab.lineBuffer.trim();
|
||||
tab.lineBuffer = '';
|
||||
tab.cursorIndex = 0;
|
||||
tab.historyIndex = -1;
|
||||
term.writeln('');
|
||||
if (cmd) {
|
||||
if (tab.history.indexOf(cmd) === -1) {
|
||||
tab.history.push(cmd);
|
||||
if (tab.history.length > HISTORY_MAX) tab.history.shift();
|
||||
}
|
||||
runCommand(cmd, tab);
|
||||
} else {
|
||||
writePrompt(tab);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (code === 127) {
|
||||
if (tab.cursorIndex > 0) {
|
||||
tab.lineBuffer = tab.lineBuffer.slice(0, tab.cursorIndex - 1) + tab.lineBuffer.slice(tab.cursorIndex);
|
||||
tab.cursorIndex--;
|
||||
redrawLine(tab);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (code === 3) {
|
||||
if (tab.running && tab.abortController) {
|
||||
tab.abortController.abort();
|
||||
}
|
||||
tab.lineBuffer = '';
|
||||
tab.cursorIndex = 0;
|
||||
term.writeln('^C');
|
||||
writePrompt(tab);
|
||||
return;
|
||||
}
|
||||
if (data.length === 1 && code >= 32) {
|
||||
tab.lineBuffer = tab.lineBuffer.slice(0, tab.cursorIndex) + data + tab.lineBuffer.slice(tab.cursorIndex);
|
||||
tab.cursorIndex++;
|
||||
redrawLine(tab);
|
||||
return;
|
||||
}
|
||||
tab.lineBuffer += data;
|
||||
tab.cursorIndex = tab.lineBuffer.length;
|
||||
term.write(data);
|
||||
sendToWS(data);
|
||||
});
|
||||
|
||||
tab.term = term;
|
||||
|
||||
Reference in New Issue
Block a user