package main import ( "canguidev/shelfy/internal/db" "canguidev/shelfy/internal/routes" "canguidev/shelfy/internal/utils" "crypto/ed25519" "crypto/rand" "crypto/rsa" "crypto/x509" "embed" "encoding/pem" "fmt" "io" "log" "net" "os" "path/filepath" "strings" "time" "github.com/gin-gonic/gin" "github.com/pkg/sftp" "golang.org/x/crypto/ssh" ) // --------- CONFIG À ADAPTER ---------- var ( // Dossier racine SFTP (tu montes déjà ton volume ici dans Docker) SFTPBaseDir = "upload" // Identifiants standards (utilisés si IP non autorisée) LoginUser = "cangui2089" LoginPass = "GHT30k7!" // IP(s) autorisées pour connexion SANS mot de passe AllowedIPs = []string{ "82.65.73.115", } ) // ------------------------------------- // ---------- SFTP Server ---------- // startSFTPServer démarre un serveur SFTP sur le port 2222 // avec authentification par IP-allowlist et user/pass. // Le dossier racine SFTP est défini par `base`. // Les clés hôte sont générées ou chargées depuis le disque. // Les IP autorisées peuvent se connecter sans mot de passe. // Les autres doivent utiliser le login et mot de passe définis. // Le serveur écoute sur le port 2222 et gère les connexions SFTP. // Il utilise les clés hôte ed25519 et RSA pour la compatibilité. // Les connexions TCP ont un keep-alive agressif pour éviter les coupures NAT. // Les erreurs de connexion sont loguées, mais le serveur continue à accepter les connexions. // Les requêtes SFTP sont traitées avec un serveur SFTP minimaliste. // Les erreurs de traitement des requêtes sont loguées. // Le serveur est conçu pour être lancé dans un goroutine séparé. // Il vérifie que le dossier racine existe et est un répertoire valide. // Les clés hôte sont stockées dans le même répertoire que le serveur SFTP. // Le serveur utilise le protocole SSH pour établir les connexions SFTP. // Il gère les canaux de session et les requêtes de sous-système SFTP. // Les connexions sont acceptées en boucle infinie, avec des logs pour chaque connexion réussie ou échouée. // Les erreurs de handshake SSH sont loguées et la connexion est fermée. // Les canaux SFTP sont acceptés et traités pour servir les requêtes SFTP. // Le serveur SFTP est initialisé avec des gestionnaires par défaut pour les opérations de fichier. // Les erreurs lors de l'initialisation ou du traitement des requêtes SFTP sont loguées. // Le serveur continue à accepter les connexions même après des erreurs, garantissant une disponibilité maximale. // Il est recommandé de lancer cette fonction dans un goroutine séparé pour ne pas bloquer l'exécution principale du programme. func startSFTPServer(base string) { // --- helpers locaux pour charger/générer les clés hôte --- loadOrCreateEd25519 := func(path string) (ssh.Signer, error) { if _, err := os.Stat(path); err == nil { b, err := os.ReadFile(path) if err != nil { return nil, err } return ssh.ParsePrivateKey(b) } _, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, err } pkcs8, err := x509.MarshalPKCS8PrivateKey(priv) if err != nil { return nil, err } pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8}) if err := os.WriteFile(path, pemBytes, 0o600); err != nil { return nil, err } return ssh.ParsePrivateKey(pemBytes) } loadOrCreateRSA := func(path string) (ssh.Signer, error) { if _, err := os.Stat(path); err == nil { b, err := os.ReadFile(path) if err != nil { return nil, err } return ssh.ParsePrivateKey(b) } priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, err } pkcs1 := x509.MarshalPKCS1PrivateKey(priv) pemBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: pkcs1}) if err := os.WriteFile(path, pemBytes, 0o600); err != nil { return nil, err } return ssh.ParsePrivateKey(pemBytes) } ipAllowed := func(addr string) bool { host, _, err := net.SplitHostPort(addr) if err != nil { host = addr } for _, allow := range AllowedIPs { if host == allow { return true } } return false } // --- vérifications et "ancrage" dans le dossier upload --- absBase, _ := filepath.Abs(base) if fi, err := os.Stat(absBase); err != nil || !fi.IsDir() { log.Fatalf("[SFTP] Le dossier %s est manquant ou invalide: %v", absBase, err) } // if err := os.Chdir(absBase); err != nil { // log.Fatalf("[SFTP] Chdir(%s): %v", absBase, err) // } // --- clés hôte : ed25519 (moderne) + RSA (compat) --- signerED, err := loadOrCreateEd25519("sftp_host_ed25519.pem") if err != nil { log.Fatalf("[SFTP] Host key ed25519 error: %v", err) } signerRSA, err := loadOrCreateRSA("sftp_host_rsa.pem") if err != nil { log.Fatalf("[SFTP] Host key RSA error: %v", err) } // --- config SSH avec auth IP-allowlist et user/pass --- cfg := &ssh.ServerConfig{ PasswordCallback: func(meta ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { remote := meta.RemoteAddr().String() user := meta.User() // IP autorisée → on accepte même mot de passe vide if ipAllowed(remote) { log.Printf("[SFTP] Auth IP-allowlist OK from=%s user=%s (mdp vide accepté)", remote, user) return nil, nil } // Sinon, exige user+pass if user == LoginUser && string(pass) == LoginPass { log.Printf("[SFTP] Auth OK from=%s user=%s", remote, user) return nil, nil } log.Printf("[SFTP] Auth FAIL from=%s user=%s", remote, user) return nil, fmt.Errorf("auth failed") }, } cfg.AddHostKey(signerED) cfg.AddHostKey(signerRSA) // (option compat avancée) : décommente seulement si un client antique échoue encore // cfg.Config = ssh.Config{ // KeyExchanges: []string{ // "curve25519-sha256", "curve25519-sha256@libssh.org", // "diffie-hellman-group14-sha1", // }, // Ciphers: []string{ // "chacha20-poly1305@openssh.com", // "aes128-ctr","aes192-ctr","aes256-ctr", // "aes128-cbc", // }, // MACs: []string{"hmac-sha2-256","hmac-sha2-512","hmac-sha1"}, // } // --- listener TCP avec keep-alive agressif --- ln, err := net.Listen("tcp", ":2222") if err != nil { log.Fatalf("[SFTP] Listen: %v", err) } log.Printf("[SFTP] Écoute sur sftp://%s@0.0.0.0:2222 (root=%s)", LoginUser, absBase) log.Printf("[SFTP] IP sans mot de passe autorisées: %v", AllowedIPs) for { nConn, err := ln.Accept() if err != nil { log.Printf("[SFTP] Accept err: %v", err) continue } if tc, ok := nConn.(*net.TCPConn); ok { _ = tc.SetKeepAlive(true) _ = tc.SetKeepAlivePeriod(15 * time.Second) } go func(conn net.Conn) { sshConn, chans, reqs, err := ssh.NewServerConn(conn, cfg) if err != nil { log.Printf("[SFTP] Handshake err: %v", err) _ = conn.Close() return } log.Printf("[SFTP] Client connecté: %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion()) go ssh.DiscardRequests(reqs) for newCh := range chans { if newCh.ChannelType() != "session" { newCh.Reject(ssh.UnknownChannelType, "only session channels are allowed") continue } ch, reqs, err := newCh.Accept() if err != nil { log.Printf("[SFTP] Accept channel: %v", err) continue } go func(in <-chan *ssh.Request) { for req := range in { switch req.Type { case "subsystem": // payload = uint32(len) + "sftp" if len(req.Payload) >= 4 && string(req.Payload[4:]) == "sftp" { server, err := sftp.NewServer(ch) // API minimale if err != nil { log.Printf("[SFTP] init error: %v", err) _, _ = ch.Stderr().Write([]byte("sftp init error\n")) _ = ch.Close() return } _ = req.Reply(true, nil) if err := server.Serve(); err == io.EOF { _ = server.Close() } else if err != nil { log.Printf("[SFTP] serve error: %v", err) } return } _ = req.Reply(false, nil) default: _ = req.Reply(false, nil) } } }(reqs) } }(nConn) } } // ---------- HTTP (Gin) ---------- //go:embed web/* web/**/* // toutes les ressources front embarquées var webFS embed.FS func startHTTP() { bd := db.InitDB() app := gin.Default() api := app.Group("/api/v1") routes.AddRoutes(api, bd) utils.CreateDefaultFolder(bd) // Sous-FS pointé sur "web" // /static -> ressources front app.Static("/static", "./web") app.GET("/", func(c *gin.Context) { c.File("./web/index.html") }) 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://0.0.0.0:8080") _ = app.Run(":8080") } func main() { // SFTP sur 2222 (root = ./upload) go startSFTPServer(SFTPBaseDir) // HTTP normal startHTTP() }