prithivMLmods commited on
Commit
27215dc
·
verified ·
1 Parent(s): 849b882

upload server

Browse files
server/package.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "appletserver",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "scripts": {
6
+ "start": "node server.js",
7
+ "dev": "nodemon server.js"
8
+ },
9
+ "dependencies": {
10
+ "axios": "^1.6.7",
11
+ "dotenv": "^16.4.5",
12
+ "express": "^4.18.2",
13
+ "express-rate-limit": "^7.5.0",
14
+ "ws": "^8.17.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^22.14.0",
18
+ "nodemon": "^3.1.0"
19
+ }
20
+ }
server/public/service-worker.js ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ // service-worker.js
7
+
8
+ // Define the target URL that we want to intercept and proxy.
9
+ const TARGET_URL_PREFIX = 'https://generativelanguage.googleapis.com';
10
+
11
+ // Installation event:
12
+ self.addEventListener('install', (event) => {
13
+ try {
14
+ console.log('Service Worker: Installing...');
15
+ event.waitUntil(self.skipWaiting());
16
+ } catch (error) {
17
+ console.error('Service Worker: Error during install event:', error);
18
+ // If skipWaiting fails, the new SW might get stuck in a waiting state.
19
+ }
20
+ });
21
+
22
+ // Activation event:
23
+ self.addEventListener('activate', (event) => {
24
+ try {
25
+ console.log('Service Worker: Activating...');
26
+ event.waitUntil(self.clients.claim());
27
+ } catch (error) {
28
+ console.error('Service Worker: Error during activate event:', error);
29
+ // If clients.claim() fails, the SW might not control existing pages until next nav.
30
+ }
31
+ });
32
+
33
+ // Fetch event:
34
+ self.addEventListener('fetch', (event) => {
35
+ try {
36
+ const requestUrl = event.request.url;
37
+
38
+ if (requestUrl.startsWith(TARGET_URL_PREFIX)) {
39
+ console.log(`Service Worker: Intercepting request to ${requestUrl}`);
40
+
41
+ const remainingPathAndQuery = requestUrl.substring(TARGET_URL_PREFIX.length);
42
+ const proxyUrl = `${self.location.origin}/api-proxy${remainingPathAndQuery}`;
43
+
44
+ console.log(`Service Worker: Proxying to ${proxyUrl}`);
45
+
46
+ // Construct headers for the request to the proxy
47
+ const newHeaders = new Headers();
48
+ // Copy essential headers from the original request
49
+ // For OPTIONS (preflight) requests, Access-Control-Request-* are critical.
50
+ // For actual requests (POST, GET), Content-Type, Accept etc.
51
+ const headersToCopy = [
52
+ 'Content-Type',
53
+ 'Accept',
54
+ 'Access-Control-Request-Method',
55
+ 'Access-Control-Request-Headers',
56
+ ];
57
+
58
+ for (const headerName of headersToCopy) {
59
+ if (event.request.headers.has(headerName)) {
60
+ newHeaders.set(headerName, event.request.headers.get(headerName));
61
+ }
62
+ }
63
+
64
+ if (event.request.method === 'POST') {
65
+
66
+ // Ensure Content-Type is set for POST requests to the proxy, defaulting to application/json
67
+ if (!newHeaders.has('Content-Type')) {
68
+ console.warn("Service Worker: POST request to proxy was missing Content-Type in newHeaders. Defaulting to application/json.");
69
+ newHeaders.set('Content-Type', 'application/json');
70
+ } else {
71
+ console.log(`Service Worker: POST request to proxy has Content-Type: ${newHeaders.get('Content-Type')}`);
72
+ }
73
+ }
74
+
75
+ const requestOptions = {
76
+ method: event.request.method,
77
+ headers: newHeaders, // Use simplified headers
78
+ body: event.request.body, // Still use the original body stream
79
+ mode: event.request.mode,
80
+ credentials: event.request.credentials,
81
+ cache: event.request.cache,
82
+ redirect: event.request.redirect,
83
+ referrer: event.request.referrer,
84
+ integrity: event.request.integrity,
85
+ };
86
+
87
+ // Only set duplex if there's a body and it's a relevant method
88
+ if (event.request.method !== 'GET' && event.request.method !== 'HEAD' && event.request.body ) {
89
+ requestOptions.duplex = 'half';
90
+ }
91
+
92
+ const promise = fetch(new Request(proxyUrl, requestOptions))
93
+ .then((response) => {
94
+ console.log(`Service Worker: Successfully proxied request to ${proxyUrl}, Status: ${response.status}`);
95
+ return response;
96
+ })
97
+ .catch((error) => {
98
+ // Log more error details
99
+ console.error(`Service Worker: Error proxying request to ${proxyUrl}. Message: ${error.message}, Name: ${error.name}, Stack: ${error.stack}`);
100
+ return new Response(
101
+ JSON.stringify({ error: 'Proxying failed', details: error.message, name: error.name, proxiedUrl: proxyUrl }),
102
+ {
103
+ status: 502, // Bad Gateway is appropriate for proxy errors
104
+ headers: { 'Content-Type': 'application/json' }
105
+ }
106
+ );
107
+ });
108
+
109
+ event.respondWith(promise);
110
+
111
+ } else {
112
+ // If the request URL doesn't match our target, let it proceed as normal.
113
+ event.respondWith(fetch(event.request));
114
+ }
115
+ } catch (error) {
116
+ // Log more error details for unhandled errors too
117
+ console.error('Service Worker: Unhandled error in fetch event handler. Message:', error.message, 'Name:', error.name, 'Stack:', error.stack);
118
+ event.respondWith(
119
+ new Response(
120
+ JSON.stringify({ error: 'Service worker fetch handler failed', details: error.message, name: error.name }),
121
+ {
122
+ status: 500,
123
+ headers: { 'Content-Type': 'application/json' }
124
+ }
125
+ )
126
+ );
127
+ }
128
+ });
server/public/websocket-interceptor.js ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (function() {
2
+ const TARGET_WS_HOST = 'generativelanguage.googleapis.com'; // Host to intercept
3
+ const originalWebSocket = window.WebSocket;
4
+
5
+ if (!originalWebSocket) {
6
+ console.error('[WebSocketInterceptor] Original window.WebSocket not found. Cannot apply interceptor.');
7
+ return;
8
+ }
9
+
10
+ const handler = {
11
+ construct(target, args) {
12
+ let [url, protocols] = args;
13
+ //stringify url's if necessary for parsing
14
+ let newUrlString = typeof url === 'string' ? url : (url && typeof url.toString === 'function' ? url.toString() : null);
15
+ //get ready to check for host to proxy
16
+ let isTarget = false;
17
+
18
+ if (newUrlString) {
19
+ try {
20
+ // For full URLs, parse string and check the host
21
+ if (newUrlString.startsWith('ws://') || newUrlString.startsWith('wss://')) {
22
+ //URL object again
23
+ const parsedUrl = new URL(newUrlString);
24
+ if (parsedUrl.host === TARGET_WS_HOST) {
25
+ isTarget = true;
26
+ //use wss if https, else ws
27
+ const proxyScheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
28
+ const proxyHost = window.location.host;
29
+ newUrlString = `${proxyScheme}://${proxyHost}/api-proxy${parsedUrl.pathname}${parsedUrl.search}`;
30
+ }
31
+ }
32
+ } catch (e) {
33
+ console.warn('[WebSocketInterceptor-Proxy] Error parsing WebSocket URL, using original:', url, e);
34
+ }
35
+ } else {
36
+ console.warn('[WebSocketInterceptor-Proxy] WebSocket URL is not a string or stringifiable. Using original.');
37
+ }
38
+
39
+ if (isTarget) {
40
+ console.log('[WebSocketInterceptor-Proxy] Original WebSocket URL:', url);
41
+ console.log('[WebSocketInterceptor-Proxy] Redirecting to proxy URL:', newUrlString);
42
+ }
43
+
44
+ // Call the original constructor with potentially modified arguments
45
+ // Reflect.construct ensures 'new target(...)' behavior and correct prototype chain
46
+ if (protocols) {
47
+ return Reflect.construct(target, [newUrlString, protocols]);
48
+ } else {
49
+ return Reflect.construct(target, [newUrlString]);
50
+ }
51
+ },
52
+ get(target, prop, receiver) {
53
+ // Forward static property access (e.g., WebSocket.OPEN, WebSocket.CONNECTING)
54
+ // and prototype access to the original WebSocket constructor/prototype
55
+ if (prop === 'prototype') {
56
+ return target.prototype;
57
+ }
58
+ return Reflect.get(target, prop, receiver);
59
+ }
60
+ };
61
+
62
+ window.WebSocket = new Proxy(originalWebSocket, handler);
63
+
64
+ console.log('[WebSocketInterceptor-Proxy] Global WebSocket constructor has been wrapped using Proxy.');
65
+ })();
server/server.js ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ require('dotenv').config();
8
+ const express = require('express');
9
+ const fs = require('fs');
10
+ const axios = require('axios');
11
+ const https = require('https');
12
+ const path = require('path');
13
+ const WebSocket = require('ws');
14
+ const { URLSearchParams, URL } = require('url');
15
+ const rateLimit = require('express-rate-limit');
16
+
17
+ const app = express();
18
+ const port = process.env.PORT || 3000;
19
+ const externalApiBaseUrl = 'https://generativelanguage.googleapis.com';
20
+ const externalWsBaseUrl = 'wss://generativelanguage.googleapis.com';
21
+ // Support either API key env-var variant
22
+ const apiKey = process.env.GEMINI_API_KEY || process.env.API_KEY;
23
+
24
+ const staticPath = path.join(__dirname,'dist');
25
+ const publicPath = path.join(__dirname,'public');
26
+
27
+
28
+ if (!apiKey) {
29
+ // Only log an error, don't exit. The server will serve apps without proxy functionality
30
+ console.error("Warning: GEMINI_API_KEY or API_KEY environment variable is not set! Proxy functionality will be disabled.");
31
+ }
32
+ else {
33
+ console.log("API KEY FOUND (proxy will use this)")
34
+ }
35
+
36
+ // Limit body size to 50mb
37
+ app.use(express.json({ limit: '50mb' }));
38
+ app.use(express.urlencoded({extended: true, limit: '50mb'}));
39
+ app.set('trust proxy', 1 /* number of proxies between user and server */)
40
+
41
+ // Rate limiter for the proxy
42
+ const proxyLimiter = rateLimit({
43
+ windowMs: 15 * 60 * 1000, // Set ratelimit window at 15min (in ms)
44
+ max: 100, // Limit each IP to 100 requests per window
45
+ message: 'Too many requests from this IP, please try again after 15 minutes',
46
+ standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
47
+ legacyHeaders: false, // no `X-RateLimit-*` headers
48
+ handler: (req, res, next, options) => {
49
+ console.warn(`Rate limit exceeded for IP: ${req.ip}. Path: ${req.path}`);
50
+ res.status(options.statusCode).send(options.message);
51
+ }
52
+ });
53
+
54
+ // Apply the rate limiter to the /api-proxy route before the main proxy logic
55
+ app.use('/api-proxy', proxyLimiter);
56
+
57
+ // Proxy route for Gemini API calls (HTTP)
58
+ app.use('/api-proxy', async (req, res, next) => {
59
+ console.log(req.ip);
60
+ // If the request is an upgrade request, it's for WebSockets, so pass to next middleware/handler
61
+ if (req.headers.upgrade && req.headers.upgrade.toLowerCase() === 'websocket') {
62
+ return next(); // Pass to the WebSocket upgrade handler
63
+ }
64
+
65
+ // Handle OPTIONS request for CORS preflight
66
+ if (req.method === 'OPTIONS') {
67
+ res.setHeader('Access-Control-Allow-Origin', '*'); // Adjust as needed for security
68
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
69
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Goog-Api-Key');
70
+ res.setHeader('Access-Control-Max-Age', '86400'); // Cache preflight response for 1 day
71
+ return res.sendStatus(200);
72
+ }
73
+
74
+ if (req.body) { // Only log body if it exists
75
+ console.log(" Request Body (from frontend):", req.body);
76
+ }
77
+ try {
78
+ // Construct the target URL by taking the part of the path after /api-proxy/
79
+ const targetPath = req.url.startsWith('/') ? req.url.substring(1) : req.url;
80
+ const apiUrl = `${externalApiBaseUrl}/${targetPath}`;
81
+ console.log(`HTTP Proxy: Forwarding request to ${apiUrl}`);
82
+
83
+ // Prepare headers for the outgoing request
84
+ const outgoingHeaders = {};
85
+ // Copy most headers from the incoming request
86
+ for (const header in req.headers) {
87
+ // Exclude host-specific headers and others that might cause issues upstream
88
+ if (!['host', 'connection', 'content-length', 'transfer-encoding', 'upgrade', 'sec-websocket-key', 'sec-websocket-version', 'sec-websocket-extensions'].includes(header.toLowerCase())) {
89
+ outgoingHeaders[header] = req.headers[header];
90
+ }
91
+ }
92
+
93
+ // Set the actual API key in the appropriate header
94
+ outgoingHeaders['X-Goog-Api-Key'] = apiKey;
95
+
96
+ // Set Content-Type from original request if present (for relevant methods)
97
+ if (req.headers['content-type'] && ['POST', 'PUT', 'PATCH'].includes(req.method.toUpperCase())) {
98
+ outgoingHeaders['Content-Type'] = req.headers['content-type'];
99
+ } else if (['POST', 'PUT', 'PATCH'].includes(req.method.toUpperCase())) {
100
+ // Default Content-Type to application/json if no content type for post/put/patch
101
+ outgoingHeaders['Content-Type'] = 'application/json';
102
+ }
103
+
104
+ // For GET or DELETE requests, ensure Content-Type is NOT sent,
105
+ // even if the client erroneously included it.
106
+ if (['GET', 'DELETE'].includes(req.method.toUpperCase())) {
107
+ delete outgoingHeaders['Content-Type']; // Case-sensitive common practice
108
+ delete outgoingHeaders['content-type']; // Just in case
109
+ }
110
+
111
+ // Ensure 'accept' is reasonable if not set
112
+ if (!outgoingHeaders['accept']) {
113
+ outgoingHeaders['accept'] = '*/*';
114
+ }
115
+
116
+
117
+ const axiosConfig = {
118
+ method: req.method,
119
+ url: apiUrl,
120
+ headers: outgoingHeaders,
121
+ responseType: 'stream',
122
+ validateStatus: function (status) {
123
+ return true; // Accept any status code, we'll pipe it through
124
+ },
125
+ };
126
+
127
+ if (['POST', 'PUT', 'PATCH'].includes(req.method.toUpperCase())) {
128
+ axiosConfig.data = req.body;
129
+ }
130
+ // For GET, DELETE, etc., axiosConfig.data will remain undefined,
131
+ // and axios will not send a request body.
132
+
133
+ const apiResponse = await axios(axiosConfig);
134
+
135
+ // Pass through response headers from Gemini API to the client
136
+ for (const header in apiResponse.headers) {
137
+ res.setHeader(header, apiResponse.headers[header]);
138
+ }
139
+ res.status(apiResponse.status);
140
+
141
+
142
+ apiResponse.data.on('data', (chunk) => {
143
+ res.write(chunk);
144
+ });
145
+
146
+ apiResponse.data.on('end', () => {
147
+ res.end();
148
+ });
149
+
150
+ apiResponse.data.on('error', (err) => {
151
+ console.error('Error during streaming data from target API:', err);
152
+ if (!res.headersSent) {
153
+ res.status(500).json({ error: 'Proxy error during streaming from target' });
154
+ } else {
155
+ // If headers already sent, we can't send a JSON error, just end the response.
156
+ res.end();
157
+ }
158
+ });
159
+
160
+ } catch (error) {
161
+ console.error('Proxy error before request to target API:', error);
162
+ if (!res.headersSent) {
163
+ if (error.response) {
164
+ const errorData = {
165
+ status: error.response.status,
166
+ message: error.response.data?.error?.message || 'Proxy error from upstream API',
167
+ details: error.response.data?.error?.details || null
168
+ };
169
+ res.status(error.response.status).json(errorData);
170
+ } else {
171
+ res.status(500).json({ error: 'Proxy setup error', message: error.message });
172
+ }
173
+ }
174
+ }
175
+ });
176
+
177
+ const webSocketInterceptorScriptTag = `<script src="/public/websocket-interceptor.js" defer></script>`;
178
+
179
+ // Prepare service worker registration script content
180
+ const serviceWorkerRegistrationScript = `
181
+ <script>
182
+ if ('serviceWorker' in navigator) {
183
+ window.addEventListener('load' , () => {
184
+ navigator.serviceWorker.register('./service-worker.js')
185
+ .then(registration => {
186
+ console.log('Service Worker registered successfully with scope:', registration.scope);
187
+ })
188
+ .catch(error => {
189
+ console.error('Service Worker registration failed:', error);
190
+ });
191
+ });
192
+ } else {
193
+ console.log('Service workers are not supported in this browser.');
194
+ }
195
+ </script>
196
+ `;
197
+
198
+ // Serve index.html or placeholder based on API key and file availability
199
+ app.get('/', (req, res) => {
200
+ const placeholderPath = path.join(publicPath, 'placeholder.html');
201
+
202
+ // Try to serve index.html
203
+ console.log("LOG: Route '/' accessed. Attempting to serve index.html.");
204
+ const indexPath = path.join(staticPath, 'index.html');
205
+
206
+ fs.readFile(indexPath, 'utf8', (err, indexHtmlData) => {
207
+ if (err) {
208
+ // index.html not found or unreadable, serve the original placeholder
209
+ console.log('LOG: index.html not found or unreadable. Falling back to original placeholder.');
210
+ return res.sendFile(placeholderPath);
211
+ }
212
+
213
+ // If API key is not set, serve original HTML without injection
214
+ if (!apiKey) {
215
+ console.log("LOG: API key not set. Serving original index.html without script injections.");
216
+ return res.sendFile(indexPath);
217
+ }
218
+
219
+ // index.html found and apiKey set, inject scripts
220
+ console.log("LOG: index.html read successfully. Injecting scripts.");
221
+ let injectedHtml = indexHtmlData;
222
+
223
+
224
+ if (injectedHtml.includes('<head>')) {
225
+ // Inject WebSocket interceptor first, then service worker script
226
+ injectedHtml = injectedHtml.replace(
227
+ '<head>',
228
+ `<head>${webSocketInterceptorScriptTag}${serviceWorkerRegistrationScript}`
229
+ );
230
+ console.log("LOG: Scripts injected into <head>.");
231
+ } else {
232
+ console.warn("WARNING: <head> tag not found in index.html. Prepending scripts to the beginning of the file as a fallback.");
233
+ injectedHtml = `${webSocketInterceptorScriptTag}${serviceWorkerRegistrationScript}${indexHtmlData}`;
234
+ }
235
+ res.send(injectedHtml);
236
+ });
237
+ });
238
+
239
+ app.get('/service-worker.js', (req, res) => {
240
+ return res.sendFile(path.join(publicPath, 'service-worker.js'));
241
+ });
242
+
243
+ app.use('/public', express.static(publicPath));
244
+ app.use(express.static(staticPath));
245
+
246
+ // Start the HTTP server
247
+ const server = app.listen(port, () => {
248
+ console.log(`Server listening on port ${port}`);
249
+ console.log(`HTTP proxy active on /api-proxy/**`);
250
+ console.log(`WebSocket proxy active on /api-proxy/**`);
251
+ });
252
+
253
+ // Create WebSocket server and attach it to the HTTP server
254
+ const wss = new WebSocket.Server({ noServer: true });
255
+
256
+ server.on('upgrade', (request, socket, head) => {
257
+ const requestUrl = new URL(request.url, `http://${request.headers.host}`);
258
+ const pathname = requestUrl.pathname;
259
+
260
+ if (pathname.startsWith('/api-proxy/')) {
261
+ if (!apiKey) {
262
+ console.error("WebSocket proxy: API key not configured. Closing connection.");
263
+ socket.destroy();
264
+ return;
265
+ }
266
+
267
+ wss.handleUpgrade(request, socket, head, (clientWs) => {
268
+ console.log('Client WebSocket connected to proxy for path:', pathname);
269
+
270
+ const targetPathSegment = pathname.substring('/api-proxy'.length);
271
+ const clientQuery = new URLSearchParams(requestUrl.search);
272
+ clientQuery.set('key', apiKey);
273
+ const targetGeminiWsUrl = `${externalWsBaseUrl}${targetPathSegment}?${clientQuery.toString()}`;
274
+ console.log(`Attempting to connect to target WebSocket: ${targetGeminiWsUrl}`);
275
+
276
+ const geminiWs = new WebSocket(targetGeminiWsUrl, {
277
+ protocol: request.headers['sec-websocket-protocol'],
278
+ });
279
+
280
+ const messageQueue = [];
281
+
282
+ geminiWs.on('open', () => {
283
+ console.log('Proxy connected to Gemini WebSocket');
284
+ // Send any queued messages
285
+ while (messageQueue.length > 0) {
286
+ const message = messageQueue.shift();
287
+ if (geminiWs.readyState === WebSocket.OPEN) {
288
+ // console.log('Sending queued message from client -> Gemini');
289
+ geminiWs.send(message);
290
+ } else {
291
+ // Should not happen if we are in 'open' event, but good for safety
292
+ console.warn('Gemini WebSocket not open when trying to send queued message. Re-queuing.');
293
+ messageQueue.unshift(message); // Add it back to the front
294
+ break; // Stop processing queue for now
295
+ }
296
+ }
297
+ });
298
+
299
+ geminiWs.on('message', (message) => {
300
+ // console.log('Message from Gemini -> client');
301
+ if (clientWs.readyState === WebSocket.OPEN) {
302
+ clientWs.send(message);
303
+ }
304
+ });
305
+
306
+ geminiWs.on('close', (code, reason) => {
307
+ console.log(`Gemini WebSocket closed: ${code} ${reason.toString()}`);
308
+ if (clientWs.readyState === WebSocket.OPEN || clientWs.readyState === WebSocket.CONNECTING) {
309
+ clientWs.close(code, reason.toString());
310
+ }
311
+ });
312
+
313
+ geminiWs.on('error', (error) => {
314
+ console.error('Error on Gemini WebSocket connection:', error);
315
+ if (clientWs.readyState === WebSocket.OPEN || clientWs.readyState === WebSocket.CONNECTING) {
316
+ clientWs.close(1011, 'Upstream WebSocket error');
317
+ }
318
+ });
319
+
320
+ clientWs.on('message', (message) => {
321
+ if (geminiWs.readyState === WebSocket.OPEN) {
322
+ // console.log('Message from client -> Gemini');
323
+ geminiWs.send(message);
324
+ } else if (geminiWs.readyState === WebSocket.CONNECTING) {
325
+ // console.log('Queueing message from client -> Gemini (Gemini still connecting)');
326
+ messageQueue.push(message);
327
+ } else {
328
+ console.warn('Client sent message but Gemini WebSocket is not open or connecting. Message dropped.');
329
+ }
330
+ });
331
+
332
+ clientWs.on('close', (code, reason) => {
333
+ console.log(`Client WebSocket closed: ${code} ${reason.toString()}`);
334
+ if (geminiWs.readyState === WebSocket.OPEN || geminiWs.readyState === WebSocket.CONNECTING) {
335
+ geminiWs.close(code, reason.toString());
336
+ }
337
+ });
338
+
339
+ clientWs.on('error', (error) => {
340
+ console.error('Error on client WebSocket connection:', error);
341
+ if (geminiWs.readyState === WebSocket.OPEN || geminiWs.readyState === WebSocket.CONNECTING) {
342
+ geminiWs.close(1011, 'Client WebSocket error');
343
+ }
344
+ });
345
+ });
346
+ } else {
347
+ console.log(`WebSocket upgrade request for non-proxy path: ${pathname}. Closing connection.`);
348
+ socket.destroy();
349
+ }
350
+ });