This commit is contained in:
Lizandro Guarnizo
2026-05-12 10:29:00 -05:00
parent f1d73ae63b
commit 40131e7266
3 changed files with 204 additions and 35 deletions
+151 -21
View File
@@ -1,6 +1,7 @@
package controllers
import (
"fmt"
"strings"
"unicode"
@@ -9,12 +10,30 @@ import (
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
)
// sanitizeCFToken elimina espacios, saltos de línea, BOM y cualquier caracter
// no imprimible que pueda romper la autenticación Bearer de Cloudflare.
// sanitizeCFToken limpia el token antes de usarlo como Bearer en Cloudflare.
// Cubre los casos más comunes de pegado incorrecto desde el dashboard:
// - BOM UTF-8
// - Prefijo "Bearer " copiado accidentalmente
// - Comillas simples/dobles alrededor del token
// - Espacios y caracteres de control (tabulaciones, saltos de línea)
// - Caracteres fuera del rango ASCII imprimible (U+0021U+007E)
func sanitizeCFToken(s string) string {
// Eliminar BOM UTF-8 si existe
// 1. BOM UTF-8
s = strings.TrimPrefix(s, "\xef\xbb\xbf")
// Filtrar solo rómanos imprimibles (printable ASCII, excluye control chars)
// 2. Espacios al inicio/fin (cubre paste con espacio visible)
s = strings.TrimSpace(s)
// 3. Prefijo "Bearer " copiado accidentalmente (case-insensitive)
if after, ok := strings.CutPrefix(strings.ToLower(s), "bearer "); ok {
s = s[len(s)-len(after):] // preservar casing original del token
}
s = strings.TrimSpace(s)
// 4. Comillas envolventes (simples o dobles)
if len(s) >= 2 {
if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') {
s = s[1 : len(s)-1]
}
}
// 5. Solo ASCII imprimible (U+0021U+007E), descarta control chars y unicode
var b strings.Builder
for _, r := range s {
if r > 32 && r < 127 && unicode.IsPrint(r) {
@@ -40,13 +59,15 @@ func CloudflareConfigPage(c *fiber.Ctx) error {
return nil
}
// SaveCloudflareConfig guarda o actualiza el API token de Cloudflare.
// SaveCloudflareConfig guarda o actualiza las credenciales de Cloudflare.
func SaveCloudflareConfig(c *fiber.Ctx) error {
type body struct {
ID uint `json:"id" form:"id"`
ID uint `json:"id" form:"id"`
AuthType string `json:"auth_type" form:"auth_type"` // "token" o "global_key"
APIToken string `json:"api_token" form:"api_token"`
Email string `json:"email" form:"email"`
AccountID string `json:"account_id" form:"account_id"`
Nota string `json:"nota" form:"nota"`
Nota string `json:"nota" form:"nota"`
}
var b body
if err := c.BodyParser(&b); err != nil {
@@ -55,9 +76,17 @@ func SaveCloudflareConfig(c *fiber.Ctx) error {
if b.APIToken == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "api_token requerido"})
}
if b.AuthType == "" {
b.AuthType = "token"
}
if b.AuthType == "global_key" && b.Email == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "email requerido para auth_type=global_key"})
}
cfg := models.CloudflareConfig{
AuthType: b.AuthType,
APIToken: sanitizeCFToken(b.APIToken),
Email: strings.TrimSpace(b.Email),
AccountID: sanitizeCFToken(b.AccountID),
Nota: b.Nota,
}
@@ -81,6 +110,7 @@ func CleanCloudflareToken(c *fiber.Ctx) error {
before := len([]rune(cfg.APIToken))
cfg.APIToken = sanitizeCFToken(cfg.APIToken)
cfg.AccountID = sanitizeCFToken(cfg.AccountID)
cfg.Email = strings.TrimSpace(cfg.Email)
after := len([]rune(cfg.APIToken))
if err := models.SaveCloudflareConfig(*cfg); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
@@ -98,10 +128,17 @@ func cloudflareClient() (*services.CloudflareClient, error) {
if err != nil {
return nil, err
}
if cfg.AuthType == "global_key" {
return services.NewCloudflareClientGlobalKey(
strings.TrimSpace(cfg.Email),
sanitizeCFToken(cfg.APIToken),
sanitizeCFToken(cfg.AccountID),
), nil
}
return services.NewCloudflareClient(sanitizeCFToken(cfg.APIToken), sanitizeCFToken(cfg.AccountID)), nil
}
// VerifyCloudflareToken verifica el token y devuelve sus permisos reales.
// VerifyCloudflareToken verifica las credenciales y devuelve sus permisos reales.
func VerifyCloudflareToken(c *fiber.Ctx) error {
cfg, err := models.GetCloudflareConfig()
if err != nil {
@@ -110,31 +147,124 @@ func VerifyCloudflareToken(c *fiber.Ctx) error {
"error": "No hay configuración activa de Cloudflare",
})
}
raw := cfg.APIToken
token := sanitizeCFToken(raw)
if token == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"ok": false, "error": "Token vacío"})
}
dirty := len([]rune(raw)) != len([]rune(token))
dirty := raw != token
runes := []rune(token)
preview := ""
if len(runes) >= 8 {
preview = string(runes[:4]) + "…" + string(runes[len(runes)-4:])
preview := token
if len(runes) > 14 {
preview = string(runes[:6]) + "…" + string(runes[len(runes)-6:])
}
client := services.NewCloudflareClient(token, sanitizeCFToken(cfg.AccountID))
isGlobal := cfg.AuthType == "global_key"
// 1. Verificar token
// Diagnóstico de formato según tipo
var tokenDiag fiber.Map
if isGlobal {
hintFormat := ""
if len(token) != 37 {
hintFormat = fmt.Sprintf(
"Longitud %d ≠ 37. La Global API Key tiene exactamente 37 caracteres hexadecimales.",
len(token),
)
}
tokenDiag = fiber.Map{
"auth_type": "global_key",
"length": len(token),
"expected_length": "37 caracteres (Global API Key)",
"preview": preview,
"was_sanitized": dirty,
"email": cfg.Email,
"hint_format": hintFormat,
}
} else {
hintFormat := ""
if len(token) != 40 {
hintFormat = fmt.Sprintf(
"Longitud %d ≠ 40. Posible token incompleto, con prefijo extra, o tipo incorrecto (¿estás usando Global API Key en vez de API Token?)",
len(token),
)
}
tokenDiag = fiber.Map{
"auth_type": "token",
"length": len(token),
"expected_length": "40 caracteres (API Token estándar)",
"preview": preview,
"was_sanitized": dirty,
"raw_length": len([]rune(raw)),
"hint_format": hintFormat,
}
}
var client *services.CloudflareClient
if isGlobal {
client = services.NewCloudflareClientGlobalKey(
strings.TrimSpace(cfg.Email),
token,
sanitizeCFToken(cfg.AccountID),
)
} else {
client = services.NewCloudflareClient(token, sanitizeCFToken(cfg.AccountID))
}
// ── Para Global API Key: no existe /user/tokens/verify, verificamos con /user ──
if isGlobal {
user, err := client.GetUser()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"ok": false,
"step": "get_user",
"error": err.Error(),
"token": tokenDiag,
"hint": "Verifica que el email y la Global API Key sean correctos. La Global API Key se encuentra en dash.cloudflare.com → Mi Perfil → API Tokens → Global API Key → View.",
})
}
zones, zonesErr := client.GetZones()
zonesOK := zonesErr == nil
zonesErrMsg := ""
if zonesErr != nil {
zonesErrMsg = zonesErr.Error()
}
dnsOK := false
dnsErrMsg := ""
dnsZoneID := ""
if zonesOK && len(zones) > 0 {
dnsZoneID = zones[0].ID
_, dnsErr := client.GetDNSRecords(dnsZoneID)
dnsOK = dnsErr == nil
if dnsErr != nil {
dnsErrMsg = dnsErr.Error()
}
}
return c.JSON(fiber.Map{
"ok": true,
"auth_type": "global_key",
"user_email": user.Email,
"user_id": user.ID,
"token": tokenDiag,
"permissions_test": fiber.Map{
"zone_read": fiber.Map{"ok": zonesOK, "error": zonesErrMsg},
"dns_read": fiber.Map{"ok": dnsOK, "error": dnsErrMsg, "zone_tested": dnsZoneID},
},
})
}
// ── API Token normal ──
verify, err := client.VerifyToken()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"ok": false,
"step": "verify_token",
"error": err.Error(),
"hint": "Token inválido. Crea uno nuevo en dash.cloudflare.com → Mi Perfil → API Tokens",
"token_len": len(token),
"token_prev": preview,
"was_dirty": dirty,
"ok": false,
"step": "verify_token",
"error": err.Error(),
"token": tokenDiag,
"hint": "Ve a dash.cloudflare.com → Mi Perfil → API Tokens → crea un token nuevo con permisos Zone:Read y DNS:Read/Write",
})
}