server.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  1. package ftp
  2. import (
  3. "bufio"
  4. "fmt"
  5. "io"
  6. "log"
  7. "net"
  8. "os"
  9. "path/filepath"
  10. "strconv"
  11. "strings"
  12. "sync"
  13. "time"
  14. "ftp-server/config"
  15. )
  16. // Server represents an FTP server
  17. type Server struct {
  18. config *config.Config
  19. configPath string
  20. listener net.Listener
  21. clients map[net.Conn]*clientConn
  22. mu sync.Mutex
  23. }
  24. type clientConn struct {
  25. conn net.Conn
  26. server *Server
  27. reader *bufio.Reader
  28. username string
  29. authenticated bool
  30. cwd string
  31. dataHost string
  32. dataPort int
  33. pasvMode bool
  34. pasvListener net.Listener
  35. transferType string
  36. renameFrom string
  37. }
  38. // NewServer creates a new FTP server
  39. func NewServer(cfg *config.Config, configPath string) *Server {
  40. return &Server{
  41. config: cfg,
  42. configPath: configPath,
  43. clients: make(map[net.Conn]*clientConn),
  44. }
  45. }
  46. // Start starts the FTP server
  47. func (s *Server) Start() error {
  48. addr := fmt.Sprintf("%s:%d", s.config.FTP.Host, s.config.FTP.Port)
  49. listener, err := net.Listen("tcp", addr)
  50. if err != nil {
  51. return fmt.Errorf("FTP server listen error: %v", err)
  52. }
  53. s.listener = listener
  54. // Ensure root directory exists
  55. rootDir := s.config.FTP.RootDir
  56. if err := os.MkdirAll(rootDir, 0755); err != nil {
  57. return fmt.Errorf("failed to create root directory: %v", err)
  58. }
  59. log.Printf("FTP server listening on %s, root dir: %s", addr, rootDir)
  60. go s.acceptLoop()
  61. return nil
  62. }
  63. // Stop stops the FTP server
  64. func (s *Server) Stop() {
  65. if s.listener != nil {
  66. s.listener.Close()
  67. }
  68. s.mu.Lock()
  69. for conn, client := range s.clients {
  70. client.pasvListener.Close()
  71. conn.Close()
  72. }
  73. s.mu.Unlock()
  74. }
  75. func (s *Server) acceptLoop() {
  76. for {
  77. conn, err := s.listener.Accept()
  78. if err != nil {
  79. log.Printf("FTP accept error: %v", err)
  80. return
  81. }
  82. client := &clientConn{
  83. conn: conn,
  84. server: s,
  85. reader: bufio.NewReader(conn),
  86. cwd: "/",
  87. transferType: "ASCII",
  88. }
  89. s.mu.Lock()
  90. s.clients[conn] = client
  91. s.mu.Unlock()
  92. log.Printf("FTP client connected: %s", conn.RemoteAddr())
  93. client.sendResponse(220, "FTP Server Ready")
  94. go client.handle()
  95. }
  96. }
  97. func (c *clientConn) sendResponse(code int, message string) {
  98. fmt.Fprintf(c.conn, "%d %s\r\n", code, message)
  99. }
  100. func (c *clientConn) sendMultiResponse(code int, lines []string) {
  101. for i, line := range lines {
  102. if i == len(lines)-1 {
  103. fmt.Fprintf(c.conn, "%d %s\r\n", code, line)
  104. } else {
  105. fmt.Fprintf(c.conn, "%d-%s\r\n", code, line)
  106. }
  107. }
  108. }
  109. func (c *clientConn) handle() {
  110. defer func() {
  111. c.conn.Close()
  112. if c.pasvListener != nil {
  113. c.pasvListener.Close()
  114. }
  115. c.server.mu.Lock()
  116. delete(c.server.clients, c.conn)
  117. c.server.mu.Unlock()
  118. log.Printf("FTP client disconnected: %s", c.conn.RemoteAddr())
  119. }()
  120. for {
  121. c.conn.SetDeadline(time.Now().Add(5 * time.Minute))
  122. line, err := c.reader.ReadString('\n')
  123. if err != nil {
  124. if err != io.EOF {
  125. log.Printf("FTP read error: %v", err)
  126. }
  127. return
  128. }
  129. line = strings.TrimRight(line, "\r\n")
  130. if line == "" {
  131. continue
  132. }
  133. log.Printf("FTP [%s] %s", c.conn.RemoteAddr(), line)
  134. c.processCommand(line)
  135. }
  136. }
  137. func (c *clientConn) processCommand(line string) {
  138. parts := strings.SplitN(line, " ", 2)
  139. cmd := strings.ToUpper(parts[0])
  140. var args string
  141. if len(parts) > 1 {
  142. args = parts[1]
  143. }
  144. switch cmd {
  145. case "USER":
  146. c.handleUSER(args)
  147. case "PASS":
  148. c.handlePASS(args)
  149. case "QUIT":
  150. c.handleQUIT()
  151. case "PWD":
  152. c.handlePWD()
  153. case "CWD":
  154. c.handleCWD(args)
  155. case "CDUP":
  156. c.handleCDUP()
  157. case "TYPE":
  158. c.handleTYPE(args)
  159. case "PASV":
  160. c.handlePASV()
  161. case "PORT":
  162. c.handlePORT(args)
  163. case "LIST":
  164. c.handleLIST(args)
  165. case "NLST":
  166. c.handleNLST(args)
  167. case "RETR":
  168. c.handleRETR(args)
  169. case "STOR":
  170. c.handleSTOR(args)
  171. case "DELE":
  172. c.handleDELE(args)
  173. case "MKD":
  174. c.handleMKD(args)
  175. case "RMD":
  176. c.handleRMD(args)
  177. case "RNFR":
  178. c.handleRNFR(args)
  179. case "RNTO":
  180. c.handleRNTO(args)
  181. case "SIZE":
  182. c.handleSIZE(args)
  183. case "SYST":
  184. c.sendResponse(215, "UNIX Type: L8")
  185. case "FEAT":
  186. c.handleFEAT()
  187. case "NOOP":
  188. c.sendResponse(200, "OK")
  189. case "OPTS":
  190. c.handleOPTS(args)
  191. default:
  192. c.sendResponse(502, fmt.Sprintf("Command %s not implemented", cmd))
  193. }
  194. }
  195. func (c *clientConn) handleUSER(args string) {
  196. if args == "" {
  197. c.sendResponse(501, "Syntax error: USER <username>")
  198. return
  199. }
  200. c.username = args
  201. c.authenticated = false
  202. c.sendResponse(331, "Password required")
  203. }
  204. func (c *clientConn) handlePASS(args string) {
  205. if c.username == "" {
  206. c.sendResponse(503, "Login with USER first")
  207. return
  208. }
  209. user := c.server.config.GetFTPUser(c.username)
  210. if user != nil && user.Password == args {
  211. c.authenticated = true
  212. // Set home directory as CWD
  213. homeDir := user.HomeDir
  214. if homeDir == "" {
  215. homeDir = c.server.config.FTP.RootDir
  216. }
  217. c.cwd = "/"
  218. c.sendResponse(230, "Login successful")
  219. log.Printf("FTP user '%s' logged in from %s", c.username, c.conn.RemoteAddr())
  220. } else {
  221. c.username = ""
  222. c.sendResponse(530, "Login incorrect")
  223. }
  224. }
  225. func (c *clientConn) requireAuth() bool {
  226. if !c.authenticated {
  227. c.sendResponse(530, "Please login first")
  228. return false
  229. }
  230. return true
  231. }
  232. func (c *clientConn) handleQUIT() {
  233. c.sendResponse(221, "Goodbye")
  234. c.conn.Close()
  235. }
  236. func (c *clientConn) handlePWD() {
  237. if !c.requireAuth() {
  238. return
  239. }
  240. c.sendResponse(257, fmt.Sprintf("\"%s\" is current directory", c.cwd))
  241. }
  242. func (c *clientConn) handleCWD(args string) {
  243. if !c.requireAuth() {
  244. return
  245. }
  246. newPath := c.resolvePath(args)
  247. realPath := c.toRealPath(newPath)
  248. info, err := os.Stat(realPath)
  249. if err != nil || !info.IsDir() {
  250. c.sendResponse(550, "Directory not found")
  251. return
  252. }
  253. c.cwd = newPath
  254. c.sendResponse(250, fmt.Sprintf("Directory changed to %s", c.cwd))
  255. }
  256. func (c *clientConn) handleCDUP() {
  257. if !c.requireAuth() {
  258. return
  259. }
  260. if c.cwd != "/" {
  261. c.cwd = filepath.Dir(c.cwd)
  262. if c.cwd == "" {
  263. c.cwd = "/"
  264. }
  265. }
  266. c.sendResponse(250, fmt.Sprintf("Directory changed to %s", c.cwd))
  267. }
  268. func (c *clientConn) handleTYPE(args string) {
  269. args = strings.ToUpper(args)
  270. switch args {
  271. case "A", "A N":
  272. c.transferType = "ASCII"
  273. c.sendResponse(200, "Type set to ASCII")
  274. case "I", "L 8":
  275. c.transferType = "BINARY"
  276. c.sendResponse(200, "Type set to Binary")
  277. default:
  278. c.sendResponse(504, "Type not supported")
  279. }
  280. }
  281. func (c *clientConn) handlePASV() {
  282. if !c.requireAuth() {
  283. return
  284. }
  285. if c.pasvListener != nil {
  286. c.pasvListener.Close()
  287. }
  288. listener, err := net.Listen("tcp", "0.0.0.0:0")
  289. if err != nil {
  290. c.sendResponse(425, "Cannot open passive connection")
  291. return
  292. }
  293. c.pasvListener = listener
  294. c.pasvMode = true
  295. addr := listener.Addr().(*net.TCPAddr)
  296. hostParts := strings.Split(c.conn.LocalAddr().(*net.TCPAddr).IP.String(), ".")
  297. p1 := addr.Port / 256
  298. p2 := addr.Port % 256
  299. c.sendResponse(227, fmt.Sprintf("Entering Passive Mode (%s,%s,%s,%s,%d,%d)",
  300. hostParts[0], hostParts[1], hostParts[2], hostParts[3], p1, p2))
  301. }
  302. func (c *clientConn) handlePORT(args string) {
  303. if !c.requireAuth() {
  304. return
  305. }
  306. parts := strings.Split(args, ",")
  307. if len(parts) != 6 {
  308. c.sendResponse(501, "Syntax error in PORT command")
  309. return
  310. }
  311. host := strings.Join(parts[0:4], ".")
  312. p1, _ := strconv.Atoi(parts[4])
  313. p2, _ := strconv.Atoi(parts[5])
  314. c.dataHost = host
  315. c.dataPort = p1*256 + p2
  316. c.pasvMode = false
  317. c.sendResponse(200, "PORT command successful")
  318. }
  319. func (c *clientConn) getDataConn() (net.Conn, error) {
  320. if c.pasvMode && c.pasvListener != nil {
  321. conn, err := c.pasvListener.Accept()
  322. c.pasvListener.Close()
  323. c.pasvListener = nil
  324. return conn, err
  325. }
  326. addr := fmt.Sprintf("%s:%d", c.dataHost, c.dataPort)
  327. return net.DialTimeout("tcp", addr, 10*time.Second)
  328. }
  329. func (c *clientConn) handleLIST(args string) {
  330. if !c.requireAuth() {
  331. return
  332. }
  333. targetPath := args
  334. if targetPath == "" || targetPath == "-a" || targetPath == "-la" || targetPath == "-al" {
  335. targetPath = c.cwd
  336. } else if !strings.HasPrefix(targetPath, "/") {
  337. targetPath = c.resolvePath(targetPath)
  338. }
  339. realPath := c.toRealPath(targetPath)
  340. entries, err := os.ReadDir(realPath)
  341. if err != nil {
  342. c.sendResponse(550, "Directory listing failed")
  343. return
  344. }
  345. dataConn, err := c.getDataConn()
  346. if err != nil {
  347. c.sendResponse(425, "Cannot open data connection")
  348. return
  349. }
  350. defer dataConn.Close()
  351. c.sendResponse(150, "Opening data connection for directory listing")
  352. var lines []string
  353. for _, entry := range entries {
  354. info, _ := entry.Info()
  355. modTime := info.ModTime()
  356. perm := "-rwxr-xr-x"
  357. if info.IsDir() {
  358. perm = "drwxr-xr-x"
  359. }
  360. line := fmt.Sprintf("%s 1 ftp ftp %12d %s %s",
  361. perm,
  362. info.Size(),
  363. modTime.Format("Jan 02 15:04"),
  364. entry.Name(),
  365. )
  366. lines = append(lines, line)
  367. }
  368. if len(lines) == 0 {
  369. dataConn.Write([]byte(""))
  370. } else {
  371. dataConn.Write([]byte(strings.Join(lines, "\r\n") + "\r\n"))
  372. }
  373. c.sendResponse(226, "Transfer complete")
  374. }
  375. func (c *clientConn) handleNLST(args string) {
  376. if !c.requireAuth() {
  377. return
  378. }
  379. targetPath := args
  380. if targetPath == "" {
  381. targetPath = c.cwd
  382. }
  383. realPath := c.toRealPath(c.resolvePath(targetPath))
  384. entries, err := os.ReadDir(realPath)
  385. if err != nil {
  386. c.sendResponse(550, "Directory listing failed")
  387. return
  388. }
  389. dataConn, err := c.getDataConn()
  390. if err != nil {
  391. c.sendResponse(425, "Cannot open data connection")
  392. return
  393. }
  394. defer dataConn.Close()
  395. c.sendResponse(150, "Opening data connection")
  396. var names []string
  397. for _, entry := range entries {
  398. names = append(names, entry.Name())
  399. }
  400. dataConn.Write([]byte(strings.Join(names, "\r\n") + "\r\n"))
  401. c.sendResponse(226, "Transfer complete")
  402. }
  403. func (c *clientConn) handleRETR(args string) {
  404. if !c.requireAuth() {
  405. return
  406. }
  407. filePath := c.resolvePath(args)
  408. realPath := c.toRealPath(filePath)
  409. file, err := os.Open(realPath)
  410. if err != nil {
  411. c.sendResponse(550, "File not found")
  412. return
  413. }
  414. defer file.Close()
  415. dataConn, err := c.getDataConn()
  416. if err != nil {
  417. c.sendResponse(425, "Cannot open data connection")
  418. return
  419. }
  420. defer dataConn.Close()
  421. c.sendResponse(150, "Opening data connection")
  422. written, err := io.Copy(dataConn, file)
  423. if err != nil {
  424. c.sendResponse(426, "Transfer aborted")
  425. return
  426. }
  427. log.Printf("FTP RETR %s (%d bytes)", filePath, written)
  428. c.sendResponse(226, "Transfer complete")
  429. }
  430. func (c *clientConn) handleSTOR(args string) {
  431. if !c.requireAuth() {
  432. return
  433. }
  434. // Check write permission
  435. user := c.server.config.GetFTPUser(c.username)
  436. if user == nil || !user.Write {
  437. c.sendResponse(550, "Permission denied")
  438. return
  439. }
  440. filePath := c.resolvePath(args)
  441. realPath := c.toRealPath(filePath)
  442. // Ensure parent directory exists
  443. os.MkdirAll(filepath.Dir(realPath), 0755)
  444. file, err := os.Create(realPath)
  445. if err != nil {
  446. c.sendResponse(550, "Cannot create file")
  447. return
  448. }
  449. defer file.Close()
  450. dataConn, err := c.getDataConn()
  451. if err != nil {
  452. c.sendResponse(425, "Cannot open data connection")
  453. return
  454. }
  455. defer dataConn.Close()
  456. c.sendResponse(150, "Opening data connection")
  457. written, err := io.Copy(file, dataConn)
  458. if err != nil {
  459. c.sendResponse(426, "Transfer aborted")
  460. return
  461. }
  462. log.Printf("FTP STOR %s (%d bytes)", filePath, written)
  463. c.sendResponse(226, "Transfer complete")
  464. }
  465. func (c *clientConn) handleDELE(args string) {
  466. if !c.requireAuth() {
  467. return
  468. }
  469. user := c.server.config.GetFTPUser(c.username)
  470. if user == nil || !user.Write {
  471. c.sendResponse(550, "Permission denied")
  472. return
  473. }
  474. filePath := c.resolvePath(args)
  475. realPath := c.toRealPath(filePath)
  476. if err := os.Remove(realPath); err != nil {
  477. c.sendResponse(550, "Delete failed")
  478. return
  479. }
  480. c.sendResponse(250, "File deleted")
  481. }
  482. func (c *clientConn) handleMKD(args string) {
  483. if !c.requireAuth() {
  484. return
  485. }
  486. user := c.server.config.GetFTPUser(c.username)
  487. if user == nil || !user.Write {
  488. c.sendResponse(550, "Permission denied")
  489. return
  490. }
  491. dirPath := c.resolvePath(args)
  492. realPath := c.toRealPath(dirPath)
  493. if err := os.MkdirAll(realPath, 0755); err != nil {
  494. c.sendResponse(550, "Cannot create directory")
  495. return
  496. }
  497. c.sendResponse(257, fmt.Sprintf("\"%s\" created", dirPath))
  498. }
  499. func (c *clientConn) handleRMD(args string) {
  500. if !c.requireAuth() {
  501. return
  502. }
  503. user := c.server.config.GetFTPUser(c.username)
  504. if user == nil || !user.Write {
  505. c.sendResponse(550, "Permission denied")
  506. return
  507. }
  508. dirPath := c.resolvePath(args)
  509. realPath := c.toRealPath(dirPath)
  510. if err := os.RemoveAll(realPath); err != nil {
  511. c.sendResponse(550, "Cannot remove directory")
  512. return
  513. }
  514. c.sendResponse(250, "Directory removed")
  515. }
  516. func (c *clientConn) handleRNFR(args string) {
  517. if !c.requireAuth() {
  518. return
  519. }
  520. user := c.server.config.GetFTPUser(c.username)
  521. if user == nil || !user.Write {
  522. c.sendResponse(550, "Permission denied")
  523. return
  524. }
  525. filePath := c.resolvePath(args)
  526. realPath := c.toRealPath(filePath)
  527. if _, err := os.Stat(realPath); err != nil {
  528. c.sendResponse(550, "File not found")
  529. return
  530. }
  531. c.renameFrom = filePath
  532. c.sendResponse(350, "Ready for RNTO")
  533. }
  534. func (c *clientConn) handleRNTO(args string) {
  535. if !c.requireAuth() {
  536. return
  537. }
  538. if c.renameFrom == "" {
  539. c.sendResponse(503, "RNFR required first")
  540. return
  541. }
  542. fromPath := c.toRealPath(c.renameFrom)
  543. toPath := c.toRealPath(c.resolvePath(args))
  544. if err := os.Rename(fromPath, toPath); err != nil {
  545. c.sendResponse(550, "Rename failed")
  546. return
  547. }
  548. c.sendResponse(250, "Renamed")
  549. c.renameFrom = ""
  550. }
  551. func (c *clientConn) handleSIZE(args string) {
  552. if !c.requireAuth() {
  553. return
  554. }
  555. filePath := c.resolvePath(args)
  556. realPath := c.toRealPath(filePath)
  557. info, err := os.Stat(realPath)
  558. if err != nil {
  559. c.sendResponse(550, "File not found")
  560. return
  561. }
  562. c.sendResponse(213, fmt.Sprintf("%d", info.Size()))
  563. }
  564. func (c *clientConn) handleFEAT() {
  565. features := []string{
  566. "Features:",
  567. " PASV",
  568. " UTF8",
  569. " SIZE",
  570. }
  571. c.sendMultiResponse(211, features)
  572. }
  573. func (c *clientConn) handleOPTS(args string) {
  574. if strings.ToUpper(args) == "UTF8 ON" {
  575. c.sendResponse(200, "UTF8 set to on")
  576. } else {
  577. c.sendResponse(501, "Option not understood")
  578. }
  579. }
  580. // resolvePath resolves a relative path to an absolute path within the FTP root
  581. func (c *clientConn) resolvePath(path string) string {
  582. if strings.HasPrefix(path, "/") {
  583. return filepath.Clean(path)
  584. }
  585. return filepath.Clean(filepath.Join(c.cwd, path))
  586. }
  587. // toRealPath converts an FTP virtual path to a real filesystem path
  588. func (c *clientConn) toRealPath(ftpPath string) string {
  589. user := c.server.config.GetFTPUser(c.username)
  590. rootDir := c.server.config.FTP.RootDir
  591. if user != nil && user.HomeDir != "" {
  592. rootDir = user.HomeDir
  593. }
  594. // Remove leading slash
  595. cleanPath := strings.TrimPrefix(ftpPath, "/")
  596. return filepath.Join(rootDir, cleanPath)
  597. }