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" "sync" "time" "github.com/gin-gonic/gin" "github.com/pkg/sftp" "golang.org/x/crypto/ssh" ) // ====== Config ====== var ( SFTPBaseDir = "upload" // utilisé seulement pour vérif existence + logs BindAddr = "0.0.0.0:2222" // adapte si tu veux binder sur une IP privée LoginUser = "cangui2089" LoginPass = "GHT30k7!" AllowedIPs = []string{"82.65.73.115"} // IPs autorisées (pour pass bypass) // Si tu veux fermer le service à toute IP non autorisée AVANT handshake, mets true : OnlyAllowListedBeforeHandshake = false ) // ====== Helpers ====== func ipAllowed(addr string) bool { host, _, err := net.SplitHostPort(addr) if err != nil { host = addr } for _, allow := range AllowedIPs { if host == allow { return true } } return false } func isBenignHandshakeErr(err error) bool { if err == nil { return false } s := err.Error() switch { case strings.Contains(s, "unmarshal error for field Language of type disconnectMsg"), strings.Contains(s, "connection reset by peer"), strings.Contains(s, "no common algorithm"), strings.Contains(s, "read: connection timed out"), strings.Contains(s, "unexpected message"), strings.Contains(s, "EOF"): return true } return false } func loadOrCreateEd25519(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) } func loadOrCreateRSA(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) } // ====== Rate limit très simple ====== type bucket struct { tokens int last time.Time } var rl = struct { m map[string]*bucket sync.Mutex }{m: map[string]*bucket{}} func allow(ip string, ratePerMin, burst int) bool { rl.Lock() defer rl.Unlock() now := time.Now() b := rl.m[ip] if b == nil { b = &bucket{tokens: burst, last: now} rl.m[ip] = b } elapsed := now.Sub(b.last).Minutes() add := int(elapsed * float64(ratePerMin)) if add > 0 { if b.tokens+add > burst { b.tokens = burst } else { b.tokens += add } b.last = now } if b.tokens <= 0 { return false } b.tokens-- return true } // ====== SFTP Server (sans Chdir global) ====== func startSFTPServer(baseDir, bindAddr string) { absBase, _ := filepath.Abs(baseDir) if fi, err := os.Stat(absBase); err != nil || !fi.IsDir() { log.Fatalf("[SFTP] Le dossier %s est manquant ou invalide: %v", absBase, err) } signerED, err := loadOrCreateEd25519(filepath.Join(absBase, "sftp_host_ed25519.pem")) if err != nil { log.Fatalf("[SFTP] Host key ed25519 error: %v", err) } signerRSA, err := loadOrCreateRSA(filepath.Join(absBase, "sftp_host_rsa.pem")) if err != nil { log.Fatalf("[SFTP] Host key RSA error: %v", err) } cfg := &ssh.ServerConfig{ MaxAuthTries: 2, // réduire le bruteforce PasswordCallback: func(meta ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { remote := meta.RemoteAddr().String() user := meta.User() if ipAllowed(remote) { log.Printf("[SFTP] Auth IP-allowlist OK from=%s user=%s (mdp vide accepté)", remote, user) return nil, nil } 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) ln, err := net.Listen("tcp", bindAddr) if err != nil { log.Fatalf("[SFTP] Listen(%s): %v", bindAddr, err) } log.Printf("[SFTP] Écoute sur sftp://%s@%s (base=%s)", LoginUser, bindAddr, absBase) log.Printf("[SFTP] IP sans mot de passe autorisées: %v | OnlyAllowListedBeforeHandshake=%v", AllowedIPs, OnlyAllowListedBeforeHandshake) for { nConn, err := ln.Accept() if err != nil { log.Printf("[SFTP] Accept err: %v", err) continue } remoteIP, _, _ := net.SplitHostPort(nConn.RemoteAddr().String()) // Optionnel : fermer immédiatement toute IP non allowlist if OnlyAllowListedBeforeHandshake && !ipAllowed(nConn.RemoteAddr().String()) { _ = nConn.Close() continue } // Rate limit anti-bruit if !allow(remoteIP, 6, 12) { // ~6 requêtes/min, rafale 12 _ = nConn.Close() 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 { if !isBenignHandshakeErr(err) { log.Printf("[SFTP] Handshake warn: %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" { // NB: ceci ne "jaille" pas dans absBase ; pas de Chdir global non plus. server, err := sftp.NewServer(ch) 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, BindAddr) // HTTP normal startHTTP() }