| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- 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()
- }
- }
|