refactor: common util package, ESL/AMI/security fixes, Prometheus metrics
Backend stability and security improvements: * internal/util/ — common RandomHex helper, removed 3 duplicates * ESL: deduplicated readMessage (locked/unlocked), net.JoinHostPort for IPv6 * AMI: synchronous reconnect() in readEventsLoop, net.JoinHostPort for IPv6 * Auth: /api/auth/refresh accepts Authorization header only (no ?token=) * decodeJSON: http.MaxBytesReader(1<<20) body limit * Trunks: gatewayParams() uses configured ESL.GatewayPrefix * Config: jwt_secret_env env-var fallback * FSCollector: time.After → time.NewTimer with defer Stop * Monitoring: Prometheus counters (route_requests, nodes_total/healthy, uptime) * go fmt pass across all internal/ packages
This commit is contained in:
@@ -27,7 +27,7 @@ type claims struct {
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
userContextKey contextKey = "user"
|
||||
userContextKey contextKey = "user"
|
||||
refreshTokenKey contextKey = "refresh_token"
|
||||
)
|
||||
|
||||
@@ -39,8 +39,8 @@ type UserInfo struct {
|
||||
}
|
||||
|
||||
const (
|
||||
jwtExpiry = 15 * time.Minute
|
||||
refreshExpiry = 24 * time.Hour
|
||||
jwtExpiry = 15 * time.Minute
|
||||
refreshExpiry = 24 * time.Hour
|
||||
)
|
||||
|
||||
// generateJWT создаёт access+refresh токены.
|
||||
@@ -139,7 +139,7 @@ func (a *API) apiKeyMiddleware(next http.Handler) http.Handler {
|
||||
// handleLogin обрабатывает POST /api/auth/login.
|
||||
func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
var req models.LoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "некорректный JSON")
|
||||
return
|
||||
}
|
||||
@@ -199,10 +199,6 @@ func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
// handleRefresh выдаёт новый access токен по refresh токену.
|
||||
func (a *API) handleRefresh(w http.ResponseWriter, r *http.Request) {
|
||||
tokenStr := extractBearerToken(r)
|
||||
if tokenStr == "" {
|
||||
// пробуем получить из query param (для WebSocket)
|
||||
tokenStr = r.URL.Query().Get("token")
|
||||
}
|
||||
if tokenStr == "" {
|
||||
writeError(w, http.StatusUnauthorized, "требуется токен")
|
||||
return
|
||||
|
||||
@@ -17,12 +17,12 @@ func (a *API) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
if a.eslClient != nil {
|
||||
eslStats := a.eslClient.GetStats()
|
||||
eslInfo = map[string]interface{}{
|
||||
"status": eslStats.Status,
|
||||
"host": eslStats.Host,
|
||||
"status": eslStats.Status,
|
||||
"host": eslStats.Host,
|
||||
"uptime_seconds": eslStats.UptimeSec,
|
||||
"reconnects": eslStats.Reconnects,
|
||||
"gateway_ops": eslStats.GatewayOps,
|
||||
"events_recv": eslStats.EventsRecv,
|
||||
"reconnects": eslStats.Reconnects,
|
||||
"gateway_ops": eslStats.GatewayOps,
|
||||
"events_recv": eslStats.EventsRecv,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,26 @@ func (a *API) handlePrometheus(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
// Общие метрики (counter gauge)
|
||||
stats := a.engine.GetHealthStats()
|
||||
fmt.Fprintf(&sb, "# HELP pulse_route_requests_total Total number of route requests.\n")
|
||||
fmt.Fprintf(&sb, "# TYPE pulse_route_requests_total counter\n")
|
||||
fmt.Fprintf(&sb, "pulse_route_requests_total %d\n", stats.RouteRequests)
|
||||
|
||||
fmt.Fprintf(&sb, "# HELP pulse_nodes_total Total nodes registered.\n")
|
||||
fmt.Fprintf(&sb, "# TYPE pulse_nodes_total gauge\n")
|
||||
fmt.Fprintf(&sb, "pulse_nodes_total %d\n", stats.TotalNodes)
|
||||
|
||||
fmt.Fprintf(&sb, "# HELP pulse_nodes_healthy Healthy nodes count.\n")
|
||||
fmt.Fprintf(&sb, "# TYPE pulse_nodes_healthy gauge\n")
|
||||
fmt.Fprintf(&sb, "pulse_nodes_healthy %d\n", stats.HealthyNodes)
|
||||
|
||||
fmt.Fprintf(&sb, "# HELP pulse_uptime_seconds Uptime in seconds.\n")
|
||||
fmt.Fprintf(&sb, "# TYPE pulse_uptime_seconds gauge\n")
|
||||
fmt.Fprintf(&sb, "pulse_uptime_seconds %d\n", stats.UptimeSeconds)
|
||||
|
||||
fmt.Fprintf(&sb, "\n")
|
||||
|
||||
for _, n := range nodes {
|
||||
id := sanitizePromLabel(n.NodeID)
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
|
||||
// rateLimiter — in-memory token bucket rate limiter.
|
||||
type rateLimiter struct {
|
||||
mu sync.Mutex
|
||||
buckets map[string]*tokenBucket
|
||||
rate float64 // токенов в секунду
|
||||
burst int // максимальный размер бакета
|
||||
mu sync.Mutex
|
||||
buckets map[string]*tokenBucket
|
||||
rate float64 // токенов в секунду
|
||||
burst int // максимальный размер бакета
|
||||
cleanupInterval time.Duration
|
||||
lastCleanup time.Time
|
||||
lastCleanup time.Time
|
||||
}
|
||||
|
||||
type tokenBucket struct {
|
||||
@@ -28,11 +28,11 @@ func newRateLimiter(ratePerSec, burst int) *rateLimiter {
|
||||
burst = ratePerSec
|
||||
}
|
||||
return &rateLimiter{
|
||||
buckets: make(map[string]*tokenBucket),
|
||||
rate: float64(ratePerSec),
|
||||
burst: burst,
|
||||
buckets: make(map[string]*tokenBucket),
|
||||
rate: float64(ratePerSec),
|
||||
burst: burst,
|
||||
cleanupInterval: 60 * time.Second,
|
||||
lastCleanup: time.Now(),
|
||||
lastCleanup: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ func (a *API) handleRoute(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
writeJSON(w, http.StatusOK, models.RouteResponse{
|
||||
Fallback: true,
|
||||
SIPGateway: fallbackGW,
|
||||
SIPGateway: fallbackGW,
|
||||
Reason: "all_nodes_unhealthy",
|
||||
Nodes: nodes,
|
||||
})
|
||||
|
||||
@@ -18,8 +18,8 @@ type API struct {
|
||||
jwtSecret string
|
||||
monitoringAPIKey string
|
||||
wsHub *wsHub
|
||||
natsConnected func() bool // колбэк для проверки NATS-статуса (health)
|
||||
eslClient *esl.Client // ESL-клиент (nil если не сконфигурирован)
|
||||
natsConnected func() bool // колбэк для проверки NATS-статуса (health)
|
||||
eslClient *esl.Client // ESL-клиент (nil если не сконфигурирован)
|
||||
logFormat string
|
||||
routeLimiter *rateLimiter
|
||||
apiLimiter *rateLimiter
|
||||
@@ -141,9 +141,13 @@ func (a *API) BroadcastGatewayEvent(gatewayName, trunkID, status string) {
|
||||
a.wsHub.broadcast(msg)
|
||||
}
|
||||
|
||||
// decodeJSON декодирует тело запроса в структуру v.
|
||||
// maxBodySize — максимальный размер тела запроса в байтах (1 MiB).
|
||||
const maxBodySize = 1 << 20
|
||||
|
||||
// decodeJSON декодирует тело запроса в структуру v с ограничением размера.
|
||||
func decodeJSON(r *http.Request, v interface{}) error {
|
||||
defer r.Body.Close()
|
||||
r.Body = http.MaxBytesReader(nil, r.Body, maxBodySize)
|
||||
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
|
||||
return fmt.Errorf("декодирование JSON: %w", err)
|
||||
}
|
||||
|
||||
@@ -268,8 +268,9 @@ func (a *API) eslPushGatewayDelete(trunk models.Trunk) {
|
||||
|
||||
// gatewayParams возвращает параметры для создания FS gateway на основе транка.
|
||||
func (a *API) gatewayParams(trunk models.Trunk) (profile, name, proxy string) {
|
||||
profile = a.readConfig().ESL.SofiaProfile
|
||||
name = fmt.Sprintf("pulse-ingress-%s", trunk.ID)
|
||||
cfg := a.readConfig()
|
||||
profile = cfg.ESL.SofiaProfile
|
||||
name = fmt.Sprintf("%s-ingress-%s", cfg.ESL.GatewayPrefix, trunk.ID)
|
||||
proxy = extractHost(trunk.Gateway)
|
||||
return
|
||||
}
|
||||
|
||||
+2
-13
@@ -1,13 +1,12 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pulse-lets-go/internal/models"
|
||||
"github.com/pulse-lets-go/internal/util"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -227,15 +226,5 @@ func findUserIndex(users []models.User, id string) int {
|
||||
|
||||
// generateID генерирует ID вида "prefix-XXXXX".
|
||||
func generateID(prefix string) string {
|
||||
return fmt.Sprintf("%s-%s", prefix, randomHex(6))
|
||||
}
|
||||
|
||||
// randomHex генерирует случайную hex-строку заданной длины.
|
||||
func randomHex(n int) string {
|
||||
b := make([]byte, n)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
// fallback на time-based, если crypto/rand недоступен
|
||||
return fmt.Sprintf("%x", time.Now().UnixNano())
|
||||
}
|
||||
return hex.EncodeToString(b)[:n]
|
||||
return fmt.Sprintf("%s-%s", prefix, util.RandomHex(6))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user