8cf05a1576
Add a secret-masking slog.Handler that automatically replaces registered passwords with "***" in all log output. Secrets are registered per-scan when a discovery request arrives and unregistered when it completes. This approach masks credentials everywhere they appear in logs — URL userinfo, query parameters, path segments, and Go HTTP error messages — without modifying any business logic in scanner, builder, tester, or ONVIF components. API responses are unaffected and still return full URLs with credentials for frontend use.
141 lines
3.8 KiB
Go
141 lines
3.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/go-playground/validator/v10"
|
|
"github.com/eduard256/Strix/internal/camera/discovery"
|
|
"github.com/eduard256/Strix/internal/models"
|
|
"github.com/eduard256/Strix/internal/utils/logger"
|
|
"github.com/eduard256/Strix/pkg/sse"
|
|
)
|
|
|
|
// DiscoverHandler handles stream discovery requests
|
|
type DiscoverHandler struct {
|
|
scanner *discovery.Scanner
|
|
sseServer *sse.Server
|
|
validator *validator.Validate
|
|
secrets *logger.SecretStore
|
|
logger interface{ Debug(string, ...any); Error(string, error, ...any); Info(string, ...any) }
|
|
}
|
|
|
|
// NewDiscoverHandler creates a new discover handler
|
|
func NewDiscoverHandler(
|
|
scanner *discovery.Scanner,
|
|
sseServer *sse.Server,
|
|
secrets *logger.SecretStore,
|
|
logger interface{ Debug(string, ...any); Error(string, error, ...any); Info(string, ...any) },
|
|
) *DiscoverHandler {
|
|
return &DiscoverHandler{
|
|
scanner: scanner,
|
|
sseServer: sseServer,
|
|
secrets: secrets,
|
|
validator: validator.New(),
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// ServeHTTP handles discovery requests
|
|
func (h *DiscoverHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Parse request body
|
|
var req models.StreamDiscoveryRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
h.logger.Error("failed to decode discovery request", err)
|
|
h.sendErrorResponse(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Set defaults
|
|
if req.ModelLimit <= 0 {
|
|
req.ModelLimit = 6
|
|
}
|
|
if req.Timeout <= 0 {
|
|
req.Timeout = 240 // 4 minutes
|
|
}
|
|
if req.MaxStreams <= 0 {
|
|
req.MaxStreams = 10
|
|
}
|
|
|
|
// Validate request
|
|
if err := h.validator.Struct(req); err != nil {
|
|
h.logger.Error("discovery request validation failed", err)
|
|
h.sendErrorResponse(w, "Validation failed: "+err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Register password as a secret so it gets masked in all log output.
|
|
// The secret is automatically unregistered when the request completes.
|
|
if req.Password != "" {
|
|
h.secrets.Add(req.Password)
|
|
defer h.secrets.Remove(req.Password)
|
|
}
|
|
|
|
h.logger.Info("stream discovery requested",
|
|
"target", req.Target,
|
|
"model", req.Model,
|
|
"timeout", req.Timeout,
|
|
"max_streams", req.MaxStreams,
|
|
"remote_addr", r.RemoteAddr,
|
|
)
|
|
|
|
// Check if SSE is supported
|
|
flusher, ok := w.(http.Flusher)
|
|
if !ok {
|
|
h.logger.Info("SSE not supported by client", "remote_addr", r.RemoteAddr)
|
|
h.sendErrorResponse(w, "SSE not supported", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Set SSE headers
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
w.Header().Set("Connection", "keep-alive")
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("X-Accel-Buffering", "no") // Disable Nginx buffering
|
|
|
|
// Flush headers
|
|
flusher.Flush()
|
|
|
|
// Create SSE stream writer
|
|
streamWriter, err := h.sseServer.NewStreamWriter(w, r)
|
|
if err != nil {
|
|
h.logger.Error("failed to create SSE stream", err)
|
|
return
|
|
}
|
|
defer streamWriter.Close()
|
|
|
|
// Perform discovery
|
|
result, err := h.scanner.Scan(r.Context(), req, streamWriter)
|
|
if err != nil {
|
|
h.logger.Error("discovery failed", err)
|
|
_ = streamWriter.SendError(err)
|
|
return
|
|
}
|
|
|
|
h.logger.Info("discovery completed",
|
|
"target", req.Target,
|
|
"tested", result.TotalTested,
|
|
"found", result.TotalFound,
|
|
"duration", result.Duration,
|
|
)
|
|
}
|
|
|
|
// sendErrorResponse sends an error response for non-SSE requests
|
|
func (h *DiscoverHandler) sendErrorResponse(w http.ResponseWriter, message string, statusCode int) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(statusCode)
|
|
|
|
response := map[string]interface{}{
|
|
"error": true,
|
|
"message": message,
|
|
"code": statusCode,
|
|
}
|
|
|
|
_ = json.NewEncoder(w).Encode(response)
|
|
} |