| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701 |
- package ftp
- import (
- "bufio"
- "fmt"
- "io"
- "log"
- "net"
- "os"
- "path/filepath"
- "strconv"
- "strings"
- "sync"
- "time"
- "ftp-server/config"
- )
- // Server represents an FTP server
- type Server struct {
- config *config.Config
- configPath string
- listener net.Listener
- clients map[net.Conn]*clientConn
- mu sync.Mutex
- }
- type clientConn struct {
- conn net.Conn
- server *Server
- reader *bufio.Reader
- username string
- authenticated bool
- cwd string
- dataHost string
- dataPort int
- pasvMode bool
- pasvListener net.Listener
- transferType string
- renameFrom string
- }
- // NewServer creates a new FTP server
- func NewServer(cfg *config.Config, configPath string) *Server {
- return &Server{
- config: cfg,
- configPath: configPath,
- clients: make(map[net.Conn]*clientConn),
- }
- }
- // Start starts the FTP server
- func (s *Server) Start() error {
- addr := fmt.Sprintf("%s:%d", s.config.FTP.Host, s.config.FTP.Port)
- listener, err := net.Listen("tcp", addr)
- if err != nil {
- return fmt.Errorf("FTP server listen error: %v", err)
- }
- s.listener = listener
- // Ensure root directory exists
- rootDir := s.config.FTP.RootDir
- if err := os.MkdirAll(rootDir, 0755); err != nil {
- return fmt.Errorf("failed to create root directory: %v", err)
- }
- log.Printf("FTP server listening on %s, root dir: %s", addr, rootDir)
- go s.acceptLoop()
- return nil
- }
- // Stop stops the FTP server
- func (s *Server) Stop() {
- if s.listener != nil {
- s.listener.Close()
- }
- s.mu.Lock()
- for conn, client := range s.clients {
- client.pasvListener.Close()
- conn.Close()
- }
- s.mu.Unlock()
- }
- func (s *Server) acceptLoop() {
- for {
- conn, err := s.listener.Accept()
- if err != nil {
- log.Printf("FTP accept error: %v", err)
- return
- }
- client := &clientConn{
- conn: conn,
- server: s,
- reader: bufio.NewReader(conn),
- cwd: "/",
- transferType: "ASCII",
- }
- s.mu.Lock()
- s.clients[conn] = client
- s.mu.Unlock()
- log.Printf("FTP client connected: %s", conn.RemoteAddr())
- client.sendResponse(220, "FTP Server Ready")
- go client.handle()
- }
- }
- func (c *clientConn) sendResponse(code int, message string) {
- fmt.Fprintf(c.conn, "%d %s\r\n", code, message)
- }
- func (c *clientConn) sendMultiResponse(code int, lines []string) {
- for i, line := range lines {
- if i == len(lines)-1 {
- fmt.Fprintf(c.conn, "%d %s\r\n", code, line)
- } else {
- fmt.Fprintf(c.conn, "%d-%s\r\n", code, line)
- }
- }
- }
- func (c *clientConn) handle() {
- defer func() {
- c.conn.Close()
- if c.pasvListener != nil {
- c.pasvListener.Close()
- }
- c.server.mu.Lock()
- delete(c.server.clients, c.conn)
- c.server.mu.Unlock()
- log.Printf("FTP client disconnected: %s", c.conn.RemoteAddr())
- }()
- for {
- c.conn.SetDeadline(time.Now().Add(5 * time.Minute))
- line, err := c.reader.ReadString('\n')
- if err != nil {
- if err != io.EOF {
- log.Printf("FTP read error: %v", err)
- }
- return
- }
- line = strings.TrimRight(line, "\r\n")
- if line == "" {
- continue
- }
- log.Printf("FTP [%s] %s", c.conn.RemoteAddr(), line)
- c.processCommand(line)
- }
- }
- func (c *clientConn) processCommand(line string) {
- parts := strings.SplitN(line, " ", 2)
- cmd := strings.ToUpper(parts[0])
- var args string
- if len(parts) > 1 {
- args = parts[1]
- }
- switch cmd {
- case "USER":
- c.handleUSER(args)
- case "PASS":
- c.handlePASS(args)
- case "QUIT":
- c.handleQUIT()
- case "PWD":
- c.handlePWD()
- case "CWD":
- c.handleCWD(args)
- case "CDUP":
- c.handleCDUP()
- case "TYPE":
- c.handleTYPE(args)
- case "PASV":
- c.handlePASV()
- case "PORT":
- c.handlePORT(args)
- case "LIST":
- c.handleLIST(args)
- case "NLST":
- c.handleNLST(args)
- case "RETR":
- c.handleRETR(args)
- case "STOR":
- c.handleSTOR(args)
- case "DELE":
- c.handleDELE(args)
- case "MKD":
- c.handleMKD(args)
- case "RMD":
- c.handleRMD(args)
- case "RNFR":
- c.handleRNFR(args)
- case "RNTO":
- c.handleRNTO(args)
- case "SIZE":
- c.handleSIZE(args)
- case "SYST":
- c.sendResponse(215, "UNIX Type: L8")
- case "FEAT":
- c.handleFEAT()
- case "NOOP":
- c.sendResponse(200, "OK")
- case "OPTS":
- c.handleOPTS(args)
- default:
- c.sendResponse(502, fmt.Sprintf("Command %s not implemented", cmd))
- }
- }
- func (c *clientConn) handleUSER(args string) {
- if args == "" {
- c.sendResponse(501, "Syntax error: USER <username>")
- return
- }
- c.username = args
- c.authenticated = false
- c.sendResponse(331, "Password required")
- }
- func (c *clientConn) handlePASS(args string) {
- if c.username == "" {
- c.sendResponse(503, "Login with USER first")
- return
- }
- user := c.server.config.GetFTPUser(c.username)
- if user != nil && user.Password == args {
- c.authenticated = true
- // Set home directory as CWD
- homeDir := user.HomeDir
- if homeDir == "" {
- homeDir = c.server.config.FTP.RootDir
- }
- c.cwd = "/"
- c.sendResponse(230, "Login successful")
- log.Printf("FTP user '%s' logged in from %s", c.username, c.conn.RemoteAddr())
- } else {
- c.username = ""
- c.sendResponse(530, "Login incorrect")
- }
- }
- func (c *clientConn) requireAuth() bool {
- if !c.authenticated {
- c.sendResponse(530, "Please login first")
- return false
- }
- return true
- }
- func (c *clientConn) handleQUIT() {
- c.sendResponse(221, "Goodbye")
- c.conn.Close()
- }
- func (c *clientConn) handlePWD() {
- if !c.requireAuth() {
- return
- }
- c.sendResponse(257, fmt.Sprintf("\"%s\" is current directory", c.cwd))
- }
- func (c *clientConn) handleCWD(args string) {
- if !c.requireAuth() {
- return
- }
- newPath := c.resolvePath(args)
- realPath := c.toRealPath(newPath)
- info, err := os.Stat(realPath)
- if err != nil || !info.IsDir() {
- c.sendResponse(550, "Directory not found")
- return
- }
- c.cwd = newPath
- c.sendResponse(250, fmt.Sprintf("Directory changed to %s", c.cwd))
- }
- func (c *clientConn) handleCDUP() {
- if !c.requireAuth() {
- return
- }
- if c.cwd != "/" {
- c.cwd = filepath.Dir(c.cwd)
- if c.cwd == "" {
- c.cwd = "/"
- }
- }
- c.sendResponse(250, fmt.Sprintf("Directory changed to %s", c.cwd))
- }
- func (c *clientConn) handleTYPE(args string) {
- args = strings.ToUpper(args)
- switch args {
- case "A", "A N":
- c.transferType = "ASCII"
- c.sendResponse(200, "Type set to ASCII")
- case "I", "L 8":
- c.transferType = "BINARY"
- c.sendResponse(200, "Type set to Binary")
- default:
- c.sendResponse(504, "Type not supported")
- }
- }
- func (c *clientConn) handlePASV() {
- if !c.requireAuth() {
- return
- }
- if c.pasvListener != nil {
- c.pasvListener.Close()
- }
- listener, err := net.Listen("tcp", "0.0.0.0:0")
- if err != nil {
- c.sendResponse(425, "Cannot open passive connection")
- return
- }
- c.pasvListener = listener
- c.pasvMode = true
- addr := listener.Addr().(*net.TCPAddr)
- hostParts := strings.Split(c.conn.LocalAddr().(*net.TCPAddr).IP.String(), ".")
- p1 := addr.Port / 256
- p2 := addr.Port % 256
- c.sendResponse(227, fmt.Sprintf("Entering Passive Mode (%s,%s,%s,%s,%d,%d)",
- hostParts[0], hostParts[1], hostParts[2], hostParts[3], p1, p2))
- }
- func (c *clientConn) handlePORT(args string) {
- if !c.requireAuth() {
- return
- }
- parts := strings.Split(args, ",")
- if len(parts) != 6 {
- c.sendResponse(501, "Syntax error in PORT command")
- return
- }
- host := strings.Join(parts[0:4], ".")
- p1, _ := strconv.Atoi(parts[4])
- p2, _ := strconv.Atoi(parts[5])
- c.dataHost = host
- c.dataPort = p1*256 + p2
- c.pasvMode = false
- c.sendResponse(200, "PORT command successful")
- }
- func (c *clientConn) getDataConn() (net.Conn, error) {
- if c.pasvMode && c.pasvListener != nil {
- conn, err := c.pasvListener.Accept()
- c.pasvListener.Close()
- c.pasvListener = nil
- return conn, err
- }
- addr := fmt.Sprintf("%s:%d", c.dataHost, c.dataPort)
- return net.DialTimeout("tcp", addr, 10*time.Second)
- }
- func (c *clientConn) handleLIST(args string) {
- if !c.requireAuth() {
- return
- }
- targetPath := args
- if targetPath == "" || targetPath == "-a" || targetPath == "-la" || targetPath == "-al" {
- targetPath = c.cwd
- } else if !strings.HasPrefix(targetPath, "/") {
- targetPath = c.resolvePath(targetPath)
- }
- realPath := c.toRealPath(targetPath)
- entries, err := os.ReadDir(realPath)
- if err != nil {
- c.sendResponse(550, "Directory listing failed")
- return
- }
- dataConn, err := c.getDataConn()
- if err != nil {
- c.sendResponse(425, "Cannot open data connection")
- return
- }
- defer dataConn.Close()
- c.sendResponse(150, "Opening data connection for directory listing")
- var lines []string
- for _, entry := range entries {
- info, _ := entry.Info()
- modTime := info.ModTime()
- perm := "-rwxr-xr-x"
- if info.IsDir() {
- perm = "drwxr-xr-x"
- }
- line := fmt.Sprintf("%s 1 ftp ftp %12d %s %s",
- perm,
- info.Size(),
- modTime.Format("Jan 02 15:04"),
- entry.Name(),
- )
- lines = append(lines, line)
- }
- if len(lines) == 0 {
- dataConn.Write([]byte(""))
- } else {
- dataConn.Write([]byte(strings.Join(lines, "\r\n") + "\r\n"))
- }
- c.sendResponse(226, "Transfer complete")
- }
- func (c *clientConn) handleNLST(args string) {
- if !c.requireAuth() {
- return
- }
- targetPath := args
- if targetPath == "" {
- targetPath = c.cwd
- }
- realPath := c.toRealPath(c.resolvePath(targetPath))
- entries, err := os.ReadDir(realPath)
- if err != nil {
- c.sendResponse(550, "Directory listing failed")
- return
- }
- dataConn, err := c.getDataConn()
- if err != nil {
- c.sendResponse(425, "Cannot open data connection")
- return
- }
- defer dataConn.Close()
- c.sendResponse(150, "Opening data connection")
- var names []string
- for _, entry := range entries {
- names = append(names, entry.Name())
- }
- dataConn.Write([]byte(strings.Join(names, "\r\n") + "\r\n"))
- c.sendResponse(226, "Transfer complete")
- }
- func (c *clientConn) handleRETR(args string) {
- if !c.requireAuth() {
- return
- }
- filePath := c.resolvePath(args)
- realPath := c.toRealPath(filePath)
- file, err := os.Open(realPath)
- if err != nil {
- c.sendResponse(550, "File not found")
- return
- }
- defer file.Close()
- dataConn, err := c.getDataConn()
- if err != nil {
- c.sendResponse(425, "Cannot open data connection")
- return
- }
- defer dataConn.Close()
- c.sendResponse(150, "Opening data connection")
- written, err := io.Copy(dataConn, file)
- if err != nil {
- c.sendResponse(426, "Transfer aborted")
- return
- }
- log.Printf("FTP RETR %s (%d bytes)", filePath, written)
- c.sendResponse(226, "Transfer complete")
- }
- func (c *clientConn) handleSTOR(args string) {
- if !c.requireAuth() {
- return
- }
- // Check write permission
- user := c.server.config.GetFTPUser(c.username)
- if user == nil || !user.Write {
- c.sendResponse(550, "Permission denied")
- return
- }
- filePath := c.resolvePath(args)
- realPath := c.toRealPath(filePath)
- // Ensure parent directory exists
- os.MkdirAll(filepath.Dir(realPath), 0755)
- file, err := os.Create(realPath)
- if err != nil {
- c.sendResponse(550, "Cannot create file")
- return
- }
- defer file.Close()
- dataConn, err := c.getDataConn()
- if err != nil {
- c.sendResponse(425, "Cannot open data connection")
- return
- }
- defer dataConn.Close()
- c.sendResponse(150, "Opening data connection")
- written, err := io.Copy(file, dataConn)
- if err != nil {
- c.sendResponse(426, "Transfer aborted")
- return
- }
- log.Printf("FTP STOR %s (%d bytes)", filePath, written)
- c.sendResponse(226, "Transfer complete")
- }
- func (c *clientConn) handleDELE(args string) {
- if !c.requireAuth() {
- return
- }
- user := c.server.config.GetFTPUser(c.username)
- if user == nil || !user.Write {
- c.sendResponse(550, "Permission denied")
- return
- }
- filePath := c.resolvePath(args)
- realPath := c.toRealPath(filePath)
- if err := os.Remove(realPath); err != nil {
- c.sendResponse(550, "Delete failed")
- return
- }
- c.sendResponse(250, "File deleted")
- }
- func (c *clientConn) handleMKD(args string) {
- if !c.requireAuth() {
- return
- }
- user := c.server.config.GetFTPUser(c.username)
- if user == nil || !user.Write {
- c.sendResponse(550, "Permission denied")
- return
- }
- dirPath := c.resolvePath(args)
- realPath := c.toRealPath(dirPath)
- if err := os.MkdirAll(realPath, 0755); err != nil {
- c.sendResponse(550, "Cannot create directory")
- return
- }
- c.sendResponse(257, fmt.Sprintf("\"%s\" created", dirPath))
- }
- func (c *clientConn) handleRMD(args string) {
- if !c.requireAuth() {
- return
- }
- user := c.server.config.GetFTPUser(c.username)
- if user == nil || !user.Write {
- c.sendResponse(550, "Permission denied")
- return
- }
- dirPath := c.resolvePath(args)
- realPath := c.toRealPath(dirPath)
- if err := os.RemoveAll(realPath); err != nil {
- c.sendResponse(550, "Cannot remove directory")
- return
- }
- c.sendResponse(250, "Directory removed")
- }
- func (c *clientConn) handleRNFR(args string) {
- if !c.requireAuth() {
- return
- }
- user := c.server.config.GetFTPUser(c.username)
- if user == nil || !user.Write {
- c.sendResponse(550, "Permission denied")
- return
- }
- filePath := c.resolvePath(args)
- realPath := c.toRealPath(filePath)
- if _, err := os.Stat(realPath); err != nil {
- c.sendResponse(550, "File not found")
- return
- }
- c.renameFrom = filePath
- c.sendResponse(350, "Ready for RNTO")
- }
- func (c *clientConn) handleRNTO(args string) {
- if !c.requireAuth() {
- return
- }
- if c.renameFrom == "" {
- c.sendResponse(503, "RNFR required first")
- return
- }
- fromPath := c.toRealPath(c.renameFrom)
- toPath := c.toRealPath(c.resolvePath(args))
- if err := os.Rename(fromPath, toPath); err != nil {
- c.sendResponse(550, "Rename failed")
- return
- }
- c.sendResponse(250, "Renamed")
- c.renameFrom = ""
- }
- func (c *clientConn) handleSIZE(args string) {
- if !c.requireAuth() {
- return
- }
- filePath := c.resolvePath(args)
- realPath := c.toRealPath(filePath)
- info, err := os.Stat(realPath)
- if err != nil {
- c.sendResponse(550, "File not found")
- return
- }
- c.sendResponse(213, fmt.Sprintf("%d", info.Size()))
- }
- func (c *clientConn) handleFEAT() {
- features := []string{
- "Features:",
- " PASV",
- " UTF8",
- " SIZE",
- }
- c.sendMultiResponse(211, features)
- }
- func (c *clientConn) handleOPTS(args string) {
- if strings.ToUpper(args) == "UTF8 ON" {
- c.sendResponse(200, "UTF8 set to on")
- } else {
- c.sendResponse(501, "Option not understood")
- }
- }
- // resolvePath resolves a relative path to an absolute path within the FTP root
- func (c *clientConn) resolvePath(path string) string {
- if strings.HasPrefix(path, "/") {
- return filepath.Clean(path)
- }
- return filepath.Clean(filepath.Join(c.cwd, path))
- }
- // toRealPath converts an FTP virtual path to a real filesystem path
- func (c *clientConn) toRealPath(ftpPath string) string {
- user := c.server.config.GetFTPUser(c.username)
- rootDir := c.server.config.FTP.RootDir
- if user != nil && user.HomeDir != "" {
- rootDir = user.HomeDir
- }
- // Remove leading slash
- cleanPath := strings.TrimPrefix(ftpPath, "/")
- return filepath.Join(rootDir, cleanPath)
- }
|