server.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. package web
  2. import (
  3. "crypto/rand"
  4. "encoding/hex"
  5. "encoding/json"
  6. "fmt"
  7. "log"
  8. "net/http"
  9. "strings"
  10. "sync"
  11. "time"
  12. "ftp-server/config"
  13. "ftp-server/static"
  14. )
  15. // Server represents the web admin server
  16. type Server struct {
  17. config *config.Config
  18. configPath string
  19. sessions map[string]time.Time
  20. mu sync.RWMutex
  21. }
  22. // NewServer creates a new web admin server
  23. func NewServer(cfg *config.Config, configPath string) *Server {
  24. return &Server{
  25. config: cfg,
  26. configPath: configPath,
  27. sessions: make(map[string]time.Time),
  28. }
  29. }
  30. // Start starts the web admin server
  31. func (s *Server) Start() error {
  32. mux := http.NewServeMux()
  33. // Static files served directly from code
  34. // No file server needed - HTML is embedded in static package
  35. // Serve the main page
  36. mux.HandleFunc("/", s.handleIndex)
  37. // API routes
  38. mux.HandleFunc("/api/login", s.handleLogin)
  39. mux.HandleFunc("/api/logout", s.authRequired(s.handleLogout))
  40. mux.HandleFunc("/api/status", s.authRequired(s.handleStatus))
  41. mux.HandleFunc("/api/users", s.authRequired(s.handleUsers))
  42. mux.HandleFunc("/api/users/add", s.authRequired(s.handleAddUser))
  43. mux.HandleFunc("/api/users/delete", s.authRequired(s.handleDeleteUser))
  44. mux.HandleFunc("/api/users/update", s.authRequired(s.handleUpdateUser))
  45. mux.HandleFunc("/api/config", s.authRequired(s.handleConfig))
  46. mux.HandleFunc("/api/config/update", s.authRequired(s.handleUpdateConfig))
  47. // Start session cleanup
  48. go s.cleanupSessions()
  49. addr := fmt.Sprintf("%s:%d", s.config.Web.Host, s.config.Web.Port)
  50. log.Printf("Web admin server listening on http://%s", addr)
  51. return http.ListenAndServe(addr, mux)
  52. }
  53. // authRequired is middleware that requires authentication
  54. func (s *Server) authRequired(next http.HandlerFunc) http.HandlerFunc {
  55. return func(w http.ResponseWriter, r *http.Request) {
  56. token := r.Header.Get("Authorization")
  57. if token == "" {
  58. cookie, err := r.Cookie("session")
  59. if err == nil {
  60. token = cookie.Value
  61. }
  62. }
  63. token = strings.TrimPrefix(token, "Bearer ")
  64. if !s.isValidSession(token) {
  65. http.Error(w, `{"error":"Unauthorized"}`, http.StatusUnauthorized)
  66. return
  67. }
  68. next(w, r)
  69. }
  70. }
  71. func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
  72. if r.URL.Path != "/" {
  73. http.NotFound(w, r)
  74. return
  75. }
  76. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  77. w.Write([]byte(static.IndexHTML))
  78. }
  79. func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
  80. if r.Method != http.MethodPost {
  81. http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
  82. return
  83. }
  84. var creds struct {
  85. Username string `json:"username"`
  86. Password string `json:"password"`
  87. }
  88. if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
  89. http.Error(w, `{"error":"Invalid request"}`, http.StatusBadRequest)
  90. return
  91. }
  92. if !s.config.AuthenticateAdmin(creds.Username, creds.Password) {
  93. http.Error(w, `{"error":"Invalid credentials"}`, http.StatusUnauthorized)
  94. return
  95. }
  96. token := s.createSession()
  97. w.Header().Set("Content-Type", "application/json")
  98. json.NewEncoder(w).Encode(map[string]string{
  99. "token": token,
  100. })
  101. log.Printf("Web admin '%s' logged in from %s", creds.Username, r.RemoteAddr)
  102. }
  103. func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
  104. token := r.Header.Get("Authorization")
  105. token = strings.TrimPrefix(token, "Bearer ")
  106. if cookie, err := r.Cookie("session"); err == nil {
  107. token = cookie.Value
  108. }
  109. s.deleteSession(token)
  110. w.Header().Set("Content-Type", "application/json")
  111. json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
  112. }
  113. func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
  114. w.Header().Set("Content-Type", "application/json")
  115. json.NewEncoder(w).Encode(map[string]interface{}{
  116. "status": "running",
  117. "ftpPort": s.config.FTP.Port,
  118. "webPort": s.config.Web.Port,
  119. "rootDir": s.config.FTP.RootDir,
  120. "userCount": len(s.config.GetFTPUsers()),
  121. })
  122. }
  123. func (s *Server) handleUsers(w http.ResponseWriter, r *http.Request) {
  124. users := s.config.GetFTPUsers()
  125. // Hide passwords in response
  126. type safeUser struct {
  127. Username string `json:"username"`
  128. HomeDir string `json:"homeDir"`
  129. Write bool `json:"write"`
  130. }
  131. var safeUsers []safeUser
  132. for _, u := range users {
  133. safeUsers = append(safeUsers, safeUser{
  134. Username: u.Username,
  135. HomeDir: u.HomeDir,
  136. Write: u.Write,
  137. })
  138. }
  139. w.Header().Set("Content-Type", "application/json")
  140. json.NewEncoder(w).Encode(safeUsers)
  141. }
  142. func (s *Server) handleAddUser(w http.ResponseWriter, r *http.Request) {
  143. if r.Method != http.MethodPost {
  144. http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
  145. return
  146. }
  147. var user config.FTPUser
  148. if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
  149. http.Error(w, `{"error":"Invalid request"}`, http.StatusBadRequest)
  150. return
  151. }
  152. if user.Username == "" || user.Password == "" {
  153. http.Error(w, `{"error":"Username and password required"}`, http.StatusBadRequest)
  154. return
  155. }
  156. if s.config.GetFTPUser(user.Username) != nil {
  157. http.Error(w, `{"error":"User already exists"}`, http.StatusConflict)
  158. return
  159. }
  160. if user.HomeDir == "" {
  161. user.HomeDir = s.config.FTP.RootDir
  162. }
  163. s.config.AddFTPUser(user)
  164. if err := s.config.Save(s.configPath); err != nil {
  165. http.Error(w, `{"error":"Failed to save config"}`, http.StatusInternalServerError)
  166. return
  167. }
  168. log.Printf("FTP user '%s' added via web admin", user.Username)
  169. w.Header().Set("Content-Type", "application/json")
  170. json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
  171. }
  172. func (s *Server) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
  173. if r.Method != http.MethodPost {
  174. http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
  175. return
  176. }
  177. var req struct {
  178. Username string `json:"username"`
  179. }
  180. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  181. http.Error(w, `{"error":"Invalid request"}`, http.StatusBadRequest)
  182. return
  183. }
  184. if !s.config.DeleteFTPUser(req.Username) {
  185. http.Error(w, `{"error":"User not found"}`, http.StatusNotFound)
  186. return
  187. }
  188. if err := s.config.Save(s.configPath); err != nil {
  189. http.Error(w, `{"error":"Failed to save config"}`, http.StatusInternalServerError)
  190. return
  191. }
  192. log.Printf("FTP user '%s' deleted via web admin", req.Username)
  193. w.Header().Set("Content-Type", "application/json")
  194. json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
  195. }
  196. func (s *Server) handleUpdateUser(w http.ResponseWriter, r *http.Request) {
  197. if r.Method != http.MethodPost {
  198. http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
  199. return
  200. }
  201. var req struct {
  202. Username string `json:"username"`
  203. User config.FTPUser `json:"user"`
  204. KeepPassword bool `json:"keepPassword"`
  205. }
  206. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  207. http.Error(w, `{"error":"Invalid request"}`, http.StatusBadRequest)
  208. return
  209. }
  210. if req.KeepPassword && req.User.Password == "" {
  211. existing := s.config.GetFTPUser(req.Username)
  212. if existing != nil {
  213. req.User.Password = existing.Password
  214. }
  215. }
  216. if !s.config.UpdateFTPUser(req.Username, req.User) {
  217. http.Error(w, `{"error":"User not found"}`, http.StatusNotFound)
  218. return
  219. }
  220. if err := s.config.Save(s.configPath); err != nil {
  221. http.Error(w, `{"error":"Failed to save config"}`, http.StatusInternalServerError)
  222. return
  223. }
  224. log.Printf("FTP user '%s' updated via web admin", req.Username)
  225. w.Header().Set("Content-Type", "application/json")
  226. json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
  227. }
  228. func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
  229. w.Header().Set("Content-Type", "application/json")
  230. json.NewEncoder(w).Encode(map[string]interface{}{
  231. "ftp": s.config.FTP,
  232. "web": s.config.Web,
  233. "adminUsername": s.config.Admin.Username,
  234. })
  235. }
  236. func (s *Server) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
  237. if r.Method != http.MethodPost {
  238. http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
  239. return
  240. }
  241. var update struct {
  242. FTP *config.FTPConfig `json:"ftp,omitempty"`
  243. Web *config.WebConfig `json:"web,omitempty"`
  244. AdminPass string `json:"adminPassword,omitempty"`
  245. }
  246. if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
  247. http.Error(w, `{"error":"Invalid request"}`, http.StatusBadRequest)
  248. return
  249. }
  250. if update.FTP != nil {
  251. s.config.FTP = *update.FTP
  252. }
  253. if update.Web != nil {
  254. s.config.Web = *update.Web
  255. }
  256. if update.AdminPass != "" {
  257. s.config.Admin.Password = update.AdminPass
  258. }
  259. if err := s.config.Save(s.configPath); err != nil {
  260. http.Error(w, `{"error":"Failed to save config"}`, http.StatusInternalServerError)
  261. return
  262. }
  263. log.Println("Configuration updated via web admin")
  264. w.Header().Set("Content-Type", "application/json")
  265. json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
  266. }
  267. // Session management
  268. func (s *Server) createSession() string {
  269. b := make([]byte, 32)
  270. rand.Read(b)
  271. token := hex.EncodeToString(b)
  272. s.mu.Lock()
  273. s.sessions[token] = time.Now().Add(24 * time.Hour)
  274. s.mu.Unlock()
  275. return token
  276. }
  277. func (s *Server) isValidSession(token string) bool {
  278. if token == "" {
  279. return false
  280. }
  281. s.mu.RLock()
  282. expiry, ok := s.sessions[token]
  283. s.mu.RUnlock()
  284. return ok && time.Now().Before(expiry)
  285. }
  286. func (s *Server) deleteSession(token string) {
  287. s.mu.Lock()
  288. delete(s.sessions, token)
  289. s.mu.Unlock()
  290. }
  291. func (s *Server) cleanupSessions() {
  292. ticker := time.NewTicker(1 * time.Hour)
  293. defer ticker.Stop()
  294. for range ticker.C {
  295. s.mu.Lock()
  296. now := time.Now()
  297. for token, expiry := range s.sessions {
  298. if now.After(expiry) {
  299. delete(s.sessions, token)
  300. }
  301. }
  302. s.mu.Unlock()
  303. }
  304. }