package main import ( "context" "canguidev/shelfy/internal/db" "canguidev/shelfy/internal/routes" "canguidev/shelfy/internal/utils" "crypto/tls" "errors" "log" "net" "os" "path/filepath" "strings" "time" ftpserver "github.com/fclairamb/ftpserverlib" "github.com/gin-gonic/gin" "github.com/spf13/afero" ) // ---------- FTP SERVERLIB DRIVERS ---------- type mainDriver struct{} // tcpKeepAliveListener est un wrapper générique qui active le keep-alive // sur chaque connexion acceptée par un net.Listener. type tcpKeepAliveListener struct{ net.Listener } func (l tcpKeepAliveListener) Accept() (net.Conn, error) { c, err := l.Listener.Accept() if err != nil { return nil, err } if tc, ok := c.(*net.TCPConn); ok { _ = tc.SetKeepAlive(true) _ = tc.SetKeepAlivePeriod(30 * time.Second) } return c, nil } // clientDriver : wrapper sur afero.Fs type clientDriver struct { fs afero.Fs } func (d *mainDriver) GetSettings() (*ftpserver.Settings, error) { // Listener principal (port 2121) avec TCP keep-alive activé lc := net.ListenConfig{ KeepAlive: 30 * time.Second, // garde la connexion de contrôle vivante côté NAT/pare-feu } ln, err := lc.Listen(context.Background(), "tcp", ":2121") if err != nil { return nil, err } return &ftpserver.Settings{ // On fournit notre listener plutôt que ListenAddr Listener: ln, PublicHost: "163.172.68.103", PassiveTransferPortRange: &ftpserver.PortRange{ Start: 30000, End: 30100, }, Banner: "Bienvenue sur le FTP Go!", IdleTimeout: 0, // ne pas couper côté serveur ConnectionTimeout: 10, // délai pour établir une connexion de transfert }, nil } func (d *mainDriver) ClientConnected(cc ftpserver.ClientContext) (string, error) { log.Printf("[FTP] Nouvelle connexion depuis %s", cc.RemoteAddr()) return "Bienvenue FTP !", nil } func (d *mainDriver) ClientDisconnected(cc ftpserver.ClientContext) { log.Printf("[FTP] Déconnexion client %s", cc.RemoteAddr()) } func (d *mainDriver) GetTLSConfig() (*tls.Config, error) { // Pas de TLS ici (FTPS), à ajouter si besoin return nil, nil } func (d *mainDriver) AuthUser(cc ftpserver.ClientContext, user, pass string) (ftpserver.ClientDriver, error) { log.Printf("[FTP] Tentative login user='%s' pass='%s'", user, pass) remoteAddr := cc.RemoteAddr().String() host, _, _ := net.SplitHostPort(remoteAddr) // 1. Compte user/pwd if user == "cangui2089" && pass == "GHT30k7!" { uploadPath, _ := filepath.Abs("upload") fi, err := os.Stat(uploadPath) if err != nil || !fi.IsDir() { log.Printf("[FTP] Le dossier upload/ est manquant ou non valide : %v", err) return nil, errors.New("le dossier upload/ doit exister") } log.Printf("[FTP] Connexion OK, exposé: %s", uploadPath) return &clientDriver{ fs: afero.NewBasePathFs(afero.NewOsFs(), uploadPath), }, nil } // 2. Autoriser anonymous depuis IP spécifique if user == "anonymous" && host == "82.65.73.115" { base := filepath.Clean("upload") fs := afero.NewBasePathFs(afero.NewOsFs(), base) log.Printf("[FTP] Login ANONYMOUS autorisé pour %s", host) return &clientDriver{fs: fs}, nil } // 3. Refuser anonymous ou mauvais login if user == "" || user == "anonymous" { log.Printf("[FTP] Login anonymous refusé pour %s", host) return nil, errors.New("anonymous login not allowed") } return nil, errors.New("identifiants invalides") } // ---- Extension: wrapper du listener passif (PASV) pour activer keep-alive ---- // WrapPassiveListener est appelée par ftpserverlib si le mainDriver // implémente l'extension "MainDriverExtensionPassiveWrapper". // On y met le même wrapper keep-alive que pour le listener principal. func (d *mainDriver) WrapPassiveListener(l net.Listener) (net.Listener, error) { log.Printf("[FTP] WrapPassiveListener: activation TCP keep-alive sur PASV") return tcpKeepAliveListener{l}, nil } // ---------- clientDriver = wrapper sur afero.Fs ---------- func (c *clientDriver) Name() string { return "aferofs" } func (c *clientDriver) Create(name string) (afero.File, error) { return c.fs.Create(name) } func (c *clientDriver) Mkdir(name string, perm os.FileMode) error { return c.fs.Mkdir(name, perm) } func (c *clientDriver) MkdirAll(path string, perm os.FileMode) error { return c.fs.MkdirAll(path, perm) } func (c *clientDriver) Open(name string) (afero.File, error) { return c.fs.Open(name) } func (c *clientDriver) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) { log.Printf("[FTP][DEBUG] Open: %s", name) return c.fs.OpenFile(name, flag, perm) } func (c *clientDriver) Remove(name string) error { return c.fs.Remove(name) } func (c *clientDriver) RemoveAll(path string) error { return c.fs.RemoveAll(path) } func (c *clientDriver) Rename(old, new string) error { return c.fs.Rename(old, new) } func (c *clientDriver) Stat(name string) (os.FileInfo, error) { return c.fs.Stat(name) } func (c *clientDriver) LstatIfPossible(name string) (os.FileInfo, bool, error) { fi, err := c.fs.Stat(name) return fi, false, err } func (c *clientDriver) Chmod(name string, mode os.FileMode) error { fullPath := filepath.Join("upload", name) return os.Chmod(fullPath, mode) } func (c *clientDriver) Chown(name string, uid, gid int) error { fullPath := filepath.Join("upload", name) return os.Chown(fullPath, uid, gid) } func (c *clientDriver) Chtimes(name string, atime, mtime time.Time) error { fullPath := filepath.Join("upload", name) return os.Chtimes(fullPath, atime, mtime) } // ---------- MAIN ---------- func main() { // Serveur FTP dans une goroutine go func() { ftp := ftpserver.NewFtpServer(&mainDriver{}) log.Println("[FTP] Serveur FTP en écoute sur ftp://cangui2089:GHT30k7!@localhost:2121 (upload/)") if err := ftp.ListenAndServe(); err != nil { log.Fatal("[FTP] Erreur:", err) } }() // Serveur HTTP Gin (classique) bd := db.InitDB() app := gin.Default() api := app.Group("/api/v1") routes.AddRoutes(api, bd) utils.CreateDefaultFolder(bd) app.Static("/static", "./web") app.NoRoute(func(c *gin.Context) { if strings.HasPrefix(c.Request.URL.Path, "/api/") { c.JSON(404, gin.H{"error": "Not found"}) return } c.File("./web/index.html") }) log.Println("[HTTP] Serveur Gin sur http://localhost:8080") _ = app.Run(":8080") }