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"
|
|
|
|
|
|
"crypto/x509"
|
|
|
|
|
|
"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:26:00 +00:00
|
|
|
|
// ---------- SFTP (SSH natif) ----------
|
2025-07-27 14:26:30 +00:00
|
|
|
|
|
2025-08-17 13:26:00 +00:00
|
|
|
|
func loadOrCreateHostKey(path string) (ssh.Signer, error) {
|
|
|
|
|
|
// Si une clé PEM existe déjà, on la lit
|
|
|
|
|
|
if _, err := os.Stat(path); err == nil {
|
|
|
|
|
|
b, err := os.ReadFile(path)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return ssh.ParsePrivateKey(b)
|
|
|
|
|
|
}
|
2025-08-16 19:22:08 +00:00
|
|
|
|
|
2025-08-17 13:26:00 +00:00
|
|
|
|
// Sinon on génère une clé ed25519 et on la sauvegarde en PKCS#8 PEM
|
|
|
|
|
|
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
2025-08-16 19:22:08 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2025-08-17 13:26:00 +00:00
|
|
|
|
pkcs8, err := x509.MarshalPKCS8PrivateKey(priv)
|
2025-08-16 19:22:08 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2025-08-17 13:26:00 +00:00
|
|
|
|
block := &pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8}
|
|
|
|
|
|
pemBytes := pem.EncodeToMemory(block)
|
|
|
|
|
|
if err := os.WriteFile(path, pemBytes, 0o600); err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return ssh.ParsePrivateKey(pemBytes)
|
2025-07-27 18:06:11 +00:00
|
|
|
|
}
|
2025-08-16 19:22:08 +00:00
|
|
|
|
|
2025-08-17 13:26:00 +00:00
|
|
|
|
func startSFTPServer(uploadBase string) {
|
|
|
|
|
|
absBase, _ := filepath.Abs(uploadBase)
|
|
|
|
|
|
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-16 19:22:08 +00:00
|
|
|
|
|
2025-08-17 13:26:00 +00:00
|
|
|
|
// Clé hôte
|
|
|
|
|
|
signer, err := loadOrCreateHostKey("sftp_host_ed25519.pem")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Fatalf("[SFTP] Host key error: %v", err)
|
2025-07-27 19:41:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-17 13:26:00 +00:00
|
|
|
|
// Config SSH (user/pass)
|
|
|
|
|
|
conf := &ssh.ServerConfig{
|
|
|
|
|
|
PasswordCallback: func(meta ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
|
|
|
|
|
user := meta.User()
|
|
|
|
|
|
remote := meta.RemoteAddr().String()
|
|
|
|
|
|
log.Printf("[SFTP] Auth tentative user=%s from=%s", user, remote)
|
|
|
|
|
|
if user == "cangui2089" && string(pass) == "GHT30k7!" {
|
|
|
|
|
|
return nil, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil, fmt.Errorf("auth failed")
|
|
|
|
|
|
},
|
2025-07-27 19:44:19 +00:00
|
|
|
|
}
|
2025-08-17 13:26:00 +00:00
|
|
|
|
conf.AddHostKey(signer)
|
2025-07-27 19:44:19 +00:00
|
|
|
|
|
2025-08-17 13:26:00 +00:00
|
|
|
|
// Listener TCP avec keep-alive agressif
|
|
|
|
|
|
ln, err := net.Listen("tcp", ":2222")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Fatalf("[SFTP] Listen: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
log.Println("[SFTP] Écoute sur sftp://cangui2089@0.0.0.0:2222 (base visible: voir montage)")
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
// Keep-alive TCP
|
|
|
|
|
|
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) {
|
|
|
|
|
|
// Handshake SSH
|
|
|
|
|
|
sshConn, chans, reqs, err := ssh.NewServerConn(conn, conf)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Printf("[SFTP] Handshake err: %v", err)
|
|
|
|
|
|
_ = conn.Close()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
log.Printf("[SFTP] Client connecté: %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion())
|
|
|
|
|
|
|
|
|
|
|
|
// On ignore les global requests
|
|
|
|
|
|
go ssh.DiscardRequests(reqs)
|
|
|
|
|
|
|
|
|
|
|
|
// Gestion des channels (sessions)
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Sur une session, on attend un "subsystem: sftp"
|
|
|
|
|
|
go func(in <-chan *ssh.Request) {
|
|
|
|
|
|
for req := range in {
|
|
|
|
|
|
switch req.Type {
|
|
|
|
|
|
case "subsystem":
|
|
|
|
|
|
if string(req.Payload[4:]) == "sftp" { // payload = string len + "sftp"
|
|
|
|
|
|
// Lance le serveur SFTP minimal (pas d’options avancées)
|
|
|
|
|
|
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)
|
|
|
|
|
|
case "exec", "shell", "pty-req":
|
|
|
|
|
|
// On n'autorise que le sous-système SFTP
|
|
|
|
|
|
_ = req.Reply(false, nil)
|
|
|
|
|
|
default:
|
|
|
|
|
|
_ = req.Reply(false, nil)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}(reqs)
|
|
|
|
|
|
}
|
|
|
|
|
|
}(nConn)
|
|
|
|
|
|
}
|
2025-07-27 14:26:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-17 13:26:00 +00:00
|
|
|
|
// ---------- HTTP (Gin) ----------
|
2025-07-27 14:26:30 +00:00
|
|
|
|
|
2025-08-17 13:26:00 +00:00
|
|
|
|
func startHTTP() {
|
2025-07-27 14:26:30 +00:00
|
|
|
|
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")
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-08-17 13:26:00 +00:00
|
|
|
|
log.Println("[HTTP] Serveur Gin sur http://0.0.0.0:8080")
|
2025-08-16 19:22:08 +00:00
|
|
|
|
_ = app.Run(":8080")
|
2025-07-27 14:26:30 +00:00
|
|
|
|
}
|
2025-08-17 13:26:00 +00:00
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
|
// SFTP (recommandé pour VLC)
|
|
|
|
|
|
go startSFTPServer("upload")
|
|
|
|
|
|
|
|
|
|
|
|
// HTTP (ton app)
|
|
|
|
|
|
startHTTP()
|
|
|
|
|
|
}
|