shelfy-v2/main.go

286 lines
9.3 KiB
Go
Raw Normal View History

2025-07-27 14:26:30 +00:00
package main
import (
"canguidev/shelfy/internal/db"
"canguidev/shelfy/internal/routes"
"canguidev/shelfy/internal/utils"
2025-08-17 13:26:00 +00:00
"crypto/ed25519"
"crypto/rand"
2025-08-17 13:39:20 +00:00
"crypto/rsa"
2025-08-17 13:26:00 +00:00
"crypto/x509"
2025-08-18 17:37:38 +00:00
"embed"
2025-08-17 13:26:00 +00:00
"encoding/pem"
"fmt"
"io"
2025-07-27 14:26:30 +00:00
"log"
2025-07-27 19:49:30 +00:00
"net"
2025-07-27 14:26:30 +00:00
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
2025-08-17 13:26:00 +00:00
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
2025-07-27 14:26:30 +00:00
)
2025-08-17 13:39:20 +00:00
// --------- 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",
}
)
// -------------------------------------
2025-07-27 14:26:30 +00:00
2025-08-18 16:04:49 +00:00
// ---------- 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.
2025-08-16 19:22:08 +00:00
2025-08-17 13:39:20 +00:00
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)
2025-08-17 13:26:00 +00:00
if fi, err := os.Stat(absBase); err != nil || !fi.IsDir() {
log.Fatalf("[SFTP] Le dossier %s est manquant ou invalide: %v", absBase, err)
2025-07-27 17:06:27 +00:00
}
2025-08-17 13:39:20 +00:00
if err := os.Chdir(absBase); err != nil {
log.Fatalf("[SFTP] Chdir(%s): %v", absBase, err)
2025-07-27 19:41:53 +00:00
}
2025-08-17 13:39:20 +00:00
// --- 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{
2025-08-17 13:26:00 +00:00
PasswordCallback: func(meta ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
remote := meta.RemoteAddr().String()
2025-08-17 13:39:20 +00:00
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)
2025-08-17 13:26:00 +00:00
return nil, nil
}
2025-08-17 13:39:20 +00:00
// 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)
2025-08-17 13:26:00 +00:00
return nil, fmt.Errorf("auth failed")
},
2025-07-27 19:44:19 +00:00
}
2025-08-17 13:39:20 +00:00
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"},
// }
2025-07-27 19:44:19 +00:00
2025-08-17 13:39:20 +00:00
// --- listener TCP avec keep-alive agressif ---
2025-08-17 13:26:00 +00:00
ln, err := net.Listen("tcp", ":2222")
2025-08-17 13:39:20 +00:00
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)
2025-08-16 19:22:08 +00:00
2025-08-17 13:26:00 +00:00
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)
}
2025-07-27 18:06:11 +00:00
2025-08-17 13:26:00 +00:00
go func(conn net.Conn) {
2025-08-17 13:39:20 +00:00
sshConn, chans, reqs, err := ssh.NewServerConn(conn, cfg)
2025-08-17 13:26:00 +00:00
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":
2025-08-17 13:39:20 +00:00
// payload = uint32(len) + "sftp"
if len(req.Payload) >= 4 && string(req.Payload[4:]) == "sftp" {
server, err := sftp.NewServer(ch) // API minimale
2025-08-17 13:26:00 +00:00
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)
}
2025-07-27 14:26:30 +00:00
}
2025-08-17 13:39:20 +00:00
2025-08-17 13:26:00 +00:00
// ---------- HTTP (Gin) ----------
2025-07-27 14:26:30 +00:00
2025-08-18 17:37:38 +00:00
//go:embed web/* web/**/*
// toutes les ressources front embarquées
var webFS embed.FS
2025-08-17 13:26:00 +00:00
func startHTTP() {
2025-08-18 17:37:38 +00:00
bd := db.InitDB()
app := gin.Default()
api := app.Group("/api/v1")
routes.AddRoutes(api, bd)
utils.CreateDefaultFolder(bd)
// Sous-FS pointé sur "web"
2025-08-18 18:39:25 +00:00
// /static -> ressources front
app.Static("/static", "./web")
app.GET("/", func(c *gin.Context) { c.File("./web/index.html") })
2025-08-18 17:37:38 +00:00
app.NoRoute(func(c *gin.Context) {
2025-08-18 18:39:25 +00:00
if strings.HasPrefix(c.Request.URL.Path, "/api/") {
c.JSON(404, gin.H{"error":"Not found"}); return
}
c.File("./web/index.html")
2025-08-18 17:37:38 +00:00
})
2025-08-18 18:39:25 +00:00
log.Println("[HTTP] Serveur Gin sur http://0.0.0.0:8080")
_ = app.Run(":8080")
}
2025-08-18 16:20:06 +00:00
2025-08-18 17:21:56 +00:00
2025-08-17 13:26:00 +00:00
func main() {
2025-08-18 17:04:02 +00:00
// SFTP sur 2222 (root = ./upload)
2025-08-18 18:48:13 +00:00
//go startSFTPServer(SFTPBaseDir)
2025-08-17 13:26:00 +00:00
2025-08-18 17:04:02 +00:00
// HTTP normal
startHTTP()
2025-08-17 13:26:00 +00:00
}
2025-08-18 17:04:02 +00:00
func loadOrCreateRSAHostKey(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)
}
// Génère une clé RSA 2048
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil { return nil, err }
// Encode en PEM "RSA PRIVATE KEY" (PKCS#1)
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)
}