Retour à l'accueil

CMS headless en Go sans CGO et frameworks

CMS headless minimal en Go pour pages d'atterrissage sans dépendances externes. Utilise modernc/sqlite, net/http, canaux pour Bitrix24. Sécurité complète et lancement zero-config.

Création d'un CMS headless en Go : 20 MB sans dépendances
Advertisement 728x90

CMS tête-bêche minimaliste en Go pur : architecture et mise en œuvre

Le choix clé réside dans l'utilisation de modernc.org/sqlite au lieu de mattn/go-sqlite3. Il s'agit d'une portage pur Go de SQLite avec une API compatible database/sql, sans nécessiter de compilateur C.

import (
    "database/sql"
    _ "modernc.org/sqlite"
)

func Open(path string) (*sql.DB, error) {
    db, err := sql.Open("sqlite", path)
    if err != nil {
        return nil, err
    }

    // Mode WAL pour un accès concurrent
    pragmas := []string{
        "PRAGMA journal_mode=WAL",
        "PRAGMA busy_timeout=5000",
        "PRAGMA foreign_keys=ON",
        "PRAGMA synchronous=NORMAL",
    }
    for _, p := range pragmas {
        if _, err := db.Exec(p); err != nil {
            return nil, fmt.Errorf("pragma %q: %w", p, err)
        }
    }
    return db, nil
}

Le mode WAL permet la lecture parallèle pendant les écritures — essentiel pour faire fonctionner simultanément l’API et le panneau d’administration.

Routage et middleware avec net/http

En optant pour http.ServeMux plutôt que Gin/Echo/Chi, on réduit les dépendances et simplifie le débogage :

Google AdInline article slot
mux := http.NewServeMux()

// API publique
mux.Handle("/api/leads", rateLimiter(http.HandlerFunc(h.CreateLead)))
mux.Handle("/api/news", http.HandlerFunc(h.ListNews))

// Panneau d'administration
mux.Handle("/admin/", authMiddleware(sessionStore, adminHandler))

Chaîne de middleware : logger → rateLimiter → auth → csrfCheck → handler. Chaque composant est une fonction indépendante, couverte par des tests.

Intégration asynchrone avec Bitrix24

Les soumissions de leads vers le CRM sont gérées de manière asynchrone via des canaux Go et un pool de travailleurs — pas besoin de Redis ou Celery :

type Worker struct {
    db      *sql.DB
    queue   chan Job
    webhook string
    wg      sync.WaitGroup
}

func NewWorker(db *sql.DB, webhook string, poolSize int) *Worker {
    w := &Worker{
        db:      db,
        queue:   make(chan Job, 100),
        webhook: webhook,
    }
    for i := 0; i < poolSize; i++ {
        w.wg.Add(1)
        go w.process()
    }
    return w
}

func (w *Worker) Enqueue(job Job) {
    select {
    case w.queue <- job:
    default:
        log.Printf("file d'attente bitrix pleine, lead %d sera retry manuellement", job.LeadID)
    }
}

Une file de 100 tâches, arrêt propre avec timeout de 30 secondes.

Google AdInline article slot

Protection CSRF avec HMAC-SHA256

Les formulaires administrateurs utilisant HTMX sont protégés par des jetons synchronisés :

func generateCSRFToken(sessionID, secret string) string {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(sessionID))
    return hex.EncodeToString(mac.Sum(nil))
}

func validateCSRFToken(r *http.Request, sessionID, secret string) bool {
    token := r.FormValue("csrf_token")
    if token == "" {
        token = r.Header.Get("X-CSRF-Token")
    }
    expected := generateCSRFToken(sessionID, secret)
    return hmac.Equal([]byte(token), []byte(expected))
}

Les jetons sont intégrés dans les formulaires et les en-têtes HTMX. hmac.Equal empêche les attaques par timing.

Limitation de débit en mémoire vive

L’API publique limite les requêtes à 10 par minute par IP, sans stockage externe :

Google AdInline article slot
type IPLimiter struct {
    visitors sync.Map
    mu       sync.Mutex
}

type entry struct {
    count   int
    resetAt time.Time
}

func (l *IPLimiter) Allow(ip string) bool {
    now := time.Now()
    val, _ := l.visitors.LoadOrStore(ip, &entry{resetAt: now.Add(time.Minute)})
    e := val.(*entry)

    l.mu.Lock()
    defer l.mu.Unlock()

    if now.After(e.resetAt) {
        e.count = 0
        e.resetAt = now.Add(time.Minute)
    }
    if e.count >= 10 {
        return false
    }
    e.count++
    return true
}

sync.Map avec nettoyage toutes les 5 minutes.

Première exécution sans configuration

Le serveur initialise automatiquement la base de données, génère un mot de passe administrateur et l'affiche dans la console. La configuration se fait via des flags ou variables d’environnement :

  • ./cms — valeurs par défaut (port=8080, db=./cms.db)
  • CMS_PORT=8080 CMS_DB_PATH=./data.db ./cms

Mesures de sécurité

| Menace | Solution |

|--------|----------|

| Injection SQL | Requêtes paramétrées |

| XSS | html/template avec échappement automatique |

| CSRF | Jetons HMAC |

| Attaque par force brute | Limiteur à 10 requêtes/min |

| Navigation de fichiers | http.FileServer en sandbox |

Points forts :

  • Binaire ~20 Mo avec ressources statiques embarquées via embed.FS, aucune dépendance au runtime
  • SQLite en mode WAL pour la concurrence sans verrous
  • Canaux Go au lieu de files Redis/Celery
  • Sécurité robuste avec CSRF HMAC + html/template
  • CGO_ENABLED=0 — compilation croisée depuis Windows vers Linux

— Editorial Team

Advertisement 728x90

Lire ensuite