| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668 |
- 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)
- }
- }
|