- Deleted version comparison logic from `pkg/plugin/sign/version.go`. - Removed unused types and constants from `pkg/plugin/types.go`. - Updated `pkg/protocol/message.go` to remove plugin-related message types. - Enhanced `pkg/proxy/http.go` and `pkg/proxy/socks5.go` to include username/password authentication for HTTP and SOCKS5 proxies. - Modified `pkg/proxy/server.go` to pass authentication parameters to server constructors. - Added new API endpoint to generate installation commands with a token for clients. - Created database functions to manage installation tokens in `internal/server/db/install_token.go`. - Implemented the installation command generation logic in `internal/server/router/handler/install.go`. - Updated web frontend to support installation command generation and display in `web/src/views/ClientsView.vue`.
343 lines
8.3 KiB
Go
343 lines
8.3 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gotunnel/internal/server/db"
|
|
"github.com/gotunnel/internal/server/router/dto"
|
|
)
|
|
|
|
// ClientHandler 客户端处理器
|
|
type ClientHandler struct {
|
|
app AppInterface
|
|
}
|
|
|
|
// NewClientHandler 创建客户端处理器
|
|
func NewClientHandler(app AppInterface) *ClientHandler {
|
|
return &ClientHandler{app: app}
|
|
}
|
|
|
|
// List 获取客户端列表
|
|
// @Summary 获取所有客户端
|
|
// @Description 返回所有注册客户端的列表及其在线状态
|
|
// @Tags 客户端
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Success 200 {object} Response{data=[]dto.ClientListItem}
|
|
// @Router /api/clients [get]
|
|
func (h *ClientHandler) List(c *gin.Context) {
|
|
clients, err := h.app.GetClientStore().GetAllClients()
|
|
if err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
statusMap := h.app.GetServer().GetAllClientStatus()
|
|
result := make([]dto.ClientListItem, 0, len(clients))
|
|
|
|
for _, client := range clients {
|
|
item := dto.ClientListItem{
|
|
ID: client.ID,
|
|
Nickname: client.Nickname,
|
|
RuleCount: len(client.Rules),
|
|
}
|
|
if status, ok := statusMap[client.ID]; ok {
|
|
item.Online = status.Online
|
|
item.LastPing = status.LastPing
|
|
item.RemoteAddr = status.RemoteAddr
|
|
item.OS = status.OS
|
|
item.Arch = status.Arch
|
|
}
|
|
result = append(result, item)
|
|
}
|
|
|
|
Success(c, result)
|
|
}
|
|
|
|
// Create 创建客户端
|
|
// @Summary 创建新客户端
|
|
// @Description 创建一个新的客户端配置
|
|
// @Tags 客户端
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param request body dto.CreateClientRequest true "客户端信息"
|
|
// @Success 200 {object} Response
|
|
// @Failure 400 {object} Response
|
|
// @Failure 409 {object} Response
|
|
// @Router /api/clients [post]
|
|
func (h *ClientHandler) Create(c *gin.Context) {
|
|
var req dto.CreateClientRequest
|
|
if !BindJSON(c, &req) {
|
|
return
|
|
}
|
|
|
|
// 验证客户端 ID 格式
|
|
if !validateClientID(req.ID) {
|
|
BadRequest(c, "invalid client id: must be 1-64 alphanumeric characters, underscore or hyphen")
|
|
return
|
|
}
|
|
|
|
// 检查客户端是否已存在
|
|
exists, _ := h.app.GetClientStore().ClientExists(req.ID)
|
|
if exists {
|
|
Conflict(c, "client already exists")
|
|
return
|
|
}
|
|
|
|
client := &db.Client{ID: req.ID, Rules: req.Rules}
|
|
if err := h.app.GetClientStore().CreateClient(client); err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
Success(c, gin.H{"status": "ok"})
|
|
}
|
|
|
|
// Get 获取单个客户端
|
|
// @Summary 获取客户端详情
|
|
// @Description 获取指定客户端的详细信息
|
|
// @Tags 客户端
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path string true "客户端ID"
|
|
// @Success 200 {object} Response{data=dto.ClientResponse}
|
|
// @Failure 404 {object} Response
|
|
// @Router /api/client/{id} [get]
|
|
func (h *ClientHandler) Get(c *gin.Context) {
|
|
clientID := c.Param("id")
|
|
|
|
client, err := h.app.GetClientStore().GetClient(clientID)
|
|
if err != nil {
|
|
NotFound(c, "client not found")
|
|
return
|
|
}
|
|
|
|
online, lastPing, remoteAddr, clientName, clientOS, clientArch, clientVersion := h.app.GetServer().GetClientStatus(clientID)
|
|
|
|
// 如果客户端在线且有名称,优先使用在线名称
|
|
nickname := client.Nickname
|
|
if online && clientName != "" && nickname == "" {
|
|
nickname = clientName
|
|
}
|
|
|
|
resp := dto.ClientResponse{
|
|
ID: client.ID,
|
|
Nickname: nickname,
|
|
Rules: client.Rules,
|
|
Online: online,
|
|
LastPing: lastPing,
|
|
RemoteAddr: remoteAddr,
|
|
OS: clientOS,
|
|
Arch: clientArch,
|
|
Version: clientVersion,
|
|
}
|
|
|
|
Success(c, resp)
|
|
}
|
|
|
|
// Update 更新客户端
|
|
// @Summary 更新客户端配置
|
|
// @Description 更新指定客户端的配置信息
|
|
// @Tags 客户端
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path string true "客户端ID"
|
|
// @Param request body dto.UpdateClientRequest true "更新内容"
|
|
// @Success 200 {object} Response
|
|
// @Failure 400 {object} Response
|
|
// @Failure 404 {object} Response
|
|
// @Router /api/client/{id} [put]
|
|
func (h *ClientHandler) Update(c *gin.Context) {
|
|
clientID := c.Param("id")
|
|
|
|
var req dto.UpdateClientRequest
|
|
if !BindJSON(c, &req) {
|
|
return
|
|
}
|
|
|
|
client, err := h.app.GetClientStore().GetClient(clientID)
|
|
if err != nil {
|
|
NotFound(c, "client not found")
|
|
return
|
|
}
|
|
|
|
client.Nickname = req.Nickname
|
|
client.Rules = req.Rules
|
|
|
|
if err := h.app.GetClientStore().UpdateClient(client); err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
Success(c, gin.H{"status": "ok"})
|
|
}
|
|
|
|
// Delete 删除客户端
|
|
// @Summary 删除客户端
|
|
// @Description 删除指定的客户端配置
|
|
// @Tags 客户端
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path string true "客户端ID"
|
|
// @Success 200 {object} Response
|
|
// @Failure 404 {object} Response
|
|
// @Router /api/client/{id} [delete]
|
|
func (h *ClientHandler) Delete(c *gin.Context) {
|
|
clientID := c.Param("id")
|
|
|
|
exists, _ := h.app.GetClientStore().ClientExists(clientID)
|
|
if !exists {
|
|
NotFound(c, "client not found")
|
|
return
|
|
}
|
|
|
|
if err := h.app.GetClientStore().DeleteClient(clientID); err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
Success(c, gin.H{"status": "ok"})
|
|
}
|
|
|
|
// PushConfig 推送配置到客户端
|
|
// @Summary 推送配置
|
|
// @Description 将配置推送到在线客户端
|
|
// @Tags 客户端
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path string true "客户端ID"
|
|
// @Success 200 {object} Response
|
|
// @Failure 400 {object} Response
|
|
// @Router /api/client/{id}/push [post]
|
|
func (h *ClientHandler) PushConfig(c *gin.Context) {
|
|
clientID := c.Param("id")
|
|
|
|
if !h.app.GetServer().IsClientOnline(clientID) {
|
|
ClientNotOnline(c)
|
|
return
|
|
}
|
|
|
|
if err := h.app.GetServer().PushConfigToClient(clientID); err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
Success(c, gin.H{"status": "ok"})
|
|
}
|
|
|
|
// Disconnect 断开客户端连接
|
|
// @Summary 断开连接
|
|
// @Description 强制断开客户端连接
|
|
// @Tags 客户端
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path string true "客户端ID"
|
|
// @Success 200 {object} Response
|
|
// @Router /api/client/{id}/disconnect [post]
|
|
func (h *ClientHandler) Disconnect(c *gin.Context) {
|
|
clientID := c.Param("id")
|
|
|
|
if err := h.app.GetServer().DisconnectClient(clientID); err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
Success(c, gin.H{"status": "ok"})
|
|
}
|
|
|
|
// Restart 重启客户端
|
|
// @Summary 重启客户端
|
|
// @Description 发送重启命令到客户端
|
|
// @Tags 客户端
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path string true "客户端ID"
|
|
// @Success 200 {object} Response
|
|
// @Router /api/client/{id}/restart [post]
|
|
func (h *ClientHandler) Restart(c *gin.Context) {
|
|
clientID := c.Param("id")
|
|
|
|
if err := h.app.GetServer().RestartClient(clientID); err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
SuccessWithMessage(c, gin.H{"status": "ok"}, "client restart initiated")
|
|
}
|
|
|
|
// @Failure 400 {object} Response
|
|
// @Router /api/client/{id}/install-plugins [post]
|
|
|
|
// @Failure 400 {object} Response
|
|
// @Router /api/client/{id}/plugin/{pluginID}/{action} [post]
|
|
|
|
|
|
// GetSystemStats 获取客户端系统状态
|
|
func (h *ClientHandler) GetSystemStats(c *gin.Context) {
|
|
clientID := c.Param("id")
|
|
stats, err := h.app.GetServer().GetClientSystemStats(clientID)
|
|
if err != nil {
|
|
ClientNotOnline(c)
|
|
return
|
|
}
|
|
Success(c, stats)
|
|
}
|
|
|
|
// GetScreenshot 获取客户端截图
|
|
func (h *ClientHandler) GetScreenshot(c *gin.Context) {
|
|
clientID := c.Param("id")
|
|
quality := 0
|
|
if q, ok := c.GetQuery("quality"); ok {
|
|
fmt.Sscanf(q, "%d", &quality)
|
|
}
|
|
|
|
screenshot, err := h.app.GetServer().GetClientScreenshot(clientID, quality)
|
|
if err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
Success(c, screenshot)
|
|
}
|
|
|
|
// ExecuteShellRequest Shell 执行请求体
|
|
type ExecuteShellRequest struct {
|
|
Command string `json:"command" binding:"required"`
|
|
Timeout int `json:"timeout"`
|
|
}
|
|
|
|
// ExecuteShell 执行 Shell 命令
|
|
func (h *ClientHandler) ExecuteShell(c *gin.Context) {
|
|
clientID := c.Param("id")
|
|
var req ExecuteShellRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
result, err := h.app.GetServer().ExecuteClientShell(clientID, req.Command, req.Timeout)
|
|
if err != nil {
|
|
InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
Success(c, result)
|
|
}
|
|
|
|
// validateClientID 验证客户端 ID 格式
|
|
func validateClientID(id string) bool {
|
|
if len(id) < 1 || len(id) > 64 {
|
|
return false
|
|
}
|
|
for _, c := range id {
|
|
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
|
(c >= '0' && c <= '9') || c == '_' || c == '-') {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|