The WebSocket code in logs.ts was reading from 'token' instead of 'charon_auth_token', causing all WebSocket connections to fail authentication with 401 errors. This resulted in the Security Dashboard Live Log Viewer showing "Disconnected" with rapid connect/disconnect cycling. - Changed localStorage key from 'token' to 'charon_auth_token' - Both connectLiveLogs and connectSecurityLogs functions updated
241 lines
6.8 KiB
TypeScript
241 lines
6.8 KiB
TypeScript
import client from './client';
|
|
|
|
export interface LogFile {
|
|
name: string;
|
|
size: number;
|
|
mod_time: string;
|
|
}
|
|
|
|
export interface CaddyAccessLog {
|
|
level: string;
|
|
ts: number;
|
|
logger: string;
|
|
msg: string;
|
|
request: {
|
|
remote_ip: string;
|
|
method: string;
|
|
host: string;
|
|
uri: string;
|
|
proto: string;
|
|
};
|
|
status: number;
|
|
duration: number;
|
|
size: number;
|
|
}
|
|
|
|
export interface LogResponse {
|
|
filename: string;
|
|
logs: CaddyAccessLog[];
|
|
total: number;
|
|
limit: number;
|
|
offset: number;
|
|
}
|
|
|
|
export interface LogFilter {
|
|
search?: string;
|
|
host?: string;
|
|
status?: string;
|
|
level?: string;
|
|
limit?: number;
|
|
offset?: number;
|
|
sort?: 'asc' | 'desc';
|
|
}
|
|
|
|
export const getLogs = async (): Promise<LogFile[]> => {
|
|
const response = await client.get<LogFile[]>('/logs');
|
|
return response.data;
|
|
};
|
|
|
|
export const getLogContent = async (filename: string, filter: LogFilter = {}): Promise<LogResponse> => {
|
|
const params = new URLSearchParams();
|
|
if (filter.search) params.append('search', filter.search);
|
|
if (filter.host) params.append('host', filter.host);
|
|
if (filter.status) params.append('status', filter.status);
|
|
if (filter.level) params.append('level', filter.level);
|
|
if (filter.limit) params.append('limit', filter.limit.toString());
|
|
if (filter.offset) params.append('offset', filter.offset.toString());
|
|
if (filter.sort) params.append('sort', filter.sort);
|
|
|
|
const response = await client.get<LogResponse>(`/logs/${filename}?${params.toString()}`);
|
|
return response.data;
|
|
};
|
|
|
|
export const downloadLog = (filename: string) => {
|
|
// Direct window location change to trigger download
|
|
// We need to use the base URL from the client config if possible,
|
|
// but for now we assume relative path works with the proxy setup
|
|
window.location.href = `/api/v1/logs/${filename}/download`;
|
|
};
|
|
|
|
export interface LiveLogEntry {
|
|
level: string;
|
|
timestamp: string;
|
|
message: string;
|
|
source?: string;
|
|
data?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface LiveLogFilter {
|
|
level?: string;
|
|
source?: string;
|
|
}
|
|
|
|
/**
|
|
* SecurityLogEntry represents a security-relevant log entry from Cerberus.
|
|
* This matches the backend SecurityLogEntry struct from /api/v1/cerberus/logs/ws
|
|
*/
|
|
export interface SecurityLogEntry {
|
|
timestamp: string;
|
|
level: string;
|
|
logger: string;
|
|
client_ip: string;
|
|
method: string;
|
|
uri: string;
|
|
status: number;
|
|
duration: number;
|
|
size: number;
|
|
user_agent: string;
|
|
host: string;
|
|
source: 'waf' | 'crowdsec' | 'ratelimit' | 'acl' | 'normal';
|
|
blocked: boolean;
|
|
block_reason?: string;
|
|
details?: Record<string, unknown>;
|
|
}
|
|
|
|
/**
|
|
* Filters for the Cerberus security logs WebSocket endpoint.
|
|
*/
|
|
export interface SecurityLogFilter {
|
|
source?: string; // Filter by security module: waf, crowdsec, ratelimit, acl, normal
|
|
level?: string; // Filter by log level: info, warn, error
|
|
ip?: string; // Filter by client IP (partial match)
|
|
host?: string; // Filter by host (partial match)
|
|
blocked_only?: boolean; // Only show blocked requests
|
|
}
|
|
|
|
/**
|
|
* Connects to the live logs WebSocket endpoint.
|
|
* Returns a function to close the connection.
|
|
*/
|
|
export const connectLiveLogs = (
|
|
filters: LiveLogFilter,
|
|
onMessage: (log: LiveLogEntry) => void,
|
|
onOpen?: () => void,
|
|
onError?: (error: Event) => void,
|
|
onClose?: () => void
|
|
): (() => void) => {
|
|
const params = new URLSearchParams();
|
|
if (filters.level) params.append('level', filters.level);
|
|
if (filters.source) params.append('source', filters.source);
|
|
|
|
// Get auth token from localStorage (key: charon_auth_token)
|
|
const token = localStorage.getItem('charon_auth_token');
|
|
if (token) {
|
|
params.append('token', token);
|
|
}
|
|
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const wsUrl = `${protocol}//${window.location.host}/api/v1/logs/live?${params.toString()}`;
|
|
|
|
console.log('Connecting to WebSocket:', wsUrl);
|
|
const ws = new WebSocket(wsUrl);
|
|
|
|
ws.onopen = () => {
|
|
console.log('WebSocket connection established');
|
|
onOpen?.();
|
|
};
|
|
|
|
ws.onmessage = (event: MessageEvent) => {
|
|
try {
|
|
const log = JSON.parse(event.data) as LiveLogEntry;
|
|
onMessage(log);
|
|
} catch (err) {
|
|
console.error('Failed to parse log message:', err);
|
|
}
|
|
};
|
|
|
|
ws.onerror = (error: Event) => {
|
|
console.error('WebSocket error:', error);
|
|
onError?.(error);
|
|
};
|
|
|
|
ws.onclose = (event: CloseEvent) => {
|
|
console.log('WebSocket connection closed', { code: event.code, reason: event.reason, wasClean: event.wasClean });
|
|
onClose?.();
|
|
};
|
|
|
|
return () => {
|
|
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
ws.close();
|
|
}
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Connects to the Cerberus security logs WebSocket endpoint.
|
|
* This streams parsed Caddy access logs with security event annotations.
|
|
*
|
|
* @param filters - Optional filters for source, level, IP, host, and blocked_only
|
|
* @param onMessage - Callback for each received SecurityLogEntry
|
|
* @param onOpen - Callback when connection is established
|
|
* @param onError - Callback on connection error
|
|
* @param onClose - Callback when connection closes
|
|
* @returns A function to close the WebSocket connection
|
|
*/
|
|
export const connectSecurityLogs = (
|
|
filters: SecurityLogFilter,
|
|
onMessage: (log: SecurityLogEntry) => void,
|
|
onOpen?: () => void,
|
|
onError?: (error: Event) => void,
|
|
onClose?: () => void
|
|
): (() => void) => {
|
|
const params = new URLSearchParams();
|
|
if (filters.source) params.append('source', filters.source);
|
|
if (filters.level) params.append('level', filters.level);
|
|
if (filters.ip) params.append('ip', filters.ip);
|
|
if (filters.host) params.append('host', filters.host);
|
|
if (filters.blocked_only) params.append('blocked_only', 'true');
|
|
|
|
// Get auth token from localStorage (key: charon_auth_token)
|
|
const token = localStorage.getItem('charon_auth_token');
|
|
if (token) {
|
|
params.append('token', token);
|
|
}
|
|
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const wsUrl = `${protocol}//${window.location.host}/api/v1/cerberus/logs/ws?${params.toString()}`;
|
|
|
|
console.log('Connecting to Cerberus logs WebSocket:', wsUrl);
|
|
const ws = new WebSocket(wsUrl);
|
|
|
|
ws.onopen = () => {
|
|
console.log('Cerberus logs WebSocket connection established');
|
|
onOpen?.();
|
|
};
|
|
|
|
ws.onmessage = (event: MessageEvent) => {
|
|
try {
|
|
const log = JSON.parse(event.data) as SecurityLogEntry;
|
|
onMessage(log);
|
|
} catch (err) {
|
|
console.error('Failed to parse security log message:', err);
|
|
}
|
|
};
|
|
|
|
ws.onerror = (error: Event) => {
|
|
console.error('Cerberus logs WebSocket error:', error);
|
|
onError?.(error);
|
|
};
|
|
|
|
ws.onclose = (event: CloseEvent) => {
|
|
console.log('Cerberus logs WebSocket closed', { code: event.code, reason: event.reason, wasClean: event.wasClean });
|
|
onClose?.();
|
|
};
|
|
|
|
return () => {
|
|
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
ws.close();
|
|
}
|
|
};
|
|
};
|