Files
FTP-Server/web/server.go
T

668 lines
17 KiB
Go

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")
rules, err := s.db.ListIPRules(ruleType)
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)
}
}