Add VPN configuration tools and agent documentation
Created comprehensive VPN setup tooling for Peaceful Spirit L2TP/IPsec connection and enhanced agent documentation framework. VPN Configuration (PST-NW-VPN): - Setup-PST-L2TP-VPN.ps1: Automated L2TP/IPsec setup with split-tunnel and DNS - Connect-PST-VPN.ps1: Connection helper with PPP adapter detection, DNS (192.168.0.2), and route config (192.168.0.0/24) - Connect-PST-VPN-Standalone.ps1: Self-contained connection script for remote deployment - Fix-PST-VPN-Auth.ps1: Authentication troubleshooting for CHAP/MSChapv2 - Diagnose-VPN-Interface.ps1: Comprehensive VPN interface and routing diagnostic - Quick-Test-VPN.ps1: Fast connectivity verification (DNS/router/routes) - Add-PST-VPN-Route-Manual.ps1: Manual route configuration helper - vpn-connect.bat, vpn-disconnect.bat: Simple batch file shortcuts - OpenVPN config files (Windows-compatible, abandoned for L2TP) Key VPN Implementation Details: - L2TP creates PPP adapter with connection name as interface description - UniFi auto-configures DNS (192.168.0.2) but requires manual route to 192.168.0.0/24 - Split-tunnel enabled (only remote traffic through VPN) - All-user connection for pre-login auto-connect via scheduled task - Authentication: CHAP + MSChapv2 for UniFi compatibility Agent Documentation: - AGENT_QUICK_REFERENCE.md: Quick reference for all specialized agents - documentation-squire.md: Documentation and task management specialist agent - Updated all agent markdown files with standardized formatting Project Organization: - Moved conversation logs to dedicated directories (guru-connect-conversation-logs, guru-rmm-conversation-logs) - Cleaned up old session JSONL files from projects/msp-tools/ - Added guru-connect infrastructure (agent, dashboard, proto, scripts, .gitea workflows) - Added guru-rmm server components and deployment configs Technical Notes: - VPN IP pool: 192.168.4.x (client gets 192.168.4.6) - Remote network: 192.168.0.0/24 (router at 192.168.0.10) - PSK: rrClvnmUeXEFo90Ol+z7tfsAZHeSK6w7 - Credentials: pst-admin / 24Hearts$ Files: 15 VPN scripts, 2 agent docs, conversation log reorganization, guru-connect/guru-rmm infrastructure additions Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* React hook for managing remote desktop session connection
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import type { ConnectionStatus, VideoFrame, MouseEvent as ProtoMouseEvent, KeyEvent as ProtoKeyEvent, MouseEventType, KeyEventType, Modifiers } from '../types/protocol';
|
||||
import { encodeMouseEvent, encodeKeyEvent, decodeVideoFrame } from '../lib/protobuf';
|
||||
|
||||
interface UseRemoteSessionOptions {
|
||||
serverUrl: string;
|
||||
sessionId: string;
|
||||
onFrame?: (frame: VideoFrame) => void;
|
||||
onStatusChange?: (status: ConnectionStatus) => void;
|
||||
}
|
||||
|
||||
interface UseRemoteSessionReturn {
|
||||
status: ConnectionStatus;
|
||||
connect: () => void;
|
||||
disconnect: () => void;
|
||||
sendMouseEvent: (event: ProtoMouseEvent) => void;
|
||||
sendKeyEvent: (event: ProtoKeyEvent) => void;
|
||||
}
|
||||
|
||||
export function useRemoteSession(options: UseRemoteSessionOptions): UseRemoteSessionReturn {
|
||||
const { serverUrl, sessionId, onFrame, onStatusChange } = options;
|
||||
|
||||
const [status, setStatus] = useState<ConnectionStatus>({
|
||||
connected: false,
|
||||
});
|
||||
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const reconnectTimeoutRef = useRef<number | null>(null);
|
||||
const frameCountRef = useRef(0);
|
||||
const lastFpsUpdateRef = useRef(Date.now());
|
||||
|
||||
// Update status and notify
|
||||
const updateStatus = useCallback((newStatus: Partial<ConnectionStatus>) => {
|
||||
setStatus(prev => {
|
||||
const updated = { ...prev, ...newStatus };
|
||||
onStatusChange?.(updated);
|
||||
return updated;
|
||||
});
|
||||
}, [onStatusChange]);
|
||||
|
||||
// Calculate FPS
|
||||
const updateFps = useCallback(() => {
|
||||
const now = Date.now();
|
||||
const elapsed = now - lastFpsUpdateRef.current;
|
||||
if (elapsed >= 1000) {
|
||||
const fps = Math.round((frameCountRef.current * 1000) / elapsed);
|
||||
updateStatus({ fps });
|
||||
frameCountRef.current = 0;
|
||||
lastFpsUpdateRef.current = now;
|
||||
}
|
||||
}, [updateStatus]);
|
||||
|
||||
// Handle incoming WebSocket messages
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
if (event.data instanceof Blob) {
|
||||
event.data.arrayBuffer().then(buffer => {
|
||||
const data = new Uint8Array(buffer);
|
||||
const frame = decodeVideoFrame(data);
|
||||
if (frame) {
|
||||
frameCountRef.current++;
|
||||
updateFps();
|
||||
onFrame?.(frame);
|
||||
}
|
||||
});
|
||||
} else if (event.data instanceof ArrayBuffer) {
|
||||
const data = new Uint8Array(event.data);
|
||||
const frame = decodeVideoFrame(data);
|
||||
if (frame) {
|
||||
frameCountRef.current++;
|
||||
updateFps();
|
||||
onFrame?.(frame);
|
||||
}
|
||||
}
|
||||
}, [onFrame, updateFps]);
|
||||
|
||||
// Connect to server
|
||||
const connect = useCallback(() => {
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any pending reconnect
|
||||
if (reconnectTimeoutRef.current) {
|
||||
window.clearTimeout(reconnectTimeoutRef.current);
|
||||
reconnectTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
const wsUrl = `${serverUrl}/ws/viewer?session_id=${encodeURIComponent(sessionId)}`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
|
||||
ws.onopen = () => {
|
||||
updateStatus({
|
||||
connected: true,
|
||||
sessionId,
|
||||
});
|
||||
};
|
||||
|
||||
ws.onmessage = handleMessage;
|
||||
|
||||
ws.onclose = (event) => {
|
||||
updateStatus({
|
||||
connected: false,
|
||||
latencyMs: undefined,
|
||||
fps: undefined,
|
||||
});
|
||||
|
||||
// Auto-reconnect after 2 seconds
|
||||
if (!event.wasClean) {
|
||||
reconnectTimeoutRef.current = window.setTimeout(() => {
|
||||
connect();
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
updateStatus({ connected: false });
|
||||
};
|
||||
|
||||
wsRef.current = ws;
|
||||
}, [serverUrl, sessionId, handleMessage, updateStatus]);
|
||||
|
||||
// Disconnect from server
|
||||
const disconnect = useCallback(() => {
|
||||
if (reconnectTimeoutRef.current) {
|
||||
window.clearTimeout(reconnectTimeoutRef.current);
|
||||
reconnectTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close(1000, 'User disconnected');
|
||||
wsRef.current = null;
|
||||
}
|
||||
|
||||
updateStatus({
|
||||
connected: false,
|
||||
sessionId: undefined,
|
||||
latencyMs: undefined,
|
||||
fps: undefined,
|
||||
});
|
||||
}, [updateStatus]);
|
||||
|
||||
// Send mouse event
|
||||
const sendMouseEvent = useCallback((event: ProtoMouseEvent) => {
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
const data = encodeMouseEvent(event);
|
||||
wsRef.current.send(data);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Send key event
|
||||
const sendKeyEvent = useCallback((event: ProtoKeyEvent) => {
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
const data = encodeKeyEvent(event);
|
||||
wsRef.current.send(data);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
disconnect();
|
||||
};
|
||||
}, [disconnect]);
|
||||
|
||||
return {
|
||||
status,
|
||||
connect,
|
||||
disconnect,
|
||||
sendMouseEvent,
|
||||
sendKeyEvent,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create mouse event from DOM mouse event
|
||||
*/
|
||||
export function createMouseEvent(
|
||||
domEvent: React.MouseEvent<HTMLElement>,
|
||||
canvasRect: DOMRect,
|
||||
displayWidth: number,
|
||||
displayHeight: number,
|
||||
eventType: MouseEventType
|
||||
): ProtoMouseEvent {
|
||||
// Calculate position relative to canvas and scale to display coordinates
|
||||
const scaleX = displayWidth / canvasRect.width;
|
||||
const scaleY = displayHeight / canvasRect.height;
|
||||
|
||||
const x = Math.round((domEvent.clientX - canvasRect.left) * scaleX);
|
||||
const y = Math.round((domEvent.clientY - canvasRect.top) * scaleY);
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
buttons: {
|
||||
left: (domEvent.buttons & 1) !== 0,
|
||||
right: (domEvent.buttons & 2) !== 0,
|
||||
middle: (domEvent.buttons & 4) !== 0,
|
||||
x1: (domEvent.buttons & 8) !== 0,
|
||||
x2: (domEvent.buttons & 16) !== 0,
|
||||
},
|
||||
wheelDeltaX: 0,
|
||||
wheelDeltaY: 0,
|
||||
eventType,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create key event from DOM keyboard event
|
||||
*/
|
||||
export function createKeyEvent(
|
||||
domEvent: React.KeyboardEvent<HTMLElement>,
|
||||
down: boolean
|
||||
): ProtoKeyEvent {
|
||||
const modifiers: Modifiers = {
|
||||
ctrl: domEvent.ctrlKey,
|
||||
alt: domEvent.altKey,
|
||||
shift: domEvent.shiftKey,
|
||||
meta: domEvent.metaKey,
|
||||
capsLock: domEvent.getModifierState('CapsLock'),
|
||||
numLock: domEvent.getModifierState('NumLock'),
|
||||
};
|
||||
|
||||
// Use key code for special keys, unicode for regular characters
|
||||
const isCharacter = domEvent.key.length === 1;
|
||||
|
||||
return {
|
||||
down,
|
||||
keyType: isCharacter ? 2 : 0, // KEY_UNICODE or KEY_VK
|
||||
vkCode: domEvent.keyCode,
|
||||
scanCode: 0, // Not available in browser
|
||||
unicode: isCharacter ? domEvent.key : undefined,
|
||||
modifiers,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user