Volver al inicio

CMS headless en Go sin CGO ni frameworks

CMS headless mínimo en Go para páginas de aterrizaje sin dependencias externas. Usa modernc/sqlite, net/http, canales para Bitrix24. Seguridad completa y lanzamiento sin configuración.

Creando CMS headless en Go: 20 MB sin dependencias
Advertisement 728x90

CMS sin cabeza minimalista en Go puro: Arquitectura e implementación

La elección fundamental es utilizar modernc.org/sqlite en vez de mattn/go-sqlite3. Se trata de una versión pura en Go de SQLite con una API compatible con database/sql, que no requiere compilador 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
    }

    // Modo WAL para acceso concurrente
    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
}

El modo WAL permite lecturas paralelas durante escrituras, lo cual es esencial para operar simultáneamente la API y el panel administrativo.

Enrutamiento y middleware con net/http

Evitar Gin/Echo/Chi a favor de http.ServeMux reduce dependencias y simplifica el depurado:

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

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

// Panel administrativo
mux.Handle("/admin/", authMiddleware(sessionStore, adminHandler))

Cadena de middleware: logger → rateLimiter → auth → csrfCheck → handler. Cada uno es una función independiente y bien testeada.

Integración asíncrona con Bitrix24

Los envíos de leads al CRM se gestionan de forma asíncrona mediante canales de Go y un grupo de trabajadores — sin necesidad de Redis ni 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("cola bitrix llena, lead %d será reintentado manualmente", job.LeadID)
    }
}

Un buffer de 100 tareas, cierre grácil con timeout de 30 segundos.

Google AdInline article slot

Protección CSRF con HMAC-SHA256

Los formularios administrativos con HTMX están protegidos con tokens sincronizados:

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))
}

Los tokens se incluyen en los formularios y encabezados HTMX. hmac.Equal evita ataques por tiempo.

Límite de frecuencia en memoria

La API pública limita las solicitudes a 10 por minuto por IP, sin almacenamiento externo:

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 con limpieza cada 5 minutos.

Primer arranque sin configuración

El servidor inicializa automáticamente la base de datos, genera una contraseña de administrador y la imprime en consola. La configuración se hace mediante banderas o variables de entorno:

  • ./cms — valores por defecto (puerto=8080, db=./cms.db)
  • CMS_PORT=8080 CMS_DB_PATH=./data.db ./cms

Medidas de seguridad

| Amenaza | Mitigación |

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

| Inyección SQL | Consultas parametrizadas |

| XSS | html/template con escape automático |

| CSRF | Tokens HMAC |

| Fuerza bruta | Límite de 10 req/min |

| Traversía de rutas | http.FileServer en sandbox |

Destacados clave:

  • Binario ~20 MB con recursos estáticos mediante embed.FS, sin dependencias en tiempo de ejecución
  • SQLite con modo WAL para concurrencia sin bloqueos
  • Canales de Go en lugar de colas Redis/Celery
  • CSRF con HMAC + html/template para seguridad robusta
  • CGO_ENABLED=0 — compilación cruzada desde Windows a Linux

— Editorial Team

Advertisement 728x90

Leer después