package web import ( "crypto/rand" "encoding/hex" "encoding/json" "fmt" "io" "log" "net/http" "os" "path/filepath" "strconv" "strings" "time" "ftp-server/config" "ftp-server/database" "ftp-server/ftp" "github.com/golang-jwt/jwt/v5" ) // Server Web管理服务器 type Server struct { config *config.Config db *database.DB ftpServer *ftp.Server jwtSecret []byte } // NewServer 创建Web服务器 func NewServer(cfg *config.Config, db *database.DB, ftpSrv *ftp.Server) *Server { secret := make([]byte, 32) rand.Read(secret) return &Server{ config: cfg, db: db, ftpServer: ftpSrv, jwtSecret: secret, } } // Start 启动Web服务 func (s *Server) Start() error { webCfg := s.config.Get().Web mux := http.NewServeMux() // 静态文件 fs := http.FileServer(http.Dir("./static")) mux.Handle("/", fs) // API路由 mux.HandleFunc("/api/login", s.handleLogin) mux.HandleFunc("/api/dashboard", s.authMiddleware(s.handleDashboard)) mux.HandleFunc("/api/users", s.authMiddleware(s.handleUsers)) mux.HandleFunc("/api/users/", s.authMiddleware(s.handleUserOperation)) mux.HandleFunc("/api/files", s.authMiddleware(s.handleFileBrowse)) mux.HandleFunc("/api/files/", s.authMiddleware(s.handleFileBrowse)) mux.HandleFunc("/api/logs", s.authMiddleware(s.handleLogs)) mux.HandleFunc("/api/online", s.authMiddleware(s.handleOnline)) mux.HandleFunc("/api/config", s.authMiddleware(s.handleConfig)) mux.HandleFunc("/api/upload", s.authMiddleware(s.handleUpload)) mux.HandleFunc("/api/ip-rules", s.authMiddleware(s.handleIPRules)) mux.HandleFunc("/api/ip-rules/", s.authMiddleware(s.handleIPRuleOperation)) addr := fmt.Sprintf("%s:%d", webCfg.Host, webCfg.Port) log.Printf("Web管理界面已启动: http://localhost:%d", webCfg.Port) server := &http.Server{ Addr: addr, Handler: mux, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, } return server.ListenAndServe() } // --- 中间件 --- func (s *Server) authMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { tokenStr := r.Header.Get("Authorization") if tokenStr == "" { // 也支持 cookie if cookie, err := r.Cookie("token"); err == nil { tokenStr = cookie.Value } } if strings.HasPrefix(tokenStr, "Bearer ") { tokenStr = tokenStr[7:] } if tokenStr == "" { s.jsonError(w, "未登录", http.StatusUnauthorized) return } token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("无效的签名方法") } return s.jwtSecret, nil }) if err != nil || !token.Valid { s.jsonError(w, "登录已过期", http.StatusUnauthorized) return } next(w, r) } } // --- 请求处理 --- // handleLogin 登录 func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed) return } var req struct { Username string `json:"username"` Password string `json:"password"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.jsonError(w, "请求格式错误", http.StatusBadRequest) return } adminCfg := s.config.Get().Admin if req.Username != adminCfg.Username || req.Password != adminCfg.Password { s.jsonError(w, "用户名或密码错误", http.StatusUnauthorized) return } token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "username": req.Username, "exp": time.Now().Add(24 * time.Hour).Unix(), }) tokenStr, err := token.SignedString(s.jwtSecret) if err != nil { s.jsonError(w, "生成令牌失败", http.StatusInternalServerError) return } s.jsonResponse(w, http.StatusOK, map[string]interface{}{ "token": tokenStr, }) } // handleDashboard 仪表盘 func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { stats, err := s.db.GetLogStats() if err != nil { s.jsonError(w, err.Error(), http.StatusInternalServerError) return } // 添加在线用户数 if s.ftpServer != nil { online := s.ftpServer.GetOnlineUsers() stats["online_users"] = len(online) } else { stats["online_users"] = 0 } s.jsonResponse(w, http.StatusOK, stats) } // handleUsers 用户管理 func (s *Server) handleUsers(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: users, err := s.db.ListUsers() if err != nil { s.jsonError(w, err.Error(), http.StatusInternalServerError) return } if users == nil { users = []database.FTPUser{} } s.jsonResponse(w, http.StatusOK, users) case http.MethodPost: var user database.FTPUser if err := json.NewDecoder(r.Body).Decode(&user); err != nil { s.jsonError(w, "请求格式错误", http.StatusBadRequest) return } if user.Username == "" || user.Password == "" { s.jsonError(w, "用户名和密码不能为空", http.StatusBadRequest) return } if user.HomeDir == "" { ftpCfg := s.config.Get().FTP user.HomeDir = filepath.Join(ftpCfg.RootDir, user.Username) } // 自动创建用户目录(如果不存在) if err := os.MkdirAll(user.HomeDir, 0755); err != nil { s.jsonError(w, fmt.Sprintf("创建用户目录失败: %v", err), http.StatusInternalServerError) return } if user.Permissions == "" { user.Permissions = "read,write" } user.Enabled = true if err := s.db.CreateUser(&user); err != nil { s.jsonError(w, err.Error(), http.StatusInternalServerError) return } s.jsonResponse(w, http.StatusOK, user) default: s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed) } } // handleUserOperation 单个用户操作 func (s *Server) handleUserOperation(w http.ResponseWriter, r *http.Request) { // 解析路径中的用户名: /api/users/{username} 或 /api/users/{username}/password pathParts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/users/"), "/") username := pathParts[0] if username == "" { s.jsonError(w, "用户名不能为空", http.StatusBadRequest) return } // 密码修改: PUT /api/users/{username}/password if len(pathParts) > 1 && pathParts[1] == "password" && r.Method == http.MethodPut { var req struct { Password string `json:"password"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.jsonError(w, "请求格式错误", http.StatusBadRequest) return } if req.Password == "" { s.jsonError(w, "密码不能为空", http.StatusBadRequest) return } if err := s.db.UpdateUserPassword(username, req.Password); err != nil { s.jsonError(w, err.Error(), http.StatusInternalServerError) return } s.jsonResponse(w, http.StatusOK, map[string]string{"message": "密码已更新"}) return } switch r.Method { case http.MethodGet: user, err := s.db.GetUser(username) if err != nil { s.jsonError(w, err.Error(), http.StatusInternalServerError) return } if user == nil { s.jsonError(w, "用户不存在", http.StatusNotFound) return } s.jsonResponse(w, http.StatusOK, user) case http.MethodPut: var user database.FTPUser if err := json.NewDecoder(r.Body).Decode(&user); err != nil { s.jsonError(w, "请求格式错误", http.StatusBadRequest) return } // 如果目录有变化,自动创建新目录 if user.HomeDir != "" { if err := os.MkdirAll(user.HomeDir, 0755); err != nil { s.jsonError(w, fmt.Sprintf("创建目录失败: %v", err), http.StatusInternalServerError) return } } if err := s.db.UpdateUser(&user); err != nil { s.jsonError(w, err.Error(), http.StatusInternalServerError) return } s.jsonResponse(w, http.StatusOK, map[string]string{"message": "用户已更新"}) case http.MethodDelete: if err := s.db.DeleteUser(username); err != nil { s.jsonError(w, err.Error(), http.StatusInternalServerError) return } s.jsonResponse(w, http.StatusOK, map[string]string{"message": "用户已删除"}) default: s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed) } } // FileInfo 文件信息 type FileInfo struct { Name string `json:"name"` Size int64 `json:"size"` IsDir bool `json:"is_dir"` ModTime time.Time `json:"mod_time"` Path string `json:"path"` } // handleFileBrowse 文件浏览 func (s *Server) handleFileBrowse(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed) return } dir := r.URL.Query().Get("path") if dir == "" { dir = s.config.Get().FTP.RootDir } // 安全检查:确保路径在FTP根目录内 rootDir, err := filepath.Abs(s.config.Get().FTP.RootDir) if err != nil { s.jsonError(w, "无效的根目录", http.StatusInternalServerError) return } absPath, err := filepath.Abs(dir) if err != nil { s.jsonError(w, "无效的路径", http.StatusBadRequest) return } if !strings.HasPrefix(absPath, rootDir) { s.jsonError(w, "路径超出允许范围", http.StatusForbidden) return } entries, err := os.ReadDir(absPath) if err != nil { if os.IsNotExist(err) { s.jsonResponse(w, http.StatusOK, []FileInfo{}) return } s.jsonError(w, fmt.Sprintf("读取目录失败: %v", err), http.StatusInternalServerError) return } var files []FileInfo for _, entry := range entries { info, err := entry.Info() if err != nil { continue } files = append(files, FileInfo{ Name: entry.Name(), Size: info.Size(), IsDir: entry.IsDir(), ModTime: info.ModTime(), Path: filepath.Join(absPath, entry.Name()), }) } if files == nil { files = []FileInfo{} } s.jsonResponse(w, http.StatusOK, map[string]interface{}{ "path": absPath, "files": files, }) } // handleUpload 文件上传 func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed) return } targetDir := r.URL.Query().Get("path") if targetDir == "" { targetDir = s.config.Get().FTP.RootDir } // 安全检查 rootDir, _ := filepath.Abs(s.config.Get().FTP.RootDir) absDir, err := filepath.Abs(targetDir) if err != nil || !strings.HasPrefix(absDir, rootDir) { s.jsonError(w, "路径超出允许范围", http.StatusForbidden) return } r.ParseMultipartForm(100 << 20) // 100MB最大 file, header, err := r.FormFile("file") if err != nil { s.jsonError(w, "读取文件失败", http.StatusBadRequest) return } defer file.Close() targetPath := filepath.Join(absDir, header.Filename) dst, err := os.Create(targetPath) if err != nil { s.jsonError(w, "创建文件失败", http.StatusInternalServerError) return } defer dst.Close() written, err := io.Copy(dst, file) if err != nil { s.jsonError(w, "写入文件失败", http.StatusInternalServerError) return } s.jsonResponse(w, http.StatusOK, map[string]interface{}{ "message": "上传成功", "size": written, "path": targetPath, }) } // handleLogs 日志查询 func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed) return } username := r.URL.Query().Get("username") action := r.URL.Query().Get("action") page, _ := strconv.Atoi(r.URL.Query().Get("page")) pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size")) if page <= 0 { page = 1 } if pageSize <= 0 { pageSize = 20 } logs, total, err := s.db.QueryLogs(username, action, page, pageSize) if err != nil { s.jsonError(w, err.Error(), http.StatusInternalServerError) return } if logs == nil { logs = []database.FTPLog{} } s.jsonResponse(w, http.StatusOK, map[string]interface{}{ "logs": logs, "total": total, "page": page, "page_size": pageSize, }) } // handleOnline 在线用户 func (s *Server) handleOnline(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed) return } var users []database.OnlineUser if s.ftpServer != nil { users = s.ftpServer.GetOnlineUsers() } if users == nil { users = []database.OnlineUser{} } s.jsonResponse(w, http.StatusOK, users) } // handleConfig 配置管理 func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: cfg := s.config.Get() // 隐藏敏感信息 safeCfg := map[string]interface{}{ "ftp": map[string]interface{}{ "host": cfg.FTP.Host, "port": cfg.FTP.Port, "passive_port_min": cfg.FTP.PassivePortMin, "passive_port_max": cfg.FTP.PassivePortMax, "root_dir": cfg.FTP.RootDir, "enable_anonymous": cfg.FTP.EnableAnonymous, "max_connections": cfg.FTP.MaxConnections, "idle_timeout": cfg.FTP.IdleTimeout, }, "web": map[string]interface{}{ "host": cfg.Web.Host, "port": cfg.Web.Port, }, } s.jsonResponse(w, http.StatusOK, safeCfg) case http.MethodPut: var update map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&update); err != nil { s.jsonError(w, "请求格式错误", http.StatusBadRequest) return } s.config.Update(func(cfg *config.Config) { if ftpCfg, ok := update["ftp"].(map[string]interface{}); ok { if v, ok := ftpCfg["port"].(float64); ok { cfg.FTP.Port = int(v) } if v, ok := ftpCfg["passive_port_min"].(float64); ok { cfg.FTP.PassivePortMin = int(v) } if v, ok := ftpCfg["passive_port_max"].(float64); ok { cfg.FTP.PassivePortMax = int(v) } if v, ok := ftpCfg["enable_anonymous"].(bool); ok { cfg.FTP.EnableAnonymous = v } if v, ok := ftpCfg["max_connections"].(float64); ok { cfg.FTP.MaxConnections = int(v) } if v, ok := ftpCfg["idle_timeout"].(float64); ok { cfg.FTP.IdleTimeout = int(v) } } if adminCfg, ok := update["admin"].(map[string]interface{}); ok { if v, ok := adminCfg["username"].(string); ok && v != "" { cfg.Admin.Username = v } if v, ok := adminCfg["password"].(string); ok && v != "" { cfg.Admin.Password = v } } }) if err := s.config.Save("config.json"); err != nil { s.jsonError(w, fmt.Sprintf("保存配置失败: %v", err), http.StatusInternalServerError) return } s.jsonResponse(w, http.StatusOK, map[string]string{"message": "配置已保存,部分设置需要重启生效"}) default: s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed) } } // --- 工具方法 --- func (s *Server) jsonResponse(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(map[string]interface{}{ "code": status, "data": data, }) } func (s *Server) jsonError(w http.ResponseWriter, msg string, status int) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(map[string]interface{}{ "code": status, "error": msg, }) } // GenerateToken 生成随机令牌(用于JWT密钥) func GenerateToken() string { b := make([]byte, 16) rand.Read(b) return hex.EncodeToString(b) } // handleIPRules IP规则列表和创建 func (s *Server) handleIPRules(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: ruleType := r.URL.Query().Get("type") username := r.URL.Query().Get("username") rules, err := s.db.ListIPRules(ruleType, username) if err != nil { s.jsonError(w, err.Error(), http.StatusInternalServerError) return } if rules == nil { rules = []database.IPAccessRule{} } s.jsonResponse(w, http.StatusOK, rules) case http.MethodPost: var rule database.IPAccessRule if err := json.NewDecoder(r.Body).Decode(&rule); err != nil { s.jsonError(w, "请求格式错误", http.StatusBadRequest) return } if rule.IP == "" { s.jsonError(w, "IP不能为空", http.StatusBadRequest) return } if rule.Type != "whitelist" && rule.Type != "blacklist" { rule.Type = "blacklist" } rule.Enabled = true if err := s.db.CreateIPRule(&rule); err != nil { s.jsonError(w, err.Error(), http.StatusInternalServerError) return } s.jsonResponse(w, http.StatusOK, rule) default: s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed) } } // handleIPRuleOperation 单条IP规则操作 func (s *Server) handleIPRuleOperation(w http.ResponseWriter, r *http.Request) { pathParts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/ip-rules/"), "/") idStr := pathParts[0] if idStr == "" { s.jsonError(w, "ID不能为空", http.StatusBadRequest) return } id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { s.jsonError(w, "无效的ID", http.StatusBadRequest) return } switch r.Method { case http.MethodPut: var rule database.IPAccessRule if err := json.NewDecoder(r.Body).Decode(&rule); err != nil { s.jsonError(w, "请求格式错误", http.StatusBadRequest) return } rule.ID = id if err := s.db.UpdateIPRule(&rule); err != nil { s.jsonError(w, err.Error(), http.StatusInternalServerError) return } s.jsonResponse(w, http.StatusOK, map[string]string{"message": "规则已更新"}) case http.MethodDelete: if err := s.db.DeleteIPRule(id); err != nil { s.jsonError(w, err.Error(), http.StatusInternalServerError) return } s.jsonResponse(w, http.StatusOK, map[string]string{"message": "规则已删除"}) default: s.jsonError(w, "方法不允许", http.StatusMethodNotAllowed) } }