# frozen_string_literal: true class Api::EventsController < ApplicationController skip_before_action :verify_authenticity_token allow_unauthenticated_access # Skip normal session auth, use DSN auth instead # POST /api/:project_id/events def create project = authenticate_project! return head :not_found unless project # Parse the incoming WAF event data event_data = parse_event_data(request) # Create event asynchronously ProcessWafEventJob.perform_later( project_id: project.id, event_data: event_data, headers: extract_serializable_headers(request) ) # Include rule version in response for agent optimization rule_version = Rule.latest_version response.headers['X-Rule-Version'] = rule_version.to_s # Get current sampling for back-pressure management current_sampling = HubLoad.current_sampling response.headers['X-Sample-Rate'] = current_sampling[:allowed_requests].to_s response.headers['X-Sample-Until'] = current_sampling[:effective_until] # Check if agent sent a rule version in the JSON body to compare against client_version = event_data.dig('last_rule_sync')&.to_i response_data = { success: true, rule_version: rule_version, sampling: current_sampling } # If agent has old rules or no version, include new rules in response if client_version.blank? || client_version != rule_version # Get rules updated since client version if client_version.present? rules = Rule.since(client_version).enabled else # Full sync for new agents rules = Rule.active.sync_order end response_data[:rules] = rules.map(&:to_agent_format) response_data[:rules_changed] = true else response_data[:rules_changed] = false end render json: response_data rescue DsnAuthenticationService::AuthenticationError => e Rails.logger.warn "DSN authentication failed: #{e.message}" head :unauthorized rescue JSON::ParserError => e Rails.logger.error "Invalid JSON in event data: #{e.message}" head :bad_request end private def authenticate_project! DsnAuthenticationService.authenticate(request, params[:project_id]) end def parse_event_data(request) # Handle different content types content_type = request.content_type || "application/json" case content_type when /application\/json/ JSON.parse(request.body.read) when /application\/x-www-form-urlencoded/ # Convert form data to JSON-like hash request.request_parameters else # Try to parse as JSON anyway JSON.parse(request.body.read) end rescue => e Rails.logger.error "Failed to parse event data: #{e.message}" {} ensure request.body.rewind if request.body.respond_to?(:rewind) end def extract_serializable_headers(request) # Only extract the headers we need for analytics, avoiding IO objects important_headers = %w[ User-Agent Content-Type Content-Length Accept X-Forwarded-For X-Real-IP X-Forwarded-Proto Authorization X-Baffle-Auth X-Sentry-Auth Referer Accept-Language Accept-Encoding ] headers = {} important_headers.each do |header| value = request.headers[header] # Standardize headers to lower case during import phase headers[header.downcase] = value if value.present? end headers end end