Files

702 sor
14 KiB
Go

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