server.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  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. data := 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. if s.config.HTTPFile.Enable {
  123. data["httpFilePort"] = s.config.HTTPFile.Port
  124. }
  125. json.NewEncoder(w).Encode(data)
  126. }
  127. func (s *Server) handleUsers(w http.ResponseWriter, r *http.Request) {
  128. users := s.config.GetFTPUsers()
  129. // Hide passwords in response
  130. type safeUser struct {
  131. Username string `json:"username"`
  132. HomeDir string `json:"homeDir"`
  133. Write bool `json:"write"`
  134. }
  135. var safeUsers []safeUser
  136. for _, u := range users {
  137. safeUsers = append(safeUsers, safeUser{
  138. Username: u.Username,
  139. HomeDir: u.HomeDir,
  140. Write: u.Write,
  141. })
  142. }
  143. w.Header().Set("Content-Type", "application/json")
  144. json.NewEncoder(w).Encode(safeUsers)
  145. }
  146. func (s *Server) handleAddUser(w http.ResponseWriter, r *http.Request) {
  147. if r.Method != http.MethodPost {
  148. http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
  149. return
  150. }
  151. var user config.FTPUser
  152. if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
  153. http.Error(w, `{"error":"Invalid request"}`, http.StatusBadRequest)
  154. return
  155. }
  156. if user.Username == "" || user.Password == "" {
  157. http.Error(w, `{"error":"Username and password required"}`, http.StatusBadRequest)
  158. return
  159. }
  160. if s.config.GetFTPUser(user.Username) != nil {
  161. http.Error(w, `{"error":"User already exists"}`, http.StatusConflict)
  162. return
  163. }
  164. if user.HomeDir == "" {
  165. user.HomeDir = s.config.FTP.RootDir
  166. }
  167. s.config.AddFTPUser(user)
  168. if err := s.config.Save(s.configPath); err != nil {
  169. http.Error(w, `{"error":"Failed to save config"}`, http.StatusInternalServerError)
  170. return
  171. }
  172. log.Printf("FTP user '%s' added via web admin", user.Username)
  173. w.Header().Set("Content-Type", "application/json")
  174. json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
  175. }
  176. func (s *Server) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
  177. if r.Method != http.MethodPost {
  178. http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
  179. return
  180. }
  181. var req struct {
  182. Username string `json:"username"`
  183. }
  184. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  185. http.Error(w, `{"error":"Invalid request"}`, http.StatusBadRequest)
  186. return
  187. }
  188. if !s.config.DeleteFTPUser(req.Username) {
  189. http.Error(w, `{"error":"User not found"}`, http.StatusNotFound)
  190. return
  191. }
  192. if err := s.config.Save(s.configPath); err != nil {
  193. http.Error(w, `{"error":"Failed to save config"}`, http.StatusInternalServerError)
  194. return
  195. }
  196. log.Printf("FTP user '%s' deleted via web admin", req.Username)
  197. w.Header().Set("Content-Type", "application/json")
  198. json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
  199. }
  200. func (s *Server) handleUpdateUser(w http.ResponseWriter, r *http.Request) {
  201. if r.Method != http.MethodPost {
  202. http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
  203. return
  204. }
  205. var req struct {
  206. Username string `json:"username"`
  207. User config.FTPUser `json:"user"`
  208. KeepPassword bool `json:"keepPassword"`
  209. }
  210. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  211. http.Error(w, `{"error":"Invalid request"}`, http.StatusBadRequest)
  212. return
  213. }
  214. if req.KeepPassword && req.User.Password == "" {
  215. existing := s.config.GetFTPUser(req.Username)
  216. if existing != nil {
  217. req.User.Password = existing.Password
  218. }
  219. }
  220. if !s.config.UpdateFTPUser(req.Username, req.User) {
  221. http.Error(w, `{"error":"User not found"}`, http.StatusNotFound)
  222. return
  223. }
  224. if err := s.config.Save(s.configPath); err != nil {
  225. http.Error(w, `{"error":"Failed to save config"}`, http.StatusInternalServerError)
  226. return
  227. }
  228. log.Printf("FTP user '%s' updated via web admin", req.Username)
  229. w.Header().Set("Content-Type", "application/json")
  230. json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
  231. }
  232. func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
  233. w.Header().Set("Content-Type", "application/json")
  234. json.NewEncoder(w).Encode(map[string]interface{}{
  235. "ftp": s.config.FTP,
  236. "web": s.config.Web,
  237. "adminUsername": s.config.Admin.Username,
  238. })
  239. }
  240. func (s *Server) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
  241. if r.Method != http.MethodPost {
  242. http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
  243. return
  244. }
  245. var update struct {
  246. FTP *config.FTPConfig `json:"ftp,omitempty"`
  247. Web *config.WebConfig `json:"web,omitempty"`
  248. AdminPass string `json:"adminPassword,omitempty"`
  249. }
  250. if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
  251. http.Error(w, `{"error":"Invalid request"}`, http.StatusBadRequest)
  252. return
  253. }
  254. if update.FTP != nil {
  255. s.config.FTP = *update.FTP
  256. }
  257. if update.Web != nil {
  258. s.config.Web = *update.Web
  259. }
  260. if update.AdminPass != "" {
  261. s.config.Admin.Password = update.AdminPass
  262. }
  263. if err := s.config.Save(s.configPath); err != nil {
  264. http.Error(w, `{"error":"Failed to save config"}`, http.StatusInternalServerError)
  265. return
  266. }
  267. log.Println("Configuration updated via web admin")
  268. w.Header().Set("Content-Type", "application/json")
  269. json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
  270. }
  271. // Session management
  272. func (s *Server) createSession() string {
  273. b := make([]byte, 32)
  274. rand.Read(b)
  275. token := hex.EncodeToString(b)
  276. s.mu.Lock()
  277. s.sessions[token] = time.Now().Add(24 * time.Hour)
  278. s.mu.Unlock()
  279. return token
  280. }
  281. func (s *Server) isValidSession(token string) bool {
  282. if token == "" {
  283. return false
  284. }
  285. s.mu.RLock()
  286. expiry, ok := s.sessions[token]
  287. s.mu.RUnlock()
  288. return ok && time.Now().Before(expiry)
  289. }
  290. func (s *Server) deleteSession(token string) {
  291. s.mu.Lock()
  292. delete(s.sessions, token)
  293. s.mu.Unlock()
  294. }
  295. func (s *Server) cleanupSessions() {
  296. ticker := time.NewTicker(1 * time.Hour)
  297. defer ticker.Stop()
  298. for range ticker.C {
  299. s.mu.Lock()
  300. now := time.Now()
  301. for token, expiry := range s.sessions {
  302. if now.After(expiry) {
  303. delete(s.sessions, token)
  304. }
  305. }
  306. s.mu.Unlock()
  307. }
  308. }