upload server
Browse files- server/package.json +20 -0
- server/public/service-worker.js +128 -0
- server/public/websocket-interceptor.js +65 -0
- server/server.js +350 -0
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 |
+
});
|