mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-18 14:04:52 +02:00
Add files via upload
This commit is contained in:
+13
-2
@@ -15,11 +15,11 @@ import (
|
||||
"cyberstrike-ai/internal/database"
|
||||
"cyberstrike-ai/internal/handler"
|
||||
"cyberstrike-ai/internal/knowledge"
|
||||
"cyberstrike-ai/internal/robot"
|
||||
"cyberstrike-ai/internal/logger"
|
||||
"cyberstrike-ai/internal/mcp"
|
||||
"cyberstrike-ai/internal/mcp/builtin"
|
||||
"cyberstrike-ai/internal/openai"
|
||||
"cyberstrike-ai/internal/robot"
|
||||
"cyberstrike-ai/internal/security"
|
||||
"cyberstrike-ai/internal/skills"
|
||||
"cyberstrike-ai/internal/storage"
|
||||
@@ -46,7 +46,7 @@ type App struct {
|
||||
knowledgeHandler *handler.KnowledgeHandler // 知识库处理器(用于动态初始化)
|
||||
agentHandler *handler.AgentHandler // Agent处理器(用于更新知识库管理器)
|
||||
robotHandler *handler.RobotHandler // 机器人处理器(钉钉/飞书/企业微信)
|
||||
robotMu sync.Mutex // 保护钉钉/飞书长连接的 cancel
|
||||
robotMu sync.Mutex // 保护钉钉/飞书长连接的 cancel
|
||||
dingCancel context.CancelFunc // 钉钉 Stream 取消函数,用于配置变更时重启
|
||||
larkCancel context.CancelFunc // 飞书长连接取消函数,用于配置变更时重启
|
||||
}
|
||||
@@ -319,6 +319,7 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
|
||||
authHandler := handler.NewAuthHandler(authManager, cfg, configPath, log.Logger)
|
||||
attackChainHandler := handler.NewAttackChainHandler(db, &cfg.OpenAI, log.Logger)
|
||||
vulnerabilityHandler := handler.NewVulnerabilityHandler(db, log.Logger)
|
||||
webshellHandler := handler.NewWebShellHandler(log.Logger, db)
|
||||
configHandler := handler.NewConfigHandler(configPath, cfg, mcpServer, executor, agent, attackChainHandler, externalMCPMgr, log.Logger)
|
||||
externalMCPHandler := handler.NewExternalMCPHandler(externalMCPMgr, cfg, configPath, log.Logger)
|
||||
roleHandler := handler.NewRoleHandler(cfg, configPath, log.Logger)
|
||||
@@ -429,6 +430,7 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
|
||||
attackChainHandler,
|
||||
app, // 传递 App 实例以便动态获取 knowledgeHandler
|
||||
vulnerabilityHandler,
|
||||
webshellHandler,
|
||||
roleHandler,
|
||||
skillsHandler,
|
||||
fofaHandler,
|
||||
@@ -556,6 +558,7 @@ func setupRoutes(
|
||||
attackChainHandler *handler.AttackChainHandler,
|
||||
app *App, // 传递 App 实例以便动态获取 knowledgeHandler
|
||||
vulnerabilityHandler *handler.VulnerabilityHandler,
|
||||
webshellHandler *handler.WebShellHandler,
|
||||
roleHandler *handler.RoleHandler,
|
||||
skillsHandler *handler.SkillsHandler,
|
||||
fofaHandler *handler.FofaHandler,
|
||||
@@ -817,6 +820,14 @@ func setupRoutes(
|
||||
protected.PUT("/vulnerabilities/:id", vulnerabilityHandler.UpdateVulnerability)
|
||||
protected.DELETE("/vulnerabilities/:id", vulnerabilityHandler.DeleteVulnerability)
|
||||
|
||||
// WebShell 管理(代理执行 + 连接配置存 SQLite)
|
||||
protected.GET("/webshell/connections", webshellHandler.ListConnections)
|
||||
protected.POST("/webshell/connections", webshellHandler.CreateConnection)
|
||||
protected.PUT("/webshell/connections/:id", webshellHandler.UpdateConnection)
|
||||
protected.DELETE("/webshell/connections/:id", webshellHandler.DeleteConnection)
|
||||
protected.POST("/webshell/exec", webshellHandler.Exec)
|
||||
protected.POST("/webshell/file", webshellHandler.FileOp)
|
||||
|
||||
// 角色管理
|
||||
protected.GET("/roles", roleHandler.GetRoles)
|
||||
protected.GET("/roles/:name", roleHandler.GetRole)
|
||||
|
||||
@@ -227,6 +227,19 @@ func (db *DB) initTables() error {
|
||||
FOREIGN KEY (queue_id) REFERENCES batch_task_queues(id) ON DELETE CASCADE
|
||||
);`
|
||||
|
||||
// 创建 WebShell 连接表
|
||||
createWebshellConnectionsTable := `
|
||||
CREATE TABLE IF NOT EXISTS webshell_connections (
|
||||
id TEXT PRIMARY KEY,
|
||||
url TEXT NOT NULL,
|
||||
password TEXT NOT NULL DEFAULT '',
|
||||
type TEXT NOT NULL DEFAULT 'php',
|
||||
method TEXT NOT NULL DEFAULT 'post',
|
||||
cmd_param TEXT NOT NULL DEFAULT '',
|
||||
remark TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);`
|
||||
|
||||
// 创建索引
|
||||
createIndexes := `
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id);
|
||||
@@ -253,6 +266,7 @@ func (db *DB) initTables() error {
|
||||
CREATE INDEX IF NOT EXISTS idx_batch_tasks_queue_id ON batch_tasks(queue_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_batch_task_queues_created_at ON batch_task_queues(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_batch_task_queues_title ON batch_task_queues(title);
|
||||
CREATE INDEX IF NOT EXISTS idx_webshell_connections_created_at ON webshell_connections(created_at);
|
||||
`
|
||||
|
||||
if _, err := db.Exec(createConversationsTable); err != nil {
|
||||
@@ -311,6 +325,10 @@ func (db *DB) initTables() error {
|
||||
return fmt.Errorf("创建batch_tasks表失败: %w", err)
|
||||
}
|
||||
|
||||
if _, err := db.Exec(createWebshellConnectionsTable); err != nil {
|
||||
return fmt.Errorf("创建webshell_connections表失败: %w", err)
|
||||
}
|
||||
|
||||
// 为已有表添加新字段(如果不存在)- 必须在创建索引之前
|
||||
if err := db.migrateConversationsTable(); err != nil {
|
||||
db.logger.Warn("迁移conversations表失败", zap.Error(err))
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// WebShellConnection WebShell 连接配置
|
||||
type WebShellConnection struct {
|
||||
ID string `json:"id"`
|
||||
URL string `json:"url"`
|
||||
Password string `json:"password"`
|
||||
Type string `json:"type"`
|
||||
Method string `json:"method"`
|
||||
CmdParam string `json:"cmdParam"`
|
||||
Remark string `json:"remark"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
// ListWebshellConnections 列出所有 WebShell 连接,按创建时间倒序
|
||||
func (db *DB) ListWebshellConnections() ([]WebShellConnection, error) {
|
||||
query := `
|
||||
SELECT id, url, password, type, method, cmd_param, remark, created_at
|
||||
FROM webshell_connections
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
db.logger.Error("查询 WebShell 连接列表失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var list []WebShellConnection
|
||||
for rows.Next() {
|
||||
var c WebShellConnection
|
||||
err := rows.Scan(&c.ID, &c.URL, &c.Password, &c.Type, &c.Method, &c.CmdParam, &c.Remark, &c.CreatedAt)
|
||||
if err != nil {
|
||||
db.logger.Warn("扫描 WebShell 连接行失败", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
list = append(list, c)
|
||||
}
|
||||
return list, rows.Err()
|
||||
}
|
||||
|
||||
// GetWebshellConnection 根据 ID 获取一条连接
|
||||
func (db *DB) GetWebshellConnection(id string) (*WebShellConnection, error) {
|
||||
query := `
|
||||
SELECT id, url, password, type, method, cmd_param, remark, created_at
|
||||
FROM webshell_connections WHERE id = ?
|
||||
`
|
||||
var c WebShellConnection
|
||||
err := db.QueryRow(query, id).Scan(&c.ID, &c.URL, &c.Password, &c.Type, &c.Method, &c.CmdParam, &c.Remark, &c.CreatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
db.logger.Error("查询 WebShell 连接失败", zap.Error(err), zap.String("id", id))
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// CreateWebshellConnection 创建 WebShell 连接
|
||||
func (db *DB) CreateWebshellConnection(c *WebShellConnection) error {
|
||||
query := `
|
||||
INSERT INTO webshell_connections (id, url, password, type, method, cmd_param, remark, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
_, err := db.Exec(query, c.ID, c.URL, c.Password, c.Type, c.Method, c.CmdParam, c.Remark, c.CreatedAt)
|
||||
if err != nil {
|
||||
db.logger.Error("创建 WebShell 连接失败", zap.Error(err), zap.String("id", c.ID))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateWebshellConnection 更新 WebShell 连接
|
||||
func (db *DB) UpdateWebshellConnection(c *WebShellConnection) error {
|
||||
query := `
|
||||
UPDATE webshell_connections
|
||||
SET url = ?, password = ?, type = ?, method = ?, cmd_param = ?, remark = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
result, err := db.Exec(query, c.URL, c.Password, c.Type, c.Method, c.CmdParam, c.Remark, c.ID)
|
||||
if err != nil {
|
||||
db.logger.Error("更新 WebShell 连接失败", zap.Error(err), zap.String("id", c.ID))
|
||||
return err
|
||||
}
|
||||
affected, _ := result.RowsAffected()
|
||||
if affected == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteWebshellConnection 删除 WebShell 连接
|
||||
func (db *DB) DeleteWebshellConnection(id string) error {
|
||||
result, err := db.Exec(`DELETE FROM webshell_connections WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
db.logger.Error("删除 WebShell 连接失败", zap.Error(err), zap.String("id", id))
|
||||
return err
|
||||
}
|
||||
affected, _ := result.RowsAffected()
|
||||
if affected == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cyberstrike-ai/internal/database"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// WebShellHandler 代理执行 WebShell 命令(类似冰蝎/蚁剑),避免前端跨域并统一构建请求
|
||||
type WebShellHandler struct {
|
||||
logger *zap.Logger
|
||||
client *http.Client
|
||||
db *database.DB
|
||||
}
|
||||
|
||||
// NewWebShellHandler 创建 WebShell 处理器,db 可为 nil(连接配置接口将不可用)
|
||||
func NewWebShellHandler(logger *zap.Logger, db *database.DB) *WebShellHandler {
|
||||
return &WebShellHandler{
|
||||
logger: logger,
|
||||
client: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{DisableKeepAlives: false},
|
||||
},
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateConnectionRequest 创建连接请求
|
||||
type CreateConnectionRequest struct {
|
||||
URL string `json:"url" binding:"required"`
|
||||
Password string `json:"password"`
|
||||
Type string `json:"type"`
|
||||
Method string `json:"method"`
|
||||
CmdParam string `json:"cmd_param"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// UpdateConnectionRequest 更新连接请求
|
||||
type UpdateConnectionRequest struct {
|
||||
URL string `json:"url" binding:"required"`
|
||||
Password string `json:"password"`
|
||||
Type string `json:"type"`
|
||||
Method string `json:"method"`
|
||||
CmdParam string `json:"cmd_param"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// ListConnections 列出所有 WebShell 连接(GET /api/webshell/connections)
|
||||
func (h *WebShellHandler) ListConnections(c *gin.Context) {
|
||||
if h.db == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "database not available"})
|
||||
return
|
||||
}
|
||||
list, err := h.db.ListWebshellConnections()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []database.WebShellConnection{}
|
||||
}
|
||||
c.JSON(http.StatusOK, list)
|
||||
}
|
||||
|
||||
// CreateConnection 创建 WebShell 连接(POST /api/webshell/connections)
|
||||
func (h *WebShellHandler) CreateConnection(c *gin.Context) {
|
||||
if h.db == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "database not available"})
|
||||
return
|
||||
}
|
||||
var req CreateConnectionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
req.URL = strings.TrimSpace(req.URL)
|
||||
if req.URL == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "url is required"})
|
||||
return
|
||||
}
|
||||
if _, err := url.Parse(req.URL); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid url"})
|
||||
return
|
||||
}
|
||||
method := strings.ToLower(strings.TrimSpace(req.Method))
|
||||
if method != "get" && method != "post" {
|
||||
method = "post"
|
||||
}
|
||||
shellType := strings.ToLower(strings.TrimSpace(req.Type))
|
||||
if shellType == "" {
|
||||
shellType = "php"
|
||||
}
|
||||
conn := &database.WebShellConnection{
|
||||
ID: "ws_" + strings.ReplaceAll(uuid.New().String(), "-", "")[:12],
|
||||
URL: req.URL,
|
||||
Password: strings.TrimSpace(req.Password),
|
||||
Type: shellType,
|
||||
Method: method,
|
||||
CmdParam: strings.TrimSpace(req.CmdParam),
|
||||
Remark: strings.TrimSpace(req.Remark),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := h.db.CreateWebshellConnection(conn); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, conn)
|
||||
}
|
||||
|
||||
// UpdateConnection 更新 WebShell 连接(PUT /api/webshell/connections/:id)
|
||||
func (h *WebShellHandler) UpdateConnection(c *gin.Context) {
|
||||
if h.db == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "database not available"})
|
||||
return
|
||||
}
|
||||
id := strings.TrimSpace(c.Param("id"))
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "id is required"})
|
||||
return
|
||||
}
|
||||
var req UpdateConnectionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
req.URL = strings.TrimSpace(req.URL)
|
||||
if req.URL == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "url is required"})
|
||||
return
|
||||
}
|
||||
if _, err := url.Parse(req.URL); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid url"})
|
||||
return
|
||||
}
|
||||
method := strings.ToLower(strings.TrimSpace(req.Method))
|
||||
if method != "get" && method != "post" {
|
||||
method = "post"
|
||||
}
|
||||
shellType := strings.ToLower(strings.TrimSpace(req.Type))
|
||||
if shellType == "" {
|
||||
shellType = "php"
|
||||
}
|
||||
conn := &database.WebShellConnection{
|
||||
ID: id,
|
||||
URL: req.URL,
|
||||
Password: strings.TrimSpace(req.Password),
|
||||
Type: shellType,
|
||||
Method: method,
|
||||
CmdParam: strings.TrimSpace(req.CmdParam),
|
||||
Remark: strings.TrimSpace(req.Remark),
|
||||
}
|
||||
if err := h.db.UpdateWebshellConnection(conn); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "connection not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
updated, _ := h.db.GetWebshellConnection(id)
|
||||
if updated != nil {
|
||||
c.JSON(http.StatusOK, updated)
|
||||
} else {
|
||||
c.JSON(http.StatusOK, conn)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteConnection 删除 WebShell 连接(DELETE /api/webshell/connections/:id)
|
||||
func (h *WebShellHandler) DeleteConnection(c *gin.Context) {
|
||||
if h.db == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "database not available"})
|
||||
return
|
||||
}
|
||||
id := strings.TrimSpace(c.Param("id"))
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "id is required"})
|
||||
return
|
||||
}
|
||||
if err := h.db.DeleteWebshellConnection(id); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "connection not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
// ExecRequest 执行命令请求(前端传入连接信息 + 命令)
|
||||
type ExecRequest struct {
|
||||
URL string `json:"url" binding:"required"`
|
||||
Password string `json:"password"`
|
||||
Type string `json:"type"` // php, asp, aspx, jsp, custom
|
||||
Method string `json:"method"` // GET 或 POST,空则默认 POST
|
||||
CmdParam string `json:"cmd_param"` // 命令参数名,如 cmd/xxx,空则默认 cmd
|
||||
Command string `json:"command" binding:"required"`
|
||||
}
|
||||
|
||||
// ExecResponse 执行命令响应
|
||||
type ExecResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Output string `json:"output"`
|
||||
Error string `json:"error,omitempty"`
|
||||
HTTPCode int `json:"http_code,omitempty"`
|
||||
}
|
||||
|
||||
// FileOpRequest 文件操作请求
|
||||
type FileOpRequest struct {
|
||||
URL string `json:"url" binding:"required"`
|
||||
Password string `json:"password"`
|
||||
Type string `json:"type"`
|
||||
Method string `json:"method"` // GET 或 POST,空则默认 POST
|
||||
CmdParam string `json:"cmd_param"` // 命令参数名,如 cmd/xxx,空则默认 cmd
|
||||
Action string `json:"action" binding:"required"` // list, read, delete, write
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"` // write 时使用
|
||||
}
|
||||
|
||||
// FileOpResponse 文件操作响应
|
||||
type FileOpResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Output string `json:"output"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (h *WebShellHandler) Exec(c *gin.Context) {
|
||||
var req ExecRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
req.URL = strings.TrimSpace(req.URL)
|
||||
req.Command = strings.TrimSpace(req.Command)
|
||||
if req.URL == "" || req.Command == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "url and command are required"})
|
||||
return
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(req.URL)
|
||||
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid url: only http(s) allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
useGET := strings.ToUpper(strings.TrimSpace(req.Method)) == "GET"
|
||||
cmdParam := strings.TrimSpace(req.CmdParam)
|
||||
if cmdParam == "" {
|
||||
cmdParam = "cmd"
|
||||
}
|
||||
var httpReq *http.Request
|
||||
if useGET {
|
||||
targetURL := h.buildExecURL(req.URL, req.Type, req.Password, cmdParam, req.Command)
|
||||
httpReq, err = http.NewRequest(http.MethodGet, targetURL, nil)
|
||||
} else {
|
||||
body := h.buildExecBody(req.Type, req.Password, cmdParam, req.Command)
|
||||
httpReq, err = http.NewRequest(http.MethodPost, req.URL, bytes.NewReader(body))
|
||||
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
if err != nil {
|
||||
h.logger.Warn("webshell exec NewRequest", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, ExecResponse{OK: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("User-Agent", "Mozilla/5.0 (compatible; CyberStrikeAI-WebShell/1.0)")
|
||||
|
||||
resp, err := h.client.Do(httpReq)
|
||||
if err != nil {
|
||||
h.logger.Warn("webshell exec Do", zap.String("url", req.URL), zap.Error(err))
|
||||
c.JSON(http.StatusOK, ExecResponse{OK: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
out, _ := io.ReadAll(resp.Body)
|
||||
output := string(out)
|
||||
httpCode := resp.StatusCode
|
||||
|
||||
c.JSON(http.StatusOK, ExecResponse{
|
||||
OK: resp.StatusCode == http.StatusOK,
|
||||
Output: output,
|
||||
HTTPCode: httpCode,
|
||||
})
|
||||
}
|
||||
|
||||
// buildExecBody 按常见 WebShell 约定构建 POST 体(多数使用 pass + cmd,可配置命令参数名)
|
||||
func (h *WebShellHandler) buildExecBody(shellType, password, cmdParam, command string) []byte {
|
||||
form := h.execParams(shellType, password, cmdParam, command)
|
||||
return []byte(form.Encode())
|
||||
}
|
||||
|
||||
// buildExecURL 构建 GET 请求的完整 URL(baseURL + ?pass=xxx&cmd=yyy,cmd 可配置)
|
||||
func (h *WebShellHandler) buildExecURL(baseURL, shellType, password, cmdParam, command string) string {
|
||||
form := h.execParams(shellType, password, cmdParam, command)
|
||||
if parsed, err := url.Parse(baseURL); err == nil {
|
||||
parsed.RawQuery = form.Encode()
|
||||
return parsed.String()
|
||||
}
|
||||
return baseURL + "?" + form.Encode()
|
||||
}
|
||||
|
||||
func (h *WebShellHandler) execParams(shellType, password, cmdParam, command string) url.Values {
|
||||
shellType = strings.ToLower(strings.TrimSpace(shellType))
|
||||
if shellType == "" {
|
||||
shellType = "php"
|
||||
}
|
||||
if strings.TrimSpace(cmdParam) == "" {
|
||||
cmdParam = "cmd"
|
||||
}
|
||||
form := url.Values{}
|
||||
form.Set("pass", password)
|
||||
form.Set(cmdParam, command)
|
||||
return form
|
||||
}
|
||||
|
||||
func (h *WebShellHandler) FileOp(c *gin.Context) {
|
||||
var req FileOpRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
req.URL = strings.TrimSpace(req.URL)
|
||||
req.Action = strings.ToLower(strings.TrimSpace(req.Action))
|
||||
if req.URL == "" || req.Action == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "url and action are required"})
|
||||
return
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(req.URL)
|
||||
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid url: only http(s) allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
// 通过执行系统命令实现文件操作(与通用一句话兼容)
|
||||
var command string
|
||||
shellType := strings.ToLower(strings.TrimSpace(req.Type))
|
||||
switch req.Action {
|
||||
case "list":
|
||||
path := strings.TrimSpace(req.Path)
|
||||
if path == "" {
|
||||
path = "."
|
||||
}
|
||||
if shellType == "asp" || shellType == "aspx" {
|
||||
command = "dir " + h.escapePath(path)
|
||||
} else {
|
||||
command = "ls -la " + h.escapePath(path)
|
||||
}
|
||||
case "read":
|
||||
if shellType == "asp" || shellType == "aspx" {
|
||||
command = "type " + h.escapePath(strings.TrimSpace(req.Path))
|
||||
} else {
|
||||
command = "cat " + h.escapePath(strings.TrimSpace(req.Path))
|
||||
}
|
||||
case "delete":
|
||||
if shellType == "asp" || shellType == "aspx" {
|
||||
command = "del " + h.escapePath(strings.TrimSpace(req.Path))
|
||||
} else {
|
||||
command = "rm -f " + h.escapePath(strings.TrimSpace(req.Path))
|
||||
}
|
||||
case "write":
|
||||
path := h.escapePath(strings.TrimSpace(req.Path))
|
||||
command = "echo " + h.escapeForEcho(req.Content) + " > " + path
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, FileOpResponse{OK: false, Error: "unsupported action: " + req.Action})
|
||||
return
|
||||
}
|
||||
|
||||
useGET := strings.ToUpper(strings.TrimSpace(req.Method)) == "GET"
|
||||
cmdParam := strings.TrimSpace(req.CmdParam)
|
||||
if cmdParam == "" {
|
||||
cmdParam = "cmd"
|
||||
}
|
||||
var httpReq *http.Request
|
||||
if useGET {
|
||||
targetURL := h.buildExecURL(req.URL, req.Type, req.Password, cmdParam, command)
|
||||
httpReq, err = http.NewRequest(http.MethodGet, targetURL, nil)
|
||||
} else {
|
||||
body := h.buildExecBody(req.Type, req.Password, cmdParam, command)
|
||||
httpReq, err = http.NewRequest(http.MethodPost, req.URL, bytes.NewReader(body))
|
||||
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, FileOpResponse{OK: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("User-Agent", "Mozilla/5.0 (compatible; CyberStrikeAI-WebShell/1.0)")
|
||||
|
||||
resp, err := h.client.Do(httpReq)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, FileOpResponse{OK: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
out, _ := io.ReadAll(resp.Body)
|
||||
output := string(out)
|
||||
|
||||
c.JSON(http.StatusOK, FileOpResponse{
|
||||
OK: resp.StatusCode == http.StatusOK,
|
||||
Output: output,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *WebShellHandler) escapePath(p string) string {
|
||||
if p == "" {
|
||||
return "."
|
||||
}
|
||||
// 简单转义空格与敏感字符,避免命令注入
|
||||
return "'" + strings.ReplaceAll(p, "'", "'\\''") + "'"
|
||||
}
|
||||
|
||||
func (h *WebShellHandler) escapeForEcho(s string) string {
|
||||
// 仅用于 write:base64 写入更安全,这里简单用单引号包裹
|
||||
return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'"
|
||||
}
|
||||
@@ -8495,6 +8495,528 @@ header {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* WebShell 管理页面样式 - 美化版 */
|
||||
.webshell-page-content {
|
||||
height: calc(100vh - 140px);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(165deg, #f0f4f8 0%, #e8eef5 50%, #f5f7fa 100%);
|
||||
}
|
||||
|
||||
.webshell-layout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
gap: 0;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-primary);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.webshell-sidebar {
|
||||
width: 360px;
|
||||
min-width: 260px;
|
||||
max-width: 50%;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(180deg, #fafbfd 0%, #f5f7fa 100%);
|
||||
}
|
||||
|
||||
/* 可拖拽调整连接列表宽度 */
|
||||
.webshell-resize-handle {
|
||||
position: relative;
|
||||
width: 6px;
|
||||
min-width: 6px;
|
||||
flex-shrink: 0;
|
||||
cursor: col-resize;
|
||||
background: var(--border-color);
|
||||
transition: background 0.15s ease;
|
||||
user-select: none;
|
||||
}
|
||||
.webshell-resize-handle:hover,
|
||||
.webshell-resize-handle.active {
|
||||
background: var(--accent-color);
|
||||
}
|
||||
.webshell-resize-handle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 12px;
|
||||
margin-left: -3px;
|
||||
}
|
||||
|
||||
.webshell-sidebar-header {
|
||||
padding: 14px 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-size: 0.95rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.webshell-sidebar-header::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%230066ff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71'/%3E%3Cpath d='M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71'/%3E%3C/svg%3E") center/contain no-repeat;
|
||||
}
|
||||
|
||||
.webshell-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.webshell-empty {
|
||||
padding: 32px 20px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 10px;
|
||||
border: 1px dashed var(--border-color);
|
||||
}
|
||||
|
||||
.webshell-item {
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border-color);
|
||||
transition: all 0.2s ease;
|
||||
background: #fff;
|
||||
box-shadow: var(--shadow-sm);
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.webshell-item:hover {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 2px 8px rgba(0, 102, 255, 0.08);
|
||||
}
|
||||
|
||||
.webshell-item.active {
|
||||
background: linear-gradient(135deg, rgba(0, 102, 255, 0.08) 0%, rgba(0, 102, 255, 0.04) 100%);
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 2px 12px rgba(0, 102, 255, 0.12);
|
||||
}
|
||||
|
||||
.webshell-item-remark {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.9rem;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.webshell-item-url {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-secondary);
|
||||
font-family: ui-monospace, monospace;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.webshell-item-actions {
|
||||
margin-top: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.webshell-delete-btn {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--error-color);
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.webshell-delete-btn:hover {
|
||||
background: rgba(220, 53, 69, 0.08);
|
||||
border-color: rgba(220, 53, 69, 0.3);
|
||||
}
|
||||
|
||||
.webshell-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.webshell-workspace {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 20px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.webshell-workspace-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 280px;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.95rem;
|
||||
text-align: center;
|
||||
padding: 32px;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.8) 0%, rgba(248,249,250,0.9) 100%);
|
||||
border-radius: 12px;
|
||||
border: 1px dashed var(--border-color);
|
||||
}
|
||||
|
||||
.webshell-workspace-placeholder::before {
|
||||
content: '';
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 16px;
|
||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23adb5bd' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' opacity='0.8'%3E%3Cpath d='M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71'/%3E%3Cpath d='M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71'/%3E%3C/svg%3E") center/contain no-repeat;
|
||||
}
|
||||
|
||||
.webshell-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.webshell-tab {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
border-bottom: 3px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
transition: color 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.webshell-tab:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.webshell-tab.active {
|
||||
color: var(--accent-color);
|
||||
border-bottom-color: var(--accent-color);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.webshell-pane {
|
||||
display: none;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.webshell-pane.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* 仅外框圆角,内部不做额外装饰,避免挡住文字 */
|
||||
.webshell-terminal-container {
|
||||
flex: 1;
|
||||
min-height: 360px;
|
||||
padding: 0;
|
||||
background: #0d1117;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.webshell-terminal-container .terminal {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 12px 14px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 防止 xterm 辅助层(.xterm-helper-textarea)遮挡终端文字:移出视口并缩小 */
|
||||
.webshell-terminal-container .xterm .xterm-helper-textarea {
|
||||
left: -9999em !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
min-width: 0 !important;
|
||||
min-height: 0 !important;
|
||||
opacity: 0 !important;
|
||||
position: absolute !important;
|
||||
z-index: -10 !important;
|
||||
}
|
||||
/* 确保光标层不超出单元格,避免整行被蓝条挡住 */
|
||||
.webshell-terminal-container .xterm-screen {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.webshell-terminal-container .xterm-viewport {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* WebShell 终端滚动条:深色主题、细窄、圆角,弱化存在感 */
|
||||
.webshell-terminal-container .xterm-viewport {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(110, 118, 129, 0.5) transparent;
|
||||
}
|
||||
.webshell-terminal-container .xterm-viewport::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.webshell-terminal-container .xterm-viewport::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
margin: 4px 0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.webshell-terminal-container .xterm-viewport::-webkit-scrollbar-thumb {
|
||||
background: rgba(110, 118, 129, 0.4);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.webshell-terminal-container .xterm-viewport::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(110, 118, 129, 0.65);
|
||||
}
|
||||
.webshell-terminal-container .xterm-viewport::-webkit-scrollbar-thumb:active {
|
||||
background: rgba(139, 148, 158, 0.7);
|
||||
}
|
||||
|
||||
.webshell-file-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
padding: 14px 16px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.webshell-file-toolbar label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.webshell-file-toolbar input.form-control {
|
||||
min-width: 200px;
|
||||
flex: 1;
|
||||
max-width: 480px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.webshell-file-toolbar .btn-secondary,
|
||||
.webshell-file-toolbar .btn-ghost {
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.webshell-file-list {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 0;
|
||||
background: var(--bg-primary);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.webshell-file-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.webshell-file-table thead {
|
||||
background: linear-gradient(180deg, #f8f9fa 0%, #f1f3f5 100%);
|
||||
}
|
||||
|
||||
.webshell-file-table th {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.webshell-file-table td {
|
||||
padding: 10px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.webshell-file-table tbody tr:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.webshell-file-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.webshell-file-table a.webshell-file-link {
|
||||
color: var(--accent-color);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.webshell-file-table a.webshell-file-link:hover {
|
||||
background: rgba(0, 102, 255, 0.08);
|
||||
color: var(--accent-hover);
|
||||
}
|
||||
|
||||
.webshell-file-table .webshell-file-read {
|
||||
color: var(--accent-color);
|
||||
margin-right: 8px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.webshell-file-table .webshell-file-read:hover {
|
||||
background: rgba(0, 102, 255, 0.1);
|
||||
}
|
||||
|
||||
.webshell-file-table .webshell-file-del {
|
||||
color: var(--error-color);
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.webshell-file-table .webshell-file-del:hover {
|
||||
background: rgba(220, 53, 69, 0.08);
|
||||
}
|
||||
|
||||
.webshell-loading {
|
||||
padding: 24px 20px;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.webshell-file-error {
|
||||
padding: 20px;
|
||||
color: var(--error-color);
|
||||
background: rgba(220, 53, 69, 0.06);
|
||||
border-radius: 10px;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.webshell-file-raw,
|
||||
.webshell-file-content pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-size: 0.82rem;
|
||||
margin-top: 12px;
|
||||
padding: 16px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.webshell-file-content {
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.webshell-file-content .btn-ghost {
|
||||
margin-top: 12px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.webshell-file-edit-wrap {
|
||||
padding: 16px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.webshell-file-edit-path {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
padding: 8px 12px;
|
||||
font-family: ui-monospace, monospace;
|
||||
word-break: break-all;
|
||||
background: var(--bg-secondary, rgba(0, 0, 0, 0.04));
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
.webshell-file-edit-textarea {
|
||||
width: 100%;
|
||||
min-height: 280px;
|
||||
padding: 14px;
|
||||
font-size: 0.82rem;
|
||||
font-family: ui-monospace, monospace;
|
||||
line-height: 1.5;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.webshell-file-edit-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.15);
|
||||
}
|
||||
.webshell-file-edit-actions {
|
||||
margin-top: 4px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.webshell-file-edit-actions .btn-primary,
|
||||
.webshell-file-edit-actions .btn-ghost {
|
||||
min-width: 72px;
|
||||
}
|
||||
|
||||
/* 仪表盘页面样式(最佳实践布局 + 视觉增强) */
|
||||
.dashboard-page {
|
||||
height: 100%;
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"infoCollect": "Recon",
|
||||
"tasks": "Tasks",
|
||||
"vulnerabilities": "Vulnerabilities",
|
||||
"webshell": "WebShell Management",
|
||||
"mcp": "MCP",
|
||||
"mcpMonitor": "MCP Monitor",
|
||||
"mcpManagement": "MCP Management",
|
||||
@@ -326,6 +327,49 @@
|
||||
"loadFailed": "Failed to load vulnerabilities",
|
||||
"deleteConfirm": "Delete this vulnerability?"
|
||||
},
|
||||
"webshell": {
|
||||
"title": "WebShell Management",
|
||||
"addConnection": "Add connection",
|
||||
"connections": "Connections",
|
||||
"noConnections": "No connections. Click \"Add connection\" to add one.",
|
||||
"selectOrAdd": "Select a connection from the list or add a new WebShell connection.",
|
||||
"url": "Shell URL",
|
||||
"urlPlaceholder": "http(s)://target.com/shell.php",
|
||||
"password": "Password / Key",
|
||||
"passwordPlaceholder": "e.g. IceSword/AntSword connection password",
|
||||
"method": "Request method",
|
||||
"methodPost": "POST",
|
||||
"methodGet": "GET",
|
||||
"type": "Shell type",
|
||||
"typePhp": "PHP",
|
||||
"typeAsp": "ASP",
|
||||
"typeAspx": "ASPX",
|
||||
"typeJsp": "JSP",
|
||||
"typeCustom": "Custom",
|
||||
"cmdParam": "Command parameter name",
|
||||
"cmdParamPlaceholder": "Leave empty for cmd; e.g. xxx for xxx=command",
|
||||
"remark": "Remark",
|
||||
"remarkPlaceholder": "Friendly name for this connection",
|
||||
"deleteConfirm": "Delete this connection?",
|
||||
"editConnection": "Edit",
|
||||
"editConnectionTitle": "Edit connection",
|
||||
"tabTerminal": "Virtual terminal",
|
||||
"tabFileManager": "File manager",
|
||||
"terminalWelcome": "WebShell virtual terminal — type a command and press Enter (Ctrl+L clear)",
|
||||
"filePath": "Current path",
|
||||
"listDir": "List directory",
|
||||
"readFile": "Read",
|
||||
"editFile": "Edit",
|
||||
"deleteFile": "Delete",
|
||||
"saveFile": "Save",
|
||||
"cancelEdit": "Cancel",
|
||||
"parentDir": "Parent directory",
|
||||
"execError": "Execution failed",
|
||||
"testConnectivity": "Test connectivity",
|
||||
"testSuccess": "Connection OK, shell is reachable",
|
||||
"testFailed": "Connectivity test failed",
|
||||
"testNoExpectedOutput": "Shell responded but expected output was not found. Check password and command parameter name."
|
||||
},
|
||||
"mcp": {
|
||||
"monitorTitle": "MCP Status Monitor",
|
||||
"execStats": "Execution stats",
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"infoCollect": "信息收集",
|
||||
"tasks": "任务管理",
|
||||
"vulnerabilities": "漏洞管理",
|
||||
"webshell": "WebShell管理",
|
||||
"mcp": "MCP",
|
||||
"mcpMonitor": "MCP状态监控",
|
||||
"mcpManagement": "MCP管理",
|
||||
@@ -326,6 +327,49 @@
|
||||
"loadFailed": "加载漏洞失败",
|
||||
"deleteConfirm": "确定要删除此漏洞吗?"
|
||||
},
|
||||
"webshell": {
|
||||
"title": "WebShell 管理",
|
||||
"addConnection": "添加连接",
|
||||
"connections": "连接列表",
|
||||
"noConnections": "暂无连接,请点击「添加连接」",
|
||||
"selectOrAdd": "请从左侧选择连接,或添加新的 WebShell 连接",
|
||||
"url": "Shell 地址",
|
||||
"urlPlaceholder": "http(s)://target.com/shell.php",
|
||||
"password": "连接密码/密钥",
|
||||
"passwordPlaceholder": "如冰蝎/蚁剑的连接密码",
|
||||
"method": "请求方式",
|
||||
"methodPost": "POST",
|
||||
"methodGet": "GET",
|
||||
"type": "Shell 类型",
|
||||
"typePhp": "PHP",
|
||||
"typeAsp": "ASP",
|
||||
"typeAspx": "ASPX",
|
||||
"typeJsp": "JSP",
|
||||
"typeCustom": "自定义",
|
||||
"cmdParam": "命令参数名",
|
||||
"cmdParamPlaceholder": "不填默认为 cmd,如填 xxx 则请求为 xxx=命令",
|
||||
"remark": "备注",
|
||||
"remarkPlaceholder": "便于识别的备注名",
|
||||
"deleteConfirm": "确定要删除该连接吗?",
|
||||
"editConnection": "编辑",
|
||||
"editConnectionTitle": "编辑连接",
|
||||
"tabTerminal": "虚拟终端",
|
||||
"tabFileManager": "文件管理",
|
||||
"terminalWelcome": "WebShell 虚拟终端 — 输入命令后按回车执行(Ctrl+L 清屏)",
|
||||
"filePath": "当前路径",
|
||||
"listDir": "列出目录",
|
||||
"readFile": "读取",
|
||||
"editFile": "编辑",
|
||||
"deleteFile": "删除",
|
||||
"saveFile": "保存",
|
||||
"cancelEdit": "取消",
|
||||
"parentDir": "上级目录",
|
||||
"execError": "执行失败",
|
||||
"testConnectivity": "测试连通性",
|
||||
"testSuccess": "连通性正常,Shell 可访问",
|
||||
"testFailed": "连通性测试失败",
|
||||
"testNoExpectedOutput": "Shell 返回了响应但未得到预期输出,请检查连接密码与命令参数名"
|
||||
},
|
||||
"mcp": {
|
||||
"monitorTitle": "MCP 状态监控",
|
||||
"execStats": "执行统计",
|
||||
|
||||
@@ -8,7 +8,7 @@ function initRouter() {
|
||||
if (hash) {
|
||||
const hashParts = hash.split('?');
|
||||
const pageId = hashParts[0];
|
||||
if (pageId && ['dashboard', 'chat', 'info-collect', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings', 'tasks'].includes(pageId)) {
|
||||
if (pageId && ['dashboard', 'chat', 'info-collect', 'vulnerabilities', 'webshell', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings', 'tasks'].includes(pageId)) {
|
||||
switchPage(pageId);
|
||||
|
||||
// 如果是chat页面且带有conversation参数,加载对应对话
|
||||
@@ -293,6 +293,12 @@ function initPage(pageId) {
|
||||
initVulnerabilityPage();
|
||||
}
|
||||
break;
|
||||
case 'webshell':
|
||||
// 初始化 WebShell 管理页面
|
||||
if (typeof initWebshellPage === 'function') {
|
||||
initWebshellPage();
|
||||
}
|
||||
break;
|
||||
case 'settings':
|
||||
// 初始化设置页面(不需要加载工具列表)
|
||||
if (typeof loadConfig === 'function') {
|
||||
@@ -362,7 +368,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const hashParts = hash.split('?');
|
||||
const pageId = hashParts[0];
|
||||
|
||||
if (pageId && ['chat', 'info-collect', 'tasks', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings'].includes(pageId)) {
|
||||
if (pageId && ['chat', 'info-collect', 'tasks', 'vulnerabilities', 'webshell', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings'].includes(pageId)) {
|
||||
switchPage(pageId);
|
||||
|
||||
// 如果是chat页面且带有conversation参数,加载对应对话
|
||||
|
||||
@@ -0,0 +1,836 @@
|
||||
// WebShell 管理(类似冰蝎/蚁剑:虚拟终端、文件管理、命令执行)
|
||||
|
||||
const WEBSHELL_SIDEBAR_WIDTH_KEY = 'webshell_sidebar_width';
|
||||
const WEBSHELL_DEFAULT_SIDEBAR_WIDTH = 360;
|
||||
const WEBSHELL_PROMPT = 'shell> ';
|
||||
let webshellConnections = [];
|
||||
let currentWebshellId = null;
|
||||
let webshellTerminalInstance = null;
|
||||
let webshellTerminalFitAddon = null;
|
||||
let webshellTerminalResizeObserver = null;
|
||||
let webshellTerminalResizeContainer = null;
|
||||
let webshellCurrentConn = null;
|
||||
let webshellLineBuffer = '';
|
||||
let webshellRunning = false;
|
||||
|
||||
// 从服务端(SQLite)拉取连接列表
|
||||
function getWebshellConnections() {
|
||||
if (typeof apiFetch === 'undefined') {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return apiFetch('/api/webshell/connections', { method: 'GET' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (list) { return Array.isArray(list) ? list : []; })
|
||||
.catch(function (e) {
|
||||
console.warn('读取 WebShell 连接列表失败', e);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
// 从服务端刷新连接列表并重绘侧栏
|
||||
function refreshWebshellConnectionsFromServer() {
|
||||
return getWebshellConnections().then(function (list) {
|
||||
webshellConnections = list;
|
||||
renderWebshellList();
|
||||
return list;
|
||||
});
|
||||
}
|
||||
|
||||
// 使用 wsT 避免与全局 window.t 冲突导致无限递归
|
||||
function wsT(key) {
|
||||
var globalT = typeof window !== 'undefined' ? window.t : null;
|
||||
if (typeof globalT === 'function' && globalT !== wsT) return globalT(key);
|
||||
var fallback = {
|
||||
'webshell.title': 'WebShell 管理',
|
||||
'webshell.addConnection': '添加连接',
|
||||
'webshell.cmdParam': '命令参数名',
|
||||
'webshell.cmdParamPlaceholder': '不填默认为 cmd,如填 xxx 则请求为 xxx=命令',
|
||||
'webshell.connections': '连接列表',
|
||||
'webshell.noConnections': '暂无连接,请点击「添加连接」',
|
||||
'webshell.selectOrAdd': '请从左侧选择连接,或添加新的 WebShell 连接',
|
||||
'webshell.deleteConfirm': '确定要删除该连接吗?',
|
||||
'webshell.editConnection': '编辑',
|
||||
'webshell.editConnectionTitle': '编辑连接',
|
||||
'webshell.tabTerminal': '虚拟终端',
|
||||
'webshell.tabFileManager': '文件管理',
|
||||
'webshell.terminalWelcome': 'WebShell 虚拟终端 — 输入命令后按回车执行(Ctrl+L 清屏)',
|
||||
'webshell.filePath': '当前路径',
|
||||
'webshell.listDir': '列出目录',
|
||||
'webshell.readFile': '读取',
|
||||
'webshell.editFile': '编辑',
|
||||
'webshell.deleteFile': '删除',
|
||||
'webshell.saveFile': '保存',
|
||||
'webshell.cancelEdit': '取消',
|
||||
'webshell.parentDir': '上级目录',
|
||||
'webshell.execError': '执行失败',
|
||||
'webshell.testConnectivity': '测试连通性',
|
||||
'webshell.testSuccess': '连通性正常,Shell 可访问',
|
||||
'webshell.testFailed': '连通性测试失败',
|
||||
'webshell.testNoExpectedOutput': 'Shell 返回了响应但未得到预期输出,请检查连接密码与命令参数名',
|
||||
'common.delete': '删除',
|
||||
'common.refresh': '刷新'
|
||||
};
|
||||
return fallback[key] || key;
|
||||
}
|
||||
|
||||
// 初始化 WebShell 管理页面(从 SQLite 拉取连接列表)
|
||||
function initWebshellPage() {
|
||||
destroyWebshellTerminal();
|
||||
webshellCurrentConn = null;
|
||||
currentWebshellId = null;
|
||||
webshellConnections = [];
|
||||
renderWebshellList();
|
||||
applyWebshellSidebarWidth();
|
||||
initWebshellSidebarResize();
|
||||
const workspace = document.getElementById('webshell-workspace');
|
||||
if (workspace) {
|
||||
workspace.innerHTML = '<div class="webshell-workspace-placeholder" data-i18n="webshell.selectOrAdd">' + (wsT('webshell.selectOrAdd')) + '</div>';
|
||||
}
|
||||
getWebshellConnections().then(function (list) {
|
||||
webshellConnections = list;
|
||||
renderWebshellList();
|
||||
});
|
||||
}
|
||||
|
||||
function getWebshellSidebarWidth() {
|
||||
try {
|
||||
const w = parseInt(localStorage.getItem(WEBSHELL_SIDEBAR_WIDTH_KEY), 10);
|
||||
if (!isNaN(w) && w >= 260 && w <= 800) return w;
|
||||
} catch (e) {}
|
||||
return WEBSHELL_DEFAULT_SIDEBAR_WIDTH;
|
||||
}
|
||||
|
||||
function setWebshellSidebarWidth(px) {
|
||||
localStorage.setItem(WEBSHELL_SIDEBAR_WIDTH_KEY, String(px));
|
||||
}
|
||||
|
||||
function applyWebshellSidebarWidth() {
|
||||
const sidebar = document.getElementById('webshell-sidebar');
|
||||
if (sidebar) sidebar.style.width = getWebshellSidebarWidth() + 'px';
|
||||
}
|
||||
|
||||
function initWebshellSidebarResize() {
|
||||
const handle = document.getElementById('webshell-resize-handle');
|
||||
const sidebar = document.getElementById('webshell-sidebar');
|
||||
if (!handle || !sidebar || handle.dataset.resizeBound === '1') return;
|
||||
handle.dataset.resizeBound = '1';
|
||||
let startX = 0, startW = 0;
|
||||
function onMove(e) {
|
||||
const dx = e.clientX - startX;
|
||||
let w = Math.round(startW + dx);
|
||||
const min = 260;
|
||||
const max = Math.min(800, Math.floor((sidebar.parentElement && sidebar.parentElement.offsetWidth || 800) * 0.6));
|
||||
w = Math.max(min, Math.min(max, w));
|
||||
sidebar.style.width = w + 'px';
|
||||
}
|
||||
function onUp() {
|
||||
handle.classList.remove('active');
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
setWebshellSidebarWidth(parseInt(sidebar.style.width, 10) || WEBSHELL_DEFAULT_SIDEBAR_WIDTH);
|
||||
}
|
||||
handle.addEventListener('mousedown', function (e) {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
startX = e.clientX;
|
||||
startW = sidebar.offsetWidth;
|
||||
handle.classList.add('active');
|
||||
document.body.style.cursor = 'col-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
});
|
||||
}
|
||||
|
||||
// 销毁当前终端实例(切换连接或离开页面时)
|
||||
function destroyWebshellTerminal() {
|
||||
if (webshellTerminalResizeObserver && webshellTerminalResizeContainer) {
|
||||
try { webshellTerminalResizeObserver.unobserve(webshellTerminalResizeContainer); } catch (e) {}
|
||||
webshellTerminalResizeObserver = null;
|
||||
webshellTerminalResizeContainer = null;
|
||||
}
|
||||
if (webshellTerminalInstance) {
|
||||
try {
|
||||
webshellTerminalInstance.dispose();
|
||||
} catch (e) {}
|
||||
webshellTerminalInstance = null;
|
||||
}
|
||||
webshellTerminalFitAddon = null;
|
||||
webshellLineBuffer = '';
|
||||
webshellRunning = false;
|
||||
}
|
||||
|
||||
// 渲染连接列表
|
||||
function renderWebshellList() {
|
||||
const listEl = document.getElementById('webshell-list');
|
||||
if (!listEl) return;
|
||||
|
||||
if (!webshellConnections.length) {
|
||||
listEl.innerHTML = '<div class="webshell-empty" data-i18n="webshell.noConnections">' + (wsT('webshell.noConnections')) + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = webshellConnections.map(conn => {
|
||||
const remark = (conn.remark || conn.url || '').replace(/</g, '<').replace(/>/g, '>');
|
||||
const url = (conn.url || '').replace(/</g, '<').replace(/>/g, '>');
|
||||
const urlTitle = (conn.url || '').replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
||||
const active = currentWebshellId === conn.id ? ' active' : '';
|
||||
const safeId = escapeHtml(conn.id);
|
||||
return (
|
||||
'<div class="webshell-item' + active + '" data-id="' + safeId + '">' +
|
||||
'<div class="webshell-item-remark" title="' + urlTitle + '">' + remark + '</div>' +
|
||||
'<div class="webshell-item-url" title="' + urlTitle + '">' + url + '</div>' +
|
||||
'<div class="webshell-item-actions">' +
|
||||
'<button type="button" class="btn-ghost btn-sm webshell-edit-conn-btn" data-id="' + safeId + '" title="' + wsT('webshell.editConnection') + '">' + wsT('webshell.editConnection') + '</button> ' +
|
||||
'<button type="button" class="btn-ghost btn-sm webshell-delete-btn" data-id="' + safeId + '" title="' + wsT('common.delete') + '">' + wsT('common.delete') + '</button>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}).join('');
|
||||
|
||||
listEl.querySelectorAll('.webshell-item').forEach(el => {
|
||||
el.addEventListener('click', function (e) {
|
||||
if (e.target.closest('.webshell-delete-btn') || e.target.closest('.webshell-edit-conn-btn')) return;
|
||||
selectWebshell(el.getAttribute('data-id'));
|
||||
});
|
||||
});
|
||||
listEl.querySelectorAll('.webshell-edit-conn-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
showEditWebshellModal(btn.getAttribute('data-id'));
|
||||
});
|
||||
});
|
||||
listEl.querySelectorAll('.webshell-delete-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
deleteWebshell(btn.getAttribute('data-id'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
if (!s) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = s;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 选择连接:渲染终端 + 文件管理 Tab,并初始化终端
|
||||
function selectWebshell(id) {
|
||||
currentWebshellId = id;
|
||||
renderWebshellList();
|
||||
const conn = webshellConnections.find(c => c.id === id);
|
||||
const workspace = document.getElementById('webshell-workspace');
|
||||
if (!workspace) return;
|
||||
if (!conn) {
|
||||
workspace.innerHTML = '<div class="webshell-workspace-placeholder">' + wsT('webshell.selectOrAdd') + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
destroyWebshellTerminal();
|
||||
webshellCurrentConn = conn;
|
||||
|
||||
workspace.innerHTML =
|
||||
'<div class="webshell-tabs">' +
|
||||
'<button type="button" class="webshell-tab active" data-tab="terminal">' + wsT('webshell.tabTerminal') + '</button>' +
|
||||
'<button type="button" class="webshell-tab" data-tab="file">' + wsT('webshell.tabFileManager') + '</button>' +
|
||||
'</div>' +
|
||||
'<div id="webshell-pane-terminal" class="webshell-pane active">' +
|
||||
'<div id="webshell-terminal-container" class="webshell-terminal-container"></div>' +
|
||||
'</div>' +
|
||||
'<div id="webshell-pane-file" class="webshell-pane">' +
|
||||
'<div class="webshell-file-toolbar">' +
|
||||
'<label><span>' + wsT('webshell.filePath') + '</span> <input type="text" id="webshell-file-path" class="form-control" value="." /></label>' +
|
||||
'<button type="button" class="btn-secondary" id="webshell-list-dir">' + wsT('webshell.listDir') + '</button>' +
|
||||
'<button type="button" class="btn-ghost" id="webshell-parent-dir">' + wsT('webshell.parentDir') + '</button>' +
|
||||
'</div>' +
|
||||
'<div id="webshell-file-list" class="webshell-file-list"></div>' +
|
||||
'</div>';
|
||||
|
||||
// Tab 切换
|
||||
workspace.querySelectorAll('.webshell-tab').forEach(btn => {
|
||||
btn.addEventListener('click', function () {
|
||||
const tab = btn.getAttribute('data-tab');
|
||||
workspace.querySelectorAll('.webshell-tab').forEach(b => b.classList.remove('active'));
|
||||
workspace.querySelectorAll('.webshell-pane').forEach(p => p.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
const pane = document.getElementById('webshell-pane-' + tab);
|
||||
if (pane) pane.classList.add('active');
|
||||
if (tab === 'terminal' && webshellTerminalInstance && webshellTerminalFitAddon) {
|
||||
try { webshellTerminalFitAddon.fit(); } catch (e) {}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 文件管理:列出目录、上级目录
|
||||
const pathInput = document.getElementById('webshell-file-path');
|
||||
document.getElementById('webshell-list-dir').addEventListener('click', function () {
|
||||
// 点击时用当前连接,编辑保存后立即生效
|
||||
webshellFileListDir(webshellCurrentConn, pathInput ? pathInput.value.trim() || '.' : '.');
|
||||
});
|
||||
document.getElementById('webshell-parent-dir').addEventListener('click', function () {
|
||||
const p = (pathInput && pathInput.value.trim()) || '.';
|
||||
if (p === '.' || p === '/') {
|
||||
pathInput.value = '..';
|
||||
} else {
|
||||
pathInput.value = p.replace(/\/[^/]+$/, '') || '.';
|
||||
}
|
||||
webshellFileListDir(webshellCurrentConn, pathInput.value || '.');
|
||||
});
|
||||
|
||||
initWebshellTerminal(conn);
|
||||
}
|
||||
|
||||
// ---------- 虚拟终端(xterm + 按行执行) ----------
|
||||
function initWebshellTerminal(conn) {
|
||||
const container = document.getElementById('webshell-terminal-container');
|
||||
if (!container || typeof Terminal === 'undefined') {
|
||||
if (container) {
|
||||
container.innerHTML = '<p class="terminal-error">' + escapeHtml('未加载 xterm.js,请刷新页面') + '</p>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const term = new Terminal({
|
||||
cursorBlink: true,
|
||||
cursorStyle: 'underline',
|
||||
fontSize: 13,
|
||||
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
||||
lineHeight: 1.2,
|
||||
scrollback: 2000,
|
||||
theme: {
|
||||
background: '#0d1117',
|
||||
foreground: '#e6edf3',
|
||||
cursor: '#58a6ff',
|
||||
cursorAccent: '#0d1117',
|
||||
selection: 'rgba(88, 166, 255, 0.3)'
|
||||
}
|
||||
});
|
||||
|
||||
let fitAddon = null;
|
||||
if (typeof FitAddon !== 'undefined') {
|
||||
const FitCtor = FitAddon.FitAddon || FitAddon;
|
||||
fitAddon = new FitCtor();
|
||||
term.loadAddon(fitAddon);
|
||||
}
|
||||
|
||||
term.open(container);
|
||||
// 先 fit 再写内容,避免未计算尺寸时光标/画布错位挡住文字
|
||||
try {
|
||||
if (fitAddon) fitAddon.fit();
|
||||
} catch (e) {}
|
||||
// 不再输出欢迎行,避免占用空间、挡住输入
|
||||
term.write(WEBSHELL_PROMPT);
|
||||
|
||||
// 按行写入输出,与系统设置终端 writeOutput 一致,避免 ls 等输出错位
|
||||
function writeWebshellOutput(term, text, isError) {
|
||||
if (!term || !text) return;
|
||||
var s = String(text).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
var lines = s.split('\n');
|
||||
var prefix = isError ? '\x1b[31m' : '';
|
||||
var suffix = isError ? '\x1b[0m' : '';
|
||||
term.write(prefix);
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
term.writeln(lines[i].replace(/\r/g, ''));
|
||||
}
|
||||
term.write(suffix);
|
||||
}
|
||||
|
||||
term.onData(function (data) {
|
||||
// Ctrl+L 清屏
|
||||
if (data === '\x0c') {
|
||||
term.clear();
|
||||
webshellLineBuffer = '';
|
||||
term.write(WEBSHELL_PROMPT);
|
||||
return;
|
||||
}
|
||||
// 回车:发送当前行到后端执行
|
||||
if (data === '\r' || data === '\n') {
|
||||
term.writeln('');
|
||||
const cmd = webshellLineBuffer.trim();
|
||||
webshellLineBuffer = '';
|
||||
if (cmd) {
|
||||
webshellRunning = true;
|
||||
// 执行时用当前连接(编辑保存后 webshellCurrentConn 已更新),避免闭包持有旧 conn
|
||||
execWebshellCommand(webshellCurrentConn, cmd).then(function (out) {
|
||||
webshellRunning = false;
|
||||
if (out && out.length) writeWebshellOutput(term, out, false);
|
||||
term.write(WEBSHELL_PROMPT);
|
||||
}).catch(function (err) {
|
||||
webshellRunning = false;
|
||||
writeWebshellOutput(term, err && err.message ? err.message : wsT('webshell.execError'), true);
|
||||
term.write(WEBSHELL_PROMPT);
|
||||
});
|
||||
} else {
|
||||
term.write(WEBSHELL_PROMPT);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 退格
|
||||
if (data === '\x7f' || data === '\b') {
|
||||
if (webshellLineBuffer.length > 0) {
|
||||
webshellLineBuffer = webshellLineBuffer.slice(0, -1);
|
||||
term.write('\b \b');
|
||||
}
|
||||
return;
|
||||
}
|
||||
webshellLineBuffer += data;
|
||||
term.write(data);
|
||||
});
|
||||
|
||||
webshellTerminalInstance = term;
|
||||
webshellTerminalFitAddon = fitAddon;
|
||||
// 延迟再次 fit,确保容器尺寸稳定后光标与文字不错位
|
||||
setTimeout(function () {
|
||||
try { if (fitAddon) fitAddon.fit(); } catch (e) {}
|
||||
}, 100);
|
||||
// 容器尺寸变化时重新 fit,避免光标/文字被遮挡
|
||||
if (fitAddon && typeof ResizeObserver !== 'undefined' && container) {
|
||||
webshellTerminalResizeContainer = container;
|
||||
webshellTerminalResizeObserver = new ResizeObserver(function () {
|
||||
try { fitAddon.fit(); } catch (e) {}
|
||||
});
|
||||
webshellTerminalResizeObserver.observe(container);
|
||||
}
|
||||
}
|
||||
|
||||
// 调用后端执行命令
|
||||
function execWebshellCommand(conn, command) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (typeof apiFetch === 'undefined') {
|
||||
reject(new Error('apiFetch 未定义'));
|
||||
return;
|
||||
}
|
||||
apiFetch('/api/webshell/exec', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
url: conn.url,
|
||||
password: conn.password || '',
|
||||
type: conn.type || 'php',
|
||||
method: (conn.method || 'post').toLowerCase(),
|
||||
cmd_param: conn.cmdParam || '',
|
||||
command: command
|
||||
})
|
||||
}).then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (data && data.output !== undefined) resolve(data.output || '');
|
||||
else if (data && data.error) reject(new Error(data.error));
|
||||
else resolve('');
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 文件管理 ----------
|
||||
function webshellFileListDir(conn, path) {
|
||||
const listEl = document.getElementById('webshell-file-list');
|
||||
if (!listEl) return;
|
||||
listEl.innerHTML = '<div class="webshell-loading">' + wsT('common.refresh') + '...</div>';
|
||||
|
||||
if (typeof apiFetch === 'undefined') {
|
||||
listEl.innerHTML = '<div class="webshell-file-error">apiFetch 未定义</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
apiFetch('/api/webshell/file', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
url: conn.url,
|
||||
password: conn.password || '',
|
||||
type: conn.type || 'php',
|
||||
method: (conn.method || 'post').toLowerCase(),
|
||||
cmd_param: conn.cmdParam || '',
|
||||
action: 'list',
|
||||
path: path
|
||||
})
|
||||
}).then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (!data.ok && data.error) {
|
||||
listEl.innerHTML = '<div class="webshell-file-error">' + escapeHtml(data.error) + '</div><pre class="webshell-file-raw">' + escapeHtml(data.output || '') + '</pre>';
|
||||
return;
|
||||
}
|
||||
renderFileList(listEl, path, data.output || '', conn);
|
||||
})
|
||||
.catch(function (err) {
|
||||
listEl.innerHTML = '<div class="webshell-file-error">' + escapeHtml(err && err.message ? err.message : wsT('webshell.execError')) + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderFileList(listEl, currentPath, rawOutput, conn) {
|
||||
// 解析 ls -la 风格输出为简单列表(兼容不同格式)
|
||||
const lines = rawOutput.split(/\n/).filter(function (l) { return l.trim(); });
|
||||
const items = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const m = line.match(/\s*(\S+)\s*$/); // 最后一列作为名称
|
||||
const name = m ? m[1].trim() : line.trim();
|
||||
if (name === '.' || name === '..') continue;
|
||||
const isDir = line.startsWith('d') || line.toLowerCase().indexOf('<dir>') !== -1;
|
||||
items.push({ name: name, isDir: isDir, line: line });
|
||||
}
|
||||
|
||||
let html = '';
|
||||
if (items.length === 0 && rawOutput.trim()) {
|
||||
html = '<pre class="webshell-file-raw">' + escapeHtml(rawOutput) + '</pre>';
|
||||
} else {
|
||||
html = '<table class="webshell-file-table"><thead><tr><th>' + wsT('webshell.filePath') + '</th><th></th></tr></thead><tbody>';
|
||||
if (currentPath !== '.' && currentPath !== '') {
|
||||
html += '<tr><td><a href="#" class="webshell-file-link" data-path="' + escapeHtml(currentPath.replace(/\/[^/]+$/, '') || '.') + '" data-isdir="1">..</a></td><td></td></tr>';
|
||||
}
|
||||
items.forEach(function (item) {
|
||||
const pathNext = currentPath === '.' ? item.name : currentPath + '/' + item.name;
|
||||
html += '<tr><td><a href="#" class="webshell-file-link" data-path="' + escapeHtml(pathNext) + '" data-isdir="' + (item.isDir ? '1' : '0') + '">' + escapeHtml(item.name) + (item.isDir ? '/' : '') + '</a></td><td>';
|
||||
if (!item.isDir) {
|
||||
html += '<button type="button" class="btn-ghost btn-sm webshell-file-read" data-path="' + escapeHtml(pathNext) + '">' + wsT('webshell.readFile') + '</button> ';
|
||||
html += '<button type="button" class="btn-ghost btn-sm webshell-file-edit" data-path="' + escapeHtml(pathNext) + '">' + wsT('webshell.editFile') + '</button> ';
|
||||
html += '<button type="button" class="btn-ghost btn-sm webshell-file-del" data-path="' + escapeHtml(pathNext) + '">' + wsT('webshell.deleteFile') + '</button>';
|
||||
}
|
||||
html += '</td></tr>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
}
|
||||
listEl.innerHTML = html;
|
||||
|
||||
listEl.querySelectorAll('.webshell-file-link').forEach(function (a) {
|
||||
a.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const path = a.getAttribute('data-path');
|
||||
const isDir = a.getAttribute('data-isdir') === '1';
|
||||
const pathInput = document.getElementById('webshell-file-path');
|
||||
if (pathInput) pathInput.value = path;
|
||||
if (isDir) webshellFileListDir(webshellCurrentConn, path);
|
||||
else webshellFileRead(webshellCurrentConn, path, listEl);
|
||||
});
|
||||
});
|
||||
listEl.querySelectorAll('.webshell-file-read').forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
webshellFileRead(webshellCurrentConn, btn.getAttribute('data-path'), listEl);
|
||||
});
|
||||
});
|
||||
listEl.querySelectorAll('.webshell-file-edit').forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
webshellFileEdit(webshellCurrentConn, btn.getAttribute('data-path'), listEl);
|
||||
});
|
||||
});
|
||||
listEl.querySelectorAll('.webshell-file-del').forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
if (!confirm(wsT('webshell.deleteConfirm'))) return;
|
||||
webshellFileDelete(webshellCurrentConn, btn.getAttribute('data-path'), function () {
|
||||
webshellFileListDir(webshellCurrentConn, document.getElementById('webshell-file-path').value.trim() || '.');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function webshellFileRead(conn, path, listEl) {
|
||||
if (typeof apiFetch === 'undefined') return;
|
||||
listEl.innerHTML = '<div class="webshell-loading">' + wsT('webshell.readFile') + '...</div>';
|
||||
apiFetch('/api/webshell/file', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: conn.url, password: conn.password || '', type: conn.type || 'php', method: (conn.method || 'post').toLowerCase(), cmd_param: conn.cmdParam || '', action: 'read', path: path })
|
||||
}).then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
const out = (data && data.output) ? data.output : (data.error || '');
|
||||
listEl.innerHTML = '<div class="webshell-file-content"><pre>' + escapeHtml(out) + '</pre><button type="button" class="btn-ghost" onclick="webshellFileListDir(webshellCurrentConn, document.getElementById(\'webshell-file-path\').value.trim() || \'.\')">' + wsT('webshell.listDir') + '</button></div>';
|
||||
})
|
||||
.catch(function (err) {
|
||||
listEl.innerHTML = '<div class="webshell-file-error">' + escapeHtml(err && err.message ? err.message : '') + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function webshellFileEdit(conn, path, listEl) {
|
||||
if (typeof apiFetch === 'undefined') return;
|
||||
listEl.innerHTML = '<div class="webshell-loading">' + wsT('webshell.editFile') + '...</div>';
|
||||
apiFetch('/api/webshell/file', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: conn.url, password: conn.password || '', type: conn.type || 'php', method: (conn.method || 'post').toLowerCase(), cmd_param: conn.cmdParam || '', action: 'read', path: path })
|
||||
}).then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
const content = (data && data.output) ? data.output : (data.error || '');
|
||||
const pathInput = document.getElementById('webshell-file-path');
|
||||
const currentPath = pathInput ? pathInput.value.trim() || '.' : '.';
|
||||
listEl.innerHTML =
|
||||
'<div class="webshell-file-edit-wrap">' +
|
||||
'<div class="webshell-file-edit-path">' + escapeHtml(path) + '</div>' +
|
||||
'<textarea id="webshell-edit-textarea" class="webshell-file-edit-textarea" rows="18">' + escapeHtml(content) + '</textarea>' +
|
||||
'<div class="webshell-file-edit-actions">' +
|
||||
'<button type="button" class="btn-primary btn-sm" id="webshell-edit-save">' + wsT('webshell.saveFile') + '</button> ' +
|
||||
'<button type="button" class="btn-ghost btn-sm" id="webshell-edit-cancel">' + wsT('webshell.cancelEdit') + '</button>' +
|
||||
'</div></div>';
|
||||
document.getElementById('webshell-edit-save').addEventListener('click', function () {
|
||||
const textarea = document.getElementById('webshell-edit-textarea');
|
||||
const newContent = textarea ? textarea.value : '';
|
||||
webshellFileWrite(webshellCurrentConn, path, newContent, function () {
|
||||
webshellFileListDir(webshellCurrentConn, currentPath);
|
||||
}, listEl);
|
||||
});
|
||||
document.getElementById('webshell-edit-cancel').addEventListener('click', function () {
|
||||
webshellFileListDir(webshellCurrentConn, currentPath);
|
||||
});
|
||||
})
|
||||
.catch(function (err) {
|
||||
listEl.innerHTML = '<div class="webshell-file-error">' + escapeHtml(err && err.message ? err.message : '') + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function webshellFileWrite(conn, path, content, onDone, listEl) {
|
||||
if (typeof apiFetch === 'undefined') return;
|
||||
if (listEl) listEl.innerHTML = '<div class="webshell-loading">' + wsT('webshell.saveFile') + '...</div>';
|
||||
apiFetch('/api/webshell/file', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: conn.url, password: conn.password || '', type: conn.type || 'php', method: (conn.method || 'post').toLowerCase(), cmd_param: conn.cmdParam || '', action: 'write', path: path, content: content })
|
||||
}).then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (data && !data.ok && data.error && listEl) {
|
||||
listEl.innerHTML = '<div class="webshell-file-error">' + escapeHtml(data.error) + '</div><pre class="webshell-file-raw">' + escapeHtml(data.output || '') + '</pre>';
|
||||
return;
|
||||
}
|
||||
if (onDone) onDone();
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (listEl) listEl.innerHTML = '<div class="webshell-file-error">' + escapeHtml(err && err.message ? err.message : wsT('webshell.execError')) + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function webshellFileDelete(conn, path, onDone) {
|
||||
if (typeof apiFetch === 'undefined') return;
|
||||
apiFetch('/api/webshell/file', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: conn.url, password: conn.password || '', type: conn.type || 'php', method: (conn.method || 'post').toLowerCase(), cmd_param: conn.cmdParam || '', action: 'delete', path: path })
|
||||
}).then(function (r) { return r.json(); })
|
||||
.then(function () { if (onDone) onDone(); })
|
||||
.catch(function () { if (onDone) onDone(); });
|
||||
}
|
||||
|
||||
// 删除连接(请求服务端删除后刷新列表)
|
||||
function deleteWebshell(id) {
|
||||
if (!confirm(wsT('webshell.deleteConfirm'))) return;
|
||||
if (currentWebshellId === id) destroyWebshellTerminal();
|
||||
if (currentWebshellId === id) currentWebshellId = null;
|
||||
if (typeof apiFetch === 'undefined') return;
|
||||
apiFetch('/api/webshell/connections/' + encodeURIComponent(id), { method: 'DELETE' })
|
||||
.then(function () {
|
||||
return refreshWebshellConnectionsFromServer();
|
||||
})
|
||||
.then(function () {
|
||||
const workspace = document.getElementById('webshell-workspace');
|
||||
if (workspace) {
|
||||
workspace.innerHTML = '<div class="webshell-workspace-placeholder">' + wsT('webshell.selectOrAdd') + '</div>';
|
||||
}
|
||||
})
|
||||
.catch(function (e) {
|
||||
console.warn('删除 WebShell 连接失败', e);
|
||||
refreshWebshellConnectionsFromServer();
|
||||
});
|
||||
}
|
||||
|
||||
// 打开添加连接弹窗
|
||||
function showAddWebshellModal() {
|
||||
var editIdEl = document.getElementById('webshell-edit-id');
|
||||
if (editIdEl) editIdEl.value = '';
|
||||
document.getElementById('webshell-url').value = '';
|
||||
document.getElementById('webshell-password').value = '';
|
||||
document.getElementById('webshell-type').value = 'php';
|
||||
document.getElementById('webshell-method').value = 'post';
|
||||
document.getElementById('webshell-cmd-param').value = '';
|
||||
document.getElementById('webshell-remark').value = '';
|
||||
var titleEl = document.getElementById('webshell-modal-title');
|
||||
if (titleEl) titleEl.textContent = wsT('webshell.addConnection');
|
||||
var modal = document.getElementById('webshell-modal');
|
||||
if (modal) modal.style.display = 'block';
|
||||
}
|
||||
|
||||
// 打开编辑连接弹窗(预填当前连接信息)
|
||||
function showEditWebshellModal(connId) {
|
||||
var conn = webshellConnections.find(function (c) { return c.id === connId; });
|
||||
if (!conn) return;
|
||||
var editIdEl = document.getElementById('webshell-edit-id');
|
||||
if (editIdEl) editIdEl.value = conn.id;
|
||||
document.getElementById('webshell-url').value = conn.url || '';
|
||||
document.getElementById('webshell-password').value = conn.password || '';
|
||||
document.getElementById('webshell-type').value = conn.type || 'php';
|
||||
document.getElementById('webshell-method').value = (conn.method || 'post').toLowerCase();
|
||||
document.getElementById('webshell-cmd-param').value = conn.cmdParam || '';
|
||||
document.getElementById('webshell-remark').value = conn.remark || '';
|
||||
var titleEl = document.getElementById('webshell-modal-title');
|
||||
if (titleEl) titleEl.textContent = wsT('webshell.editConnectionTitle');
|
||||
var modal = document.getElementById('webshell-modal');
|
||||
if (modal) modal.style.display = 'block';
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function closeWebshellModal() {
|
||||
var editIdEl = document.getElementById('webshell-edit-id');
|
||||
if (editIdEl) editIdEl.value = '';
|
||||
var modal = document.getElementById('webshell-modal');
|
||||
if (modal) modal.style.display = 'none';
|
||||
}
|
||||
|
||||
// 语言切换时刷新 WebShell 页面内所有由 JS 生成的文案(不重建终端)
|
||||
function refreshWebshellUIOnLanguageChange() {
|
||||
var page = typeof window.currentPage === 'function' ? window.currentPage() : (window.currentPage || '');
|
||||
if (page !== 'webshell') return;
|
||||
|
||||
renderWebshellList();
|
||||
|
||||
var workspace = document.getElementById('webshell-workspace');
|
||||
if (workspace) {
|
||||
if (!currentWebshellId || !webshellCurrentConn) {
|
||||
workspace.innerHTML = '<div class="webshell-workspace-placeholder" data-i18n="webshell.selectOrAdd">' + wsT('webshell.selectOrAdd') + '</div>';
|
||||
} else {
|
||||
// 只更新标签文案,不重建终端
|
||||
var tabTerminal = workspace.querySelector('.webshell-tab[data-tab="terminal"]');
|
||||
var tabFile = workspace.querySelector('.webshell-tab[data-tab="file"]');
|
||||
if (tabTerminal) tabTerminal.textContent = wsT('webshell.tabTerminal');
|
||||
if (tabFile) tabFile.textContent = wsT('webshell.tabFileManager');
|
||||
|
||||
var pathLabel = workspace.querySelector('.webshell-file-toolbar label span');
|
||||
var listDirBtn = document.getElementById('webshell-list-dir');
|
||||
var parentDirBtn = document.getElementById('webshell-parent-dir');
|
||||
if (pathLabel) pathLabel.textContent = wsT('webshell.filePath');
|
||||
if (listDirBtn) listDirBtn.textContent = wsT('webshell.listDir');
|
||||
if (parentDirBtn) parentDirBtn.textContent = wsT('webshell.parentDir');
|
||||
|
||||
var pathInput = document.getElementById('webshell-file-path');
|
||||
var fileListEl = document.getElementById('webshell-file-list');
|
||||
if (fileListEl && webshellCurrentConn && pathInput) {
|
||||
webshellFileListDir(webshellCurrentConn, pathInput.value.trim() || '.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var modal = document.getElementById('webshell-modal');
|
||||
if (modal && modal.style.display === 'block') {
|
||||
var titleEl = document.getElementById('webshell-modal-title');
|
||||
var editIdEl = document.getElementById('webshell-edit-id');
|
||||
if (titleEl) {
|
||||
titleEl.textContent = (editIdEl && editIdEl.value) ? wsT('webshell.editConnectionTitle') : wsT('webshell.addConnection');
|
||||
}
|
||||
if (typeof window.applyTranslations === 'function') {
|
||||
window.applyTranslations(modal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('languagechange', function () {
|
||||
refreshWebshellUIOnLanguageChange();
|
||||
});
|
||||
|
||||
// 测试连通性(不保存,仅用当前表单参数请求 Shell 执行 echo 1)
|
||||
function testWebshellConnection() {
|
||||
var url = (document.getElementById('webshell-url') || {}).value;
|
||||
if (url && typeof url.trim === 'function') url = url.trim();
|
||||
if (!url) {
|
||||
alert(wsT('webshell.url') ? (wsT('webshell.url') + ' 必填') : '请填写 Shell 地址');
|
||||
return;
|
||||
}
|
||||
var password = (document.getElementById('webshell-password') || {}).value;
|
||||
if (password && typeof password.trim === 'function') password = password.trim(); else password = '';
|
||||
var type = (document.getElementById('webshell-type') || {}).value || 'php';
|
||||
var method = ((document.getElementById('webshell-method') || {}).value || 'post').toLowerCase();
|
||||
var cmdParam = (document.getElementById('webshell-cmd-param') || {}).value;
|
||||
if (cmdParam && typeof cmdParam.trim === 'function') cmdParam = cmdParam.trim(); else cmdParam = '';
|
||||
var btn = document.getElementById('webshell-test-btn');
|
||||
if (btn) { btn.disabled = true; btn.textContent = (typeof wsT === 'function' ? wsT('common.refresh') : '刷新') + '...'; }
|
||||
if (typeof apiFetch === 'undefined') {
|
||||
if (btn) { btn.disabled = false; btn.textContent = wsT('webshell.testConnectivity'); }
|
||||
alert(wsT('webshell.testFailed') || '连通性测试失败');
|
||||
return;
|
||||
}
|
||||
apiFetch('/api/webshell/exec', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
url: url,
|
||||
password: password || '',
|
||||
type: type,
|
||||
method: method === 'get' ? 'get' : 'post',
|
||||
cmd_param: cmdParam || '',
|
||||
command: 'echo 1'
|
||||
})
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (btn) { btn.disabled = false; btn.textContent = wsT('webshell.testConnectivity'); }
|
||||
if (!data) {
|
||||
alert(wsT('webshell.testFailed') || '连通性测试失败');
|
||||
return;
|
||||
}
|
||||
// 仅 HTTP 200 不算通过,需校验是否真的执行了 echo 1(响应体 trim 后应为 "1")
|
||||
var output = (data.output != null) ? String(data.output).trim() : '';
|
||||
var reallyOk = data.ok && output === '1';
|
||||
if (reallyOk) {
|
||||
alert(wsT('webshell.testSuccess') || '连通性正常,Shell 可访问');
|
||||
} else {
|
||||
var msg;
|
||||
if (data.ok && output !== '1')
|
||||
msg = wsT('webshell.testNoExpectedOutput') || 'Shell 返回了响应但未得到预期输出,请检查连接密码与命令参数名';
|
||||
else
|
||||
msg = (data.error) ? data.error : (wsT('webshell.testFailed') || '连通性测试失败');
|
||||
if (data.http_code) msg += ' (HTTP ' + data.http_code + ')';
|
||||
alert(msg);
|
||||
}
|
||||
})
|
||||
.catch(function (e) {
|
||||
if (btn) { btn.disabled = false; btn.textContent = wsT('webshell.testConnectivity'); }
|
||||
alert((wsT('webshell.testFailed') || '连通性测试失败') + ': ' + (e && e.message ? e.message : String(e)));
|
||||
});
|
||||
}
|
||||
|
||||
// 保存连接(新建或更新,请求服务端写入 SQLite 后刷新列表)
|
||||
function saveWebshellConnection() {
|
||||
var url = (document.getElementById('webshell-url') || {}).value;
|
||||
if (url && typeof url.trim === 'function') url = url.trim();
|
||||
if (!url) {
|
||||
alert('请填写 Shell 地址');
|
||||
return;
|
||||
}
|
||||
var password = (document.getElementById('webshell-password') || {}).value;
|
||||
if (password && typeof password.trim === 'function') password = password.trim(); else password = '';
|
||||
var type = (document.getElementById('webshell-type') || {}).value || 'php';
|
||||
var method = ((document.getElementById('webshell-method') || {}).value || 'post').toLowerCase();
|
||||
var cmdParam = (document.getElementById('webshell-cmd-param') || {}).value;
|
||||
if (cmdParam && typeof cmdParam.trim === 'function') cmdParam = cmdParam.trim(); else cmdParam = '';
|
||||
var remark = (document.getElementById('webshell-remark') || {}).value;
|
||||
if (remark && typeof remark.trim === 'function') remark = remark.trim(); else remark = '';
|
||||
|
||||
var editIdEl = document.getElementById('webshell-edit-id');
|
||||
var editId = editIdEl ? editIdEl.value.trim() : '';
|
||||
var body = { url: url, password: password, type: type, method: method === 'get' ? 'get' : 'post', cmd_param: cmdParam, remark: remark || url };
|
||||
if (typeof apiFetch === 'undefined') return;
|
||||
|
||||
var reqUrl = editId ? ('/api/webshell/connections/' + encodeURIComponent(editId)) : '/api/webshell/connections';
|
||||
var reqMethod = editId ? 'PUT' : 'POST';
|
||||
apiFetch(reqUrl, {
|
||||
method: reqMethod,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function () {
|
||||
closeWebshellModal();
|
||||
return refreshWebshellConnectionsFromServer();
|
||||
})
|
||||
.then(function (list) {
|
||||
// 若编辑的是当前选中的连接,同步更新 webshellCurrentConn,使终端/文件管理立即使用新配置
|
||||
if (editId && currentWebshellId === editId && Array.isArray(list)) {
|
||||
var updated = list.find(function (c) { return c.id === editId; });
|
||||
if (updated) webshellCurrentConn = updated;
|
||||
}
|
||||
})
|
||||
.catch(function (e) {
|
||||
console.warn('保存 WebShell 连接失败', e);
|
||||
alert(e && e.message ? e.message : '保存失败');
|
||||
});
|
||||
}
|
||||
@@ -135,6 +135,15 @@
|
||||
<span data-i18n="nav.vulnerabilities">漏洞管理</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item" data-page="webshell">
|
||||
<div class="nav-item-content" data-title="WebShell管理" onclick="switchPage('webshell')" data-i18n="nav.webshell" data-i18n-attr="data-title" data-i18n-skip-text="true">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="4 17 10 11 4 5"></polyline>
|
||||
<line x1="12" y1="19" x2="20" y2="19"></line>
|
||||
</svg>
|
||||
<span data-i18n="nav.webshell">WebShell管理</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item nav-item-has-submenu" data-page="mcp">
|
||||
<div class="nav-item-content" data-title="MCP" onclick="toggleSubmenu('mcp')" data-i18n="nav.mcp" data-i18n-attr="data-title" data-i18n-skip-text="true">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -951,6 +960,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WebShell 管理页面 -->
|
||||
<div id="page-webshell" class="page">
|
||||
<div class="page-header">
|
||||
<h2 data-i18n="webshell.title">WebShell 管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-primary" onclick="showAddWebshellModal()" data-i18n="webshell.addConnection">添加连接</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content webshell-page-content">
|
||||
<div class="webshell-layout">
|
||||
<div id="webshell-sidebar" class="webshell-sidebar">
|
||||
<div class="webshell-sidebar-header" data-i18n="webshell.connections">连接列表</div>
|
||||
<div id="webshell-list" class="webshell-list">
|
||||
<div class="webshell-empty" data-i18n="webshell.noConnections">暂无连接,请点击「添加连接」</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="webshell-resize-handle" class="webshell-resize-handle" title="拖拽调整宽度"></div>
|
||||
<div class="webshell-main">
|
||||
<div id="webshell-workspace" class="webshell-workspace">
|
||||
<div class="webshell-workspace-placeholder" data-i18n="webshell.selectOrAdd">请从左侧选择连接,或添加新的 WebShell 连接</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务管理页面 -->
|
||||
<div id="page-tasks" class="page">
|
||||
<div class="page-header">
|
||||
@@ -2096,6 +2131,57 @@ version: 1.0.0<br>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WebShell 添加连接模态框 -->
|
||||
<div id="webshell-modal" class="modal">
|
||||
<div class="modal-content" style="max-width: 560px;">
|
||||
<div class="modal-header">
|
||||
<h2 id="webshell-modal-title" data-i18n="webshell.addConnection">添加连接</h2>
|
||||
<span class="modal-close" onclick="closeWebshellModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="webshell-edit-id" value="" />
|
||||
<div class="form-group">
|
||||
<label for="webshell-url"><span data-i18n="webshell.url">Shell 地址</span> <span style="color: red;">*</span></label>
|
||||
<input type="text" id="webshell-url" data-i18n="webshell.urlPlaceholder" data-i18n-attr="placeholder" placeholder="http(s)://target.com/shell.php" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="webshell-password" data-i18n="webshell.password">连接密码/密钥</label>
|
||||
<input type="text" id="webshell-password" data-i18n="webshell.passwordPlaceholder" data-i18n-attr="placeholder" placeholder="如冰蝎/蚁剑的连接密码" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="webshell-type" data-i18n="webshell.type">Shell 类型</label>
|
||||
<select id="webshell-type">
|
||||
<option value="php" data-i18n="webshell.typePhp">PHP</option>
|
||||
<option value="asp" data-i18n="webshell.typeAsp">ASP</option>
|
||||
<option value="aspx" data-i18n="webshell.typeAspx">ASPX</option>
|
||||
<option value="jsp" data-i18n="webshell.typeJsp">JSP</option>
|
||||
<option value="custom" data-i18n="webshell.typeCustom">自定义</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="webshell-method" data-i18n="webshell.method">请求方式</label>
|
||||
<select id="webshell-method">
|
||||
<option value="post" data-i18n="webshell.methodPost">POST</option>
|
||||
<option value="get" data-i18n="webshell.methodGet">GET</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="webshell-cmd-param" data-i18n="webshell.cmdParam">命令参数名</label>
|
||||
<input type="text" id="webshell-cmd-param" data-i18n="webshell.cmdParamPlaceholder" data-i18n-attr="placeholder" placeholder="不填默认为 cmd,如 xxx 则请求为 xxx=命令" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="webshell-remark" data-i18n="webshell.remark">备注</label>
|
||||
<input type="text" id="webshell-remark" data-i18n="webshell.remarkPlaceholder" data-i18n-attr="placeholder" placeholder="便于识别的备注名" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-secondary" id="webshell-test-btn" onclick="testWebshellConnection()" data-i18n="webshell.testConnectivity">测试连通性</button>
|
||||
<button class="btn-secondary" onclick="closeWebshellModal()" data-i18n="common.cancel">取消</button>
|
||||
<button class="btn-primary" onclick="saveWebshellConnection()" data-i18n="common.save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 角色选择弹窗 -->
|
||||
<div id="role-select-modal" class="modal">
|
||||
<div class="modal-content role-select-modal-content">
|
||||
@@ -2227,6 +2313,7 @@ version: 1.0.0<br>
|
||||
<script src="/static/js/knowledge.js"></script>
|
||||
<script src="/static/js/skills.js"></script>
|
||||
<script src="/static/js/vulnerability.js?v=4"></script>
|
||||
<script src="/static/js/webshell.js"></script>
|
||||
<script src="/static/js/tasks.js"></script>
|
||||
<script src="/static/js/roles.js"></script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user