-
Notifications
You must be signed in to change notification settings - Fork 1
π‘οΈ Sentinel: [CRITICAL] Fix insecure go2rtc API binding #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -23,6 +23,7 @@ import { PrinterStatusData, WebSocketCommand, WebSocketMessage } from '@shared/t | |||||||||||||||||||||||||||||||||||||
| import { EventEmitter } from 'events'; | ||||||||||||||||||||||||||||||||||||||
| import * as http from 'http'; | ||||||||||||||||||||||||||||||||||||||
| import { RawData, WebSocket, WebSocketServer } from 'ws'; | ||||||||||||||||||||||||||||||||||||||
| import { getGo2rtcService } from '../../services/Go2rtcService.js'; | ||||||||||||||||||||||||||||||||||||||
| import { getPrinterBackendManager } from '../../managers/PrinterBackendManager.js'; | ||||||||||||||||||||||||||||||||||||||
| import { getPrinterContextManager } from '../../managers/PrinterContextManager.js'; | ||||||||||||||||||||||||||||||||||||||
| import type { SpoolmanChangedEvent } from '../../services/SpoolmanIntegrationService.js'; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -69,6 +70,7 @@ export class WebSocketManager extends EventEmitter { | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // WebSocket server | ||||||||||||||||||||||||||||||||||||||
| private wss: WebSocketServer | null = null; | ||||||||||||||||||||||||||||||||||||||
| private cameraProxyWss: WebSocketServer | null = null; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Client tracking | ||||||||||||||||||||||||||||||||||||||
| private readonly clients: Map<WebSocket, ClientInfo> = new Map(); | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -113,6 +115,15 @@ export class WebSocketManager extends EventEmitter { | |||||||||||||||||||||||||||||||||||||
| // Setup event handlers | ||||||||||||||||||||||||||||||||||||||
| this.wss.on('connection', this.handleConnection.bind(this)); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Create Camera Proxy WebSocket server | ||||||||||||||||||||||||||||||||||||||
| this.cameraProxyWss = new WebSocketServer({ | ||||||||||||||||||||||||||||||||||||||
| server: httpServer, | ||||||||||||||||||||||||||||||||||||||
| path: '/api/camera/stream', | ||||||||||||||||||||||||||||||||||||||
| verifyClient: this.verifyClient.bind(this), | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| this.cameraProxyWss.on('connection', this.handleCameraProxyConnection.bind(this)); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Setup Spoolman integration event listener | ||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| const spoolmanService = getSpoolmanIntegrationService(); | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -673,10 +684,114 @@ export class WebSocketManager extends EventEmitter { | |||||||||||||||||||||||||||||||||||||
| console.log('WebSocket server shut down'); | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (this.cameraProxyWss) { | ||||||||||||||||||||||||||||||||||||||
| this.cameraProxyWss.close(() => { | ||||||||||||||||||||||||||||||||||||||
| console.log('Camera Proxy WebSocket server shut down'); | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| this.cameraProxyWss = null; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| this.wss = null; | ||||||||||||||||||||||||||||||||||||||
| this.isRunning = false; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * Handle new Camera Proxy WebSocket connection | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
| private handleCameraProxyConnection(ws: WebSocket, req: http.IncomingMessage): void { | ||||||||||||||||||||||||||||||||||||||
| const extendedReq = req as ExtendedIncomingMessage; | ||||||||||||||||||||||||||||||||||||||
| const token = extendedReq.wsToken; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (this.authManager.isAuthenticationRequired() && !token) { | ||||||||||||||||||||||||||||||||||||||
| console.error('[CameraProxy] Connection without token'); | ||||||||||||||||||||||||||||||||||||||
| ws.close(1008, 'Token required'); | ||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| // Extract query parameters | ||||||||||||||||||||||||||||||||||||||
| const url = new URL(req.url || '', `http://${req.headers.host}`); | ||||||||||||||||||||||||||||||||||||||
| const src = url.searchParams.get('src'); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (!src) { | ||||||||||||||||||||||||||||||||||||||
| console.error('[CameraProxy] Missing src parameter'); | ||||||||||||||||||||||||||||||||||||||
| ws.close(1008, 'Missing src parameter'); | ||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+712
to
+720
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The camera proxy WebSocket endpoint accepts an arbitrary
Suggested change
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Get go2rtc API port | ||||||||||||||||||||||||||||||||||||||
| const go2rtcService = getGo2rtcService(); | ||||||||||||||||||||||||||||||||||||||
| if (!go2rtcService.isRunning()) { | ||||||||||||||||||||||||||||||||||||||
| console.error('[CameraProxy] go2rtc service not running'); | ||||||||||||||||||||||||||||||||||||||
| ws.close(1011, 'Camera service unavailable'); | ||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const apiPort = go2rtcService.getApiPort(); | ||||||||||||||||||||||||||||||||||||||
| const targetUrl = `ws://127.0.0.1:${apiPort}/api/ws?src=${encodeURIComponent(src)}`; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| console.log(`[CameraProxy] Proxying connection for ${src} to ${targetUrl}`); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Create upstream connection | ||||||||||||||||||||||||||||||||||||||
| const upstreamWs = new WebSocket(targetUrl); | ||||||||||||||||||||||||||||||||||||||
| const messageBuffer: RawData[] = []; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Handle client messages immediately to prevent data loss during connection | ||||||||||||||||||||||||||||||||||||||
| ws.on('message', (data) => { | ||||||||||||||||||||||||||||||||||||||
| if (upstreamWs.readyState === WebSocket.OPEN) { | ||||||||||||||||||||||||||||||||||||||
| upstreamWs.send(data); | ||||||||||||||||||||||||||||||||||||||
| } else if (upstreamWs.readyState === WebSocket.CONNECTING) { | ||||||||||||||||||||||||||||||||||||||
| // Buffer messages while upstream is connecting | ||||||||||||||||||||||||||||||||||||||
| messageBuffer.push(data); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Setup proxy pipe | ||||||||||||||||||||||||||||||||||||||
| upstreamWs.on('open', () => { | ||||||||||||||||||||||||||||||||||||||
| // Flush buffered messages | ||||||||||||||||||||||||||||||||||||||
| while (messageBuffer.length > 0) { | ||||||||||||||||||||||||||||||||||||||
| const data = messageBuffer.shift(); | ||||||||||||||||||||||||||||||||||||||
| if (data) upstreamWs.send(data); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+752
to
+755
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using for (const data of messageBuffer) {
upstreamWs.send(data);
}
messageBuffer.length = 0; |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Forward messages from upstream to client | ||||||||||||||||||||||||||||||||||||||
| upstreamWs.on('message', (data) => { | ||||||||||||||||||||||||||||||||||||||
| if (ws.readyState === WebSocket.OPEN) { | ||||||||||||||||||||||||||||||||||||||
| ws.send(data); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Handle close events | ||||||||||||||||||||||||||||||||||||||
| ws.on('close', (code, reason) => { | ||||||||||||||||||||||||||||||||||||||
| if (upstreamWs.readyState === WebSocket.OPEN || upstreamWs.readyState === WebSocket.CONNECTING) { | ||||||||||||||||||||||||||||||||||||||
| upstreamWs.close(code, reason); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| upstreamWs.on('close', (code, reason) => { | ||||||||||||||||||||||||||||||||||||||
| if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) { | ||||||||||||||||||||||||||||||||||||||
| ws.close(code, reason); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Handle errors | ||||||||||||||||||||||||||||||||||||||
| ws.on('error', (error) => { | ||||||||||||||||||||||||||||||||||||||
| console.error('[CameraProxy] Client WebSocket error:', error); | ||||||||||||||||||||||||||||||||||||||
| upstreamWs.terminate(); | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| upstreamWs.on('error', (error) => { | ||||||||||||||||||||||||||||||||||||||
| console.error('[CameraProxy] Upstream WebSocket error:', error); | ||||||||||||||||||||||||||||||||||||||
| ws.terminate(); | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||
| console.error('[CameraProxy] Failed to setup proxy:', error); | ||||||||||||||||||||||||||||||||||||||
| ws.close(1011, 'Internal server error'); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * Dispose and cleanup | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -104,10 +104,18 @@ export function registerCameraRoutes(router: Router, deps: RouteDependencies): v | |
| return sendErrorResponse<StandardAPIResponse>(res, 503, 'Camera stream not available'); | ||
| } | ||
|
|
||
| // Build WebSocket URL for WebUI client | ||
| // WebUI needs to connect to go2rtc on the server's hostname, not localhost | ||
| const host = req.hostname || 'localhost'; | ||
| const wsUrl = `ws://${host}:${streamConfig.apiPort}/api/ws?src=${encodeURIComponent(streamConfig.streamName)}`; | ||
| // Build WebSocket URL for WebUI client to connect via proxy | ||
| // Connect to the WebUI server itself, which proxies to the local go2rtc instance | ||
| // Use headers.host to respect the port the client used (handles reverse proxies correctly) | ||
| const host = req.headers.host || 'localhost'; | ||
| const token = req.auth?.token; | ||
|
|
||
| let wsUrl = `ws://${host}/api/camera/stream?src=${encodeURIComponent(streamConfig.streamName)}`; | ||
|
|
||
| // Append token for authentication | ||
| if (token) { | ||
| wsUrl += `&token=${token}`; | ||
| } | ||
|
Comment on lines
+115
to
+118
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The session token is appended as a query parameter to the WebSocket URL. Sensitive information in query parameters can be leaked through web server logs, reverse proxy logs, and browser history. It is recommended to use a more secure method for passing authentication tokens, such as the |
||
|
|
||
| const response = { | ||
| success: true, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the
shutdownmethod, existing client connections oncameraProxyWssare not being closed. TheWebSocketServer.close()method only prevents new connections but doesn't terminate existing ones. This can lead to resource leaks as connections may remain open after server shutdown. You should iterate overthis.cameraProxyWss.clientsand explicitly close each connection, similar to how connections for the mainwssserver are handled.