Files

368 rindas
9.6 KiB
Go

package web
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"sync"
"time"
"ftp-server/config"
"ftp-server/static"
)
// Server represents the web admin server
type Server struct {
config *config.Config
configPath string
sessions map[string]time.Time
mu sync.RWMutex
}
// NewServer creates a new web admin server
func NewServer(cfg *config.Config, configPath string) *Server {
return &Server{
config: cfg,
configPath: configPath,
sessions: make(map[string]time.Time),
}
}
// Start starts the web admin server
func (s *Server) Start() error {
mux := http.NewServeMux()
// Static files served directly from code
// No file server needed - HTML is embedded in static package
// Serve the main page
mux.HandleFunc("/", s.handleIndex)
// API routes
mux.HandleFunc("/api/login", s.handleLogin)
mux.HandleFunc("/api/logout", s.authRequired(s.handleLogout))
mux.HandleFunc("/api/status", s.authRequired(s.handleStatus))
mux.HandleFunc("/api/users", s.authRequired(s.handleUsers))
mux.HandleFunc("/api/users/add", s.authRequired(s.handleAddUser))
mux.HandleFunc("/api/users/delete", s.authRequired(s.handleDeleteUser))
mux.HandleFunc("/api/users/update", s.authRequired(s.handleUpdateUser))
mux.HandleFunc("/api/config", s.authRequired(s.handleConfig))
mux.HandleFunc("/api/config/update", s.authRequired(s.handleUpdateConfig))
// Start session cleanup
go s.cleanupSessions()
addr := fmt.Sprintf("%s:%d", s.config.Web.Host, s.config.Web.Port)
log.Printf("Web admin server listening on http://%s", addr)
return http.ListenAndServe(addr, mux)
}
// authRequired is middleware that requires authentication
func (s *Server) authRequired(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
cookie, err := r.Cookie("session")
if err == nil {
token = cookie.Value
}
}
token = strings.TrimPrefix(token, "Bearer ")
if !s.isValidSession(token) {
http.Error(w, `{"error":"Unauthorized"}`, http.StatusUnauthorized)
return
}
next(w, r)
}
}
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(static.IndexHTML))
}
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
var creds struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
http.Error(w, `{"error":"Invalid request"}`, http.StatusBadRequest)
return
}
if !s.config.AuthenticateAdmin(creds.Username, creds.Password) {
http.Error(w, `{"error":"Invalid credentials"}`, http.StatusUnauthorized)
return
}
token := s.createSession()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"token": token,
})
log.Printf("Web admin '%s' logged in from %s", creds.Username, r.RemoteAddr)
}
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
token = strings.TrimPrefix(token, "Bearer ")
if cookie, err := r.Cookie("session"); err == nil {
token = cookie.Value
}
s.deleteSession(token)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
data := map[string]interface{}{
"status": "running",
"ftpPort": s.config.FTP.Port,
"webPort": s.config.Web.Port,
"rootDir": s.config.FTP.RootDir,
"userCount": len(s.config.GetFTPUsers()),
}
if s.config.HTTPFile.Enable {
data["httpFilePort"] = s.config.HTTPFile.Port
}
json.NewEncoder(w).Encode(data)
}
func (s *Server) handleUsers(w http.ResponseWriter, r *http.Request) {
users := s.config.GetFTPUsers()
// Hide passwords in response
type safeUser struct {
Username string `json:"username"`
HomeDir string `json:"homeDir"`
Write bool `json:"write"`
}
var safeUsers []safeUser
for _, u := range users {
safeUsers = append(safeUsers, safeUser{
Username: u.Username,
HomeDir: u.HomeDir,
Write: u.Write,
})
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(safeUsers)
}
func (s *Server) handleAddUser(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
var user config.FTPUser
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, `{"error":"Invalid request"}`, http.StatusBadRequest)
return
}
if user.Username == "" || user.Password == "" {
http.Error(w, `{"error":"Username and password required"}`, http.StatusBadRequest)
return
}
if s.config.GetFTPUser(user.Username) != nil {
http.Error(w, `{"error":"User already exists"}`, http.StatusConflict)
return
}
if user.HomeDir == "" {
user.HomeDir = s.config.FTP.RootDir
}
s.config.AddFTPUser(user)
if err := s.config.Save(s.configPath); err != nil {
http.Error(w, `{"error":"Failed to save config"}`, http.StatusInternalServerError)
return
}
log.Printf("FTP user '%s' added via web admin", user.Username)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func (s *Server) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
var req struct {
Username string `json:"username"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"Invalid request"}`, http.StatusBadRequest)
return
}
if !s.config.DeleteFTPUser(req.Username) {
http.Error(w, `{"error":"User not found"}`, http.StatusNotFound)
return
}
if err := s.config.Save(s.configPath); err != nil {
http.Error(w, `{"error":"Failed to save config"}`, http.StatusInternalServerError)
return
}
log.Printf("FTP user '%s' deleted via web admin", req.Username)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func (s *Server) handleUpdateUser(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
var req struct {
Username string `json:"username"`
User config.FTPUser `json:"user"`
KeepPassword bool `json:"keepPassword"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"Invalid request"}`, http.StatusBadRequest)
return
}
if req.KeepPassword && req.User.Password == "" {
existing := s.config.GetFTPUser(req.Username)
if existing != nil {
req.User.Password = existing.Password
}
}
if !s.config.UpdateFTPUser(req.Username, req.User) {
http.Error(w, `{"error":"User not found"}`, http.StatusNotFound)
return
}
if err := s.config.Save(s.configPath); err != nil {
http.Error(w, `{"error":"Failed to save config"}`, http.StatusInternalServerError)
return
}
log.Printf("FTP user '%s' updated via web admin", req.Username)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"ftp": s.config.FTP,
"web": s.config.Web,
"adminUsername": s.config.Admin.Username,
})
}
func (s *Server) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
var update struct {
FTP *config.FTPConfig `json:"ftp,omitempty"`
Web *config.WebConfig `json:"web,omitempty"`
AdminPass string `json:"adminPassword,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
http.Error(w, `{"error":"Invalid request"}`, http.StatusBadRequest)
return
}
if update.FTP != nil {
s.config.FTP = *update.FTP
}
if update.Web != nil {
s.config.Web = *update.Web
}
if update.AdminPass != "" {
s.config.Admin.Password = update.AdminPass
}
if err := s.config.Save(s.configPath); err != nil {
http.Error(w, `{"error":"Failed to save config"}`, http.StatusInternalServerError)
return
}
log.Println("Configuration updated via web admin")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
// Session management
func (s *Server) createSession() string {
b := make([]byte, 32)
rand.Read(b)
token := hex.EncodeToString(b)
s.mu.Lock()
s.sessions[token] = time.Now().Add(24 * time.Hour)
s.mu.Unlock()
return token
}
func (s *Server) isValidSession(token string) bool {
if token == "" {
return false
}
s.mu.RLock()
expiry, ok := s.sessions[token]
s.mu.RUnlock()
return ok && time.Now().Before(expiry)
}
func (s *Server) deleteSession(token string) {
s.mu.Lock()
delete(s.sessions, token)
s.mu.Unlock()
}
func (s *Server) cleanupSessions() {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for range ticker.C {
s.mu.Lock()
now := time.Now()
for token, expiry := range s.sessions {
if now.After(expiry) {
delete(s.sessions, token)
}
}
s.mu.Unlock()
}
}