secubox-openwrt/package/secubox/secubox-ai-gateway/files/usr/lib/ai-gateway/proxy.sh
CyberMind-FR f3cea01792 feat(ai-gateway): Add Data Classifier (Sovereignty Engine) for ANSSI CSPN
Implement secubox-ai-gateway package with intelligent AI request routing
based on data sensitivity classification for GDPR/ANSSI compliance.

Features:
- 3-tier data classification: LOCAL_ONLY, SANITIZED, CLOUD_DIRECT
- Provider hierarchy: LocalAI > Mistral (EU) > Claude > GPT > Gemini > xAI
- PII sanitizer: IPv4/IPv6, MAC, credentials, private keys scrubbing
- OpenAI-compatible API proxy on port 4050
- aigatewayctl CLI: status, classify, sanitize, provider, audit commands
- RPCD backend with 11 ubus methods for LuCI integration
- ANSSI CSPN audit logging in JSONL format

Classification patterns detect:
- IP addresses, MAC addresses, private keys
- Credentials (password, secret, token, api_key)
- System paths, security tool references
- WireGuard configuration data

All cloud providers are opt-in. Default LOCAL_ONLY ensures data
sovereignty - sensitive data never leaves the device.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-28 17:55:22 +01:00

216 lines
5.2 KiB
Bash

#!/bin/sh
# SecuBox AI Gateway - HTTP Proxy Handler
# OpenAI-compatible API on port 4000
. /usr/lib/ai-gateway/classifier.sh
. /usr/lib/ai-gateway/sanitizer.sh
. /usr/lib/ai-gateway/providers.sh
. /usr/lib/ai-gateway/audit.sh
CONFIG="ai-gateway"
# Generate unique request ID
generate_request_id() {
echo "$(date +%s%N | md5sum | head -c 16)"
}
# Handle incoming chat completion request
# Input: JSON request body
# Output: JSON response
handle_chat_completion() {
local request_json="$1"
local request_id=$(generate_request_id)
local start_time=$(date +%s%3N)
# 1. Initialize patterns for classification
init_patterns
# 2. Classify the request
classify_request "$request_json"
local class_num=$?
local classification=$(classification_to_string $class_num)
# 3. Check offline mode
local offline_mode=$(uci -q get ${CONFIG}.main.offline_mode)
if [ "$offline_mode" = "1" ]; then
classification="local_only"
fi
# 4. Select provider
local provider=$(select_provider "$classification")
if [ -z "$provider" ]; then
printf '{"error":{"message":"No provider available for classification: %s","type":"provider_error","code":"no_provider"}}' "$classification"
return 1
fi
local model=$(uci -q get ${CONFIG}.${provider}.model)
# 5. Sanitize if needed
local sanitized="0"
if [ "$classification" = "sanitized" ]; then
request_json=$(sanitize_request "$request_json")
sanitized="1"
fi
# 6. Log request
init_audit
audit_log_request "$request_id" "$classification" "$provider" "$model" "$sanitized"
# 7. Route to provider
local response=$(route_request "$request_json" "$classification")
local status="success"
echo "$response" | grep -q '"error"' && status="error"
# 8. Log response
local end_time=$(date +%s%3N)
local latency=$((end_time - start_time))
audit_log_response "$request_id" "$status" "$latency"
echo "$response"
}
# Handle models list request
handle_models() {
local models='{"object":"list","data":['
local first=1
for provider in localai mistral claude openai gemini xai; do
local enabled=$(uci -q get ${CONFIG}.${provider}.enabled)
[ "$enabled" != "1" ] && continue
local model=$(uci -q get ${CONFIG}.${provider}.model)
[ -z "$model" ] && continue
[ $first -eq 0 ] && models="${models},"
first=0
models="${models}{\"id\":\"${model}\",\"object\":\"model\",\"owned_by\":\"${provider}\"}"
done
models="${models}]}"
echo "$models"
}
# Handle health check
handle_health() {
local localai_status="down"
local endpoint=$(uci -q get ${CONFIG}.localai.endpoint || echo "http://127.0.0.1:8081")
wget -q -O /dev/null --timeout=2 "${endpoint}/readyz" 2>/dev/null && localai_status="up"
printf '{"status":"ok","localai":"%s","version":"1.0.0"}' "$localai_status"
}
# Simple HTTP request parser
# Reads from stdin, outputs: METHOD PATH BODY
parse_http_request() {
local line method path version
local content_length=0
local body=""
# Read request line
read -r line
method=$(echo "$line" | cut -d' ' -f1)
path=$(echo "$line" | cut -d' ' -f2)
# Read headers
while read -r line; do
line=$(echo "$line" | tr -d '\r')
[ -z "$line" ] && break
case "$line" in
Content-Length:*)
content_length=$(echo "$line" | cut -d':' -f2 | tr -d ' ')
;;
esac
done
# Read body
if [ "$content_length" -gt 0 ]; then
body=$(head -c "$content_length")
fi
echo "$method"
echo "$path"
echo "$body"
}
# HTTP response helper
send_response() {
local status="$1"
local body="$2"
local content_type="${3:-application/json}"
printf "HTTP/1.1 %s\r\n" "$status"
printf "Content-Type: %s\r\n" "$content_type"
printf "Content-Length: %d\r\n" "${#body}"
printf "Access-Control-Allow-Origin: *\r\n"
printf "\r\n"
printf "%s" "$body"
}
# Main HTTP handler
http_handler() {
local request=$(parse_http_request)
local method=$(echo "$request" | head -1)
local path=$(echo "$request" | head -2 | tail -1)
local body=$(echo "$request" | tail -n +3)
case "$method $path" in
"POST /v1/chat/completions")
local response=$(handle_chat_completion "$body")
send_response "200 OK" "$response"
;;
"POST /v1/completions")
# Legacy completions API - convert to chat format
local response=$(handle_chat_completion "$body")
send_response "200 OK" "$response"
;;
"GET /v1/models")
local response=$(handle_models)
send_response "200 OK" "$response"
;;
"GET /health"|"GET /readyz")
local response=$(handle_health)
send_response "200 OK" "$response"
;;
"OPTIONS "*)
send_response "200 OK" ""
;;
*)
send_response "404 Not Found" '{"error":"Not found"}'
;;
esac
}
# Start proxy server (called by init.d)
start_proxy() {
local port=$(uci -q get ${CONFIG}.main.proxy_port || echo 4000)
local host=$(uci -q get ${CONFIG}.main.proxy_host || echo 127.0.0.1)
logger -t ai-gateway "Starting proxy on ${host}:${port}"
# Use socat for simple TCP server
# Each connection spawns this script with http_handler
if command -v socat >/dev/null 2>&1; then
exec socat TCP-LISTEN:${port},bind=${host},reuseaddr,fork EXEC:"/usr/lib/ai-gateway/proxy.sh handle"
else
# Fallback: simple nc loop (less robust)
while true; do
nc -l -p "$port" -e /usr/lib/ai-gateway/proxy.sh handle
done
fi
}
# Entry point
case "$1" in
handle)
http_handler
;;
start)
start_proxy
;;
*)
echo "Usage: proxy.sh <start|handle>"
;;
esac