368 linhas
9.6 KiB
Go
368 linhas
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()
|
|
}
|
|
}
|