-
Notifications
You must be signed in to change notification settings - Fork 1.5k
feat: add WebSocket proxy for plugin backends #553
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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,7 +65,7 @@ import userRoutes from './routes/user.js'; | |
| import codexRoutes from './routes/codex.js'; | ||
| import geminiRoutes from './routes/gemini.js'; | ||
| import pluginsRoutes from './routes/plugins.js'; | ||
| import { startEnabledPluginServers, stopAllPlugins } from './utils/plugin-process-manager.js'; | ||
| import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js'; | ||
| import { initializeDatabase, sessionNamesDb, applyCustomSessionNames } from './database/db.js'; | ||
| import { configureWebPush } from './services/vapid-keys.js'; | ||
| import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js'; | ||
|
|
@@ -1396,6 +1396,50 @@ const uploadFilesHandler = async (req, res) => { | |
|
|
||
| app.post('/api/projects/:projectName/files/upload', authenticateToken, uploadFilesHandler); | ||
|
|
||
| /** | ||
| * Proxy an authenticated client WebSocket to a plugin's internal WS server. | ||
| * Auth is enforced by verifyClient before this function is reached. | ||
| */ | ||
| function handlePluginWsProxy(clientWs, pathname) { | ||
| const pluginName = pathname.replace('/plugin-ws/', ''); | ||
| if (!pluginName || /[^a-zA-Z0-9_-]/.test(pluginName)) { | ||
| clientWs.close(4400, 'Invalid plugin name'); | ||
| return; | ||
| } | ||
|
|
||
| const port = getPluginPort(pluginName); | ||
| if (!port) { | ||
| clientWs.close(4404, 'Plugin not running'); | ||
| return; | ||
| } | ||
|
|
||
| const upstream = new WebSocket(`ws://127.0.0.1:${port}/ws`); | ||
|
|
||
| upstream.on('open', () => { | ||
| console.log(`[Plugins] WS proxy connected to "${pluginName}" on port ${port}`); | ||
| }); | ||
|
|
||
| // Relay messages bidirectionally | ||
| upstream.on('message', (data) => { | ||
| if (clientWs.readyState === WebSocket.OPEN) clientWs.send(data); | ||
| }); | ||
| clientWs.on('message', (data) => { | ||
| if (upstream.readyState === WebSocket.OPEN) upstream.send(data); | ||
| }); | ||
|
Comment on lines
+1416
to
+1428
Contributor
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. Potential message loss during upstream connection phase. Messages sent by the client before the upstream WebSocket reaches the Consider buffering client messages until the upstream connection is established: 🔧 Proposed fix to buffer messages until upstream is ready function handlePluginWsProxy(clientWs, pathname) {
const pluginName = pathname.replace('/plugin-ws/', '');
if (!pluginName || /[^a-zA-Z0-9_-]/.test(pluginName)) {
clientWs.close(4400, 'Invalid plugin name');
return;
}
const port = getPluginPort(pluginName);
if (!port) {
clientWs.close(4404, 'Plugin not running');
return;
}
const upstream = new WebSocket(`ws://127.0.0.1:${port}/ws`);
+ const pendingMessages = [];
upstream.on('open', () => {
console.log(`[Plugins] WS proxy connected to "${pluginName}" on port ${port}`);
+ // Flush any messages that arrived before upstream was ready
+ for (const msg of pendingMessages) {
+ upstream.send(msg);
+ }
+ pendingMessages.length = 0;
});
// Relay messages bidirectionally
upstream.on('message', (data) => {
if (clientWs.readyState === WebSocket.OPEN) clientWs.send(data);
});
clientWs.on('message', (data) => {
- if (upstream.readyState === WebSocket.OPEN) upstream.send(data);
+ if (upstream.readyState === WebSocket.OPEN) {
+ upstream.send(data);
+ } else if (upstream.readyState === WebSocket.CONNECTING) {
+ pendingMessages.push(data);
+ }
+ // If upstream is CLOSING or CLOSED, drop the message (connection is terminating)
});🤖 Prompt for AI Agents |
||
|
|
||
| // Propagate close in both directions | ||
| upstream.on('close', () => { if (clientWs.readyState === WebSocket.OPEN) clientWs.close(); }); | ||
| clientWs.on('close', () => { if (upstream.readyState === WebSocket.OPEN) upstream.close(); }); | ||
|
|
||
| upstream.on('error', (err) => { | ||
| console.error(`[Plugins] WS proxy error for "${pluginName}":`, err.message); | ||
| if (clientWs.readyState === WebSocket.OPEN) clientWs.close(4502, 'Upstream error'); | ||
| }); | ||
| clientWs.on('error', () => { | ||
| if (upstream.readyState === WebSocket.OPEN) upstream.close(); | ||
| }); | ||
| } | ||
|
|
||
| // WebSocket connection handler that routes based on URL path | ||
| wss.on('connection', (ws, request) => { | ||
| const url = request.url; | ||
|
|
@@ -1409,6 +1453,8 @@ wss.on('connection', (ws, request) => { | |
| handleShellConnection(ws); | ||
| } else if (pathname === '/ws') { | ||
| handleChatConnection(ws, request); | ||
| } else if (pathname.startsWith('/plugin-ws/')) { | ||
| handlePluginWsProxy(ws, pathname); | ||
| } else { | ||
| console.log('[WARN] Unknown WebSocket path:', pathname); | ||
| ws.close(); | ||
|
|
||
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.
🧩 Analysis chain
🏁 Script executed:
Repository: siteboon/claudecodeui
Length of output: 262
🌐 Web query:
cloudcli-ai cloudcli-plugin-terminal GitHub repository💡 Result:
The GitHub repository for cloudcli-ai / cloudcli-plugin-terminal is:
CloudCLI GitHub org (shows the repo in its repository list): [2]
References:
[1] https://github.com/cloudcli-ai/cloudcli-plugin-terminal
[2] https://github.com/cloudcli-ai
🏁 Script executed:
Repository: siteboon/claudecodeui
Length of output: 98
Add space before the closing pipe for consistent table formatting.
The plugin entry follows the existing format correctly, and the repository is publicly accessible. However, there's a minor spacing inconsistency: add a space before the closing pipe to match the formatting of other rows in the table.
Formatting fix
📝 Committable suggestion
🤖 Prompt for AI Agents