|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
require('dotenv').config(); |
|
|
const express = require('express'); |
|
|
const fs = require('fs'); |
|
|
const axios = require('axios'); |
|
|
const https = require('https'); |
|
|
const path = require('path'); |
|
|
const WebSocket = require('ws'); |
|
|
const { URLSearchParams, URL } = require('url'); |
|
|
const rateLimit = require('express-rate-limit'); |
|
|
|
|
|
const app = express(); |
|
|
const port = process.env.PORT || 3000; |
|
|
const externalApiBaseUrl = 'https://generativelanguage.googleapis.com'; |
|
|
const externalWsBaseUrl = 'wss://generativelanguage.googleapis.com'; |
|
|
|
|
|
const apiKey = process.env.GEMINI_API_KEY || process.env.API_KEY; |
|
|
|
|
|
const staticPath = path.join(__dirname,'dist'); |
|
|
const publicPath = path.join(__dirname,'public'); |
|
|
|
|
|
|
|
|
if (!apiKey) { |
|
|
|
|
|
console.error("Warning: GEMINI_API_KEY or API_KEY environment variable is not set! Proxy functionality will be disabled."); |
|
|
} |
|
|
else { |
|
|
console.log("API KEY FOUND (proxy will use this)") |
|
|
} |
|
|
|
|
|
|
|
|
app.use(express.json({ limit: '50mb' })); |
|
|
app.use(express.urlencoded({extended: true, limit: '50mb'})); |
|
|
app.set('trust proxy', 1 ) |
|
|
|
|
|
|
|
|
const proxyLimiter = rateLimit({ |
|
|
windowMs: 15 * 60 * 1000, |
|
|
max: 100, |
|
|
message: 'Too many requests from this IP, please try again after 15 minutes', |
|
|
standardHeaders: true, |
|
|
legacyHeaders: false, |
|
|
handler: (req, res, next, options) => { |
|
|
console.warn(`Rate limit exceeded for IP: ${req.ip}. Path: ${req.path}`); |
|
|
res.status(options.statusCode).send(options.message); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
app.use('/api-proxy', proxyLimiter); |
|
|
|
|
|
|
|
|
app.use('/api-proxy', async (req, res, next) => { |
|
|
console.log(req.ip); |
|
|
|
|
|
if (req.headers.upgrade && req.headers.upgrade.toLowerCase() === 'websocket') { |
|
|
return next(); |
|
|
} |
|
|
|
|
|
|
|
|
if (req.method === 'OPTIONS') { |
|
|
res.setHeader('Access-Control-Allow-Origin', '*'); |
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); |
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Goog-Api-Key'); |
|
|
res.setHeader('Access-Control-Max-Age', '86400'); |
|
|
return res.sendStatus(200); |
|
|
} |
|
|
|
|
|
if (req.body) { |
|
|
console.log(" Request Body (from frontend):", req.body); |
|
|
} |
|
|
try { |
|
|
|
|
|
const targetPath = req.url.startsWith('/') ? req.url.substring(1) : req.url; |
|
|
const apiUrl = `${externalApiBaseUrl}/${targetPath}`; |
|
|
console.log(`HTTP Proxy: Forwarding request to ${apiUrl}`); |
|
|
|
|
|
|
|
|
const outgoingHeaders = {}; |
|
|
|
|
|
for (const header in req.headers) { |
|
|
|
|
|
if (!['host', 'connection', 'content-length', 'transfer-encoding', 'upgrade', 'sec-websocket-key', 'sec-websocket-version', 'sec-websocket-extensions'].includes(header.toLowerCase())) { |
|
|
outgoingHeaders[header] = req.headers[header]; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
outgoingHeaders['X-Goog-Api-Key'] = apiKey; |
|
|
|
|
|
|
|
|
if (req.headers['content-type'] && ['POST', 'PUT', 'PATCH'].includes(req.method.toUpperCase())) { |
|
|
outgoingHeaders['Content-Type'] = req.headers['content-type']; |
|
|
} else if (['POST', 'PUT', 'PATCH'].includes(req.method.toUpperCase())) { |
|
|
|
|
|
outgoingHeaders['Content-Type'] = 'application/json'; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (['GET', 'DELETE'].includes(req.method.toUpperCase())) { |
|
|
delete outgoingHeaders['Content-Type']; |
|
|
delete outgoingHeaders['content-type']; |
|
|
} |
|
|
|
|
|
|
|
|
if (!outgoingHeaders['accept']) { |
|
|
outgoingHeaders['accept'] = '*/*'; |
|
|
} |
|
|
|
|
|
|
|
|
const axiosConfig = { |
|
|
method: req.method, |
|
|
url: apiUrl, |
|
|
headers: outgoingHeaders, |
|
|
responseType: 'stream', |
|
|
validateStatus: function (status) { |
|
|
return true; |
|
|
}, |
|
|
}; |
|
|
|
|
|
if (['POST', 'PUT', 'PATCH'].includes(req.method.toUpperCase())) { |
|
|
axiosConfig.data = req.body; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const apiResponse = await axios(axiosConfig); |
|
|
|
|
|
|
|
|
for (const header in apiResponse.headers) { |
|
|
res.setHeader(header, apiResponse.headers[header]); |
|
|
} |
|
|
res.status(apiResponse.status); |
|
|
|
|
|
|
|
|
apiResponse.data.on('data', (chunk) => { |
|
|
res.write(chunk); |
|
|
}); |
|
|
|
|
|
apiResponse.data.on('end', () => { |
|
|
res.end(); |
|
|
}); |
|
|
|
|
|
apiResponse.data.on('error', (err) => { |
|
|
console.error('Error during streaming data from target API:', err); |
|
|
if (!res.headersSent) { |
|
|
res.status(500).json({ error: 'Proxy error during streaming from target' }); |
|
|
} else { |
|
|
|
|
|
res.end(); |
|
|
} |
|
|
}); |
|
|
|
|
|
} catch (error) { |
|
|
console.error('Proxy error before request to target API:', error); |
|
|
if (!res.headersSent) { |
|
|
if (error.response) { |
|
|
const errorData = { |
|
|
status: error.response.status, |
|
|
message: error.response.data?.error?.message || 'Proxy error from upstream API', |
|
|
details: error.response.data?.error?.details || null |
|
|
}; |
|
|
res.status(error.response.status).json(errorData); |
|
|
} else { |
|
|
res.status(500).json({ error: 'Proxy setup error', message: error.message }); |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
const webSocketInterceptorScriptTag = `<script src="/public/websocket-interceptor.js" defer></script>`; |
|
|
|
|
|
|
|
|
const serviceWorkerRegistrationScript = ` |
|
|
<script> |
|
|
if ('serviceWorker' in navigator) { |
|
|
window.addEventListener('load' , () => { |
|
|
navigator.serviceWorker.register('./service-worker.js') |
|
|
.then(registration => { |
|
|
console.log('Service Worker registered successfully with scope:', registration.scope); |
|
|
}) |
|
|
.catch(error => { |
|
|
console.error('Service Worker registration failed:', error); |
|
|
}); |
|
|
}); |
|
|
} else { |
|
|
console.log('Service workers are not supported in this browser.'); |
|
|
} |
|
|
</script> |
|
|
`; |
|
|
|
|
|
|
|
|
app.get('/', (req, res) => { |
|
|
const placeholderPath = path.join(publicPath, 'placeholder.html'); |
|
|
|
|
|
|
|
|
console.log("LOG: Route '/' accessed. Attempting to serve index.html."); |
|
|
const indexPath = path.join(staticPath, 'index.html'); |
|
|
|
|
|
fs.readFile(indexPath, 'utf8', (err, indexHtmlData) => { |
|
|
if (err) { |
|
|
|
|
|
console.log('LOG: index.html not found or unreadable. Falling back to original placeholder.'); |
|
|
return res.sendFile(placeholderPath); |
|
|
} |
|
|
|
|
|
|
|
|
if (!apiKey) { |
|
|
console.log("LOG: API key not set. Serving original index.html without script injections."); |
|
|
return res.sendFile(indexPath); |
|
|
} |
|
|
|
|
|
|
|
|
console.log("LOG: index.html read successfully. Injecting scripts."); |
|
|
let injectedHtml = indexHtmlData; |
|
|
|
|
|
|
|
|
if (injectedHtml.includes('<head>')) { |
|
|
|
|
|
injectedHtml = injectedHtml.replace( |
|
|
'<head>', |
|
|
`<head>${webSocketInterceptorScriptTag}${serviceWorkerRegistrationScript}` |
|
|
); |
|
|
console.log("LOG: Scripts injected into <head>."); |
|
|
} else { |
|
|
console.warn("WARNING: <head> tag not found in index.html. Prepending scripts to the beginning of the file as a fallback."); |
|
|
injectedHtml = `${webSocketInterceptorScriptTag}${serviceWorkerRegistrationScript}${indexHtmlData}`; |
|
|
} |
|
|
res.send(injectedHtml); |
|
|
}); |
|
|
}); |
|
|
|
|
|
app.get('/service-worker.js', (req, res) => { |
|
|
return res.sendFile(path.join(publicPath, 'service-worker.js')); |
|
|
}); |
|
|
|
|
|
app.use('/public', express.static(publicPath)); |
|
|
app.use(express.static(staticPath)); |
|
|
|
|
|
|
|
|
const server = app.listen(port, () => { |
|
|
console.log(`Server listening on port ${port}`); |
|
|
console.log(`HTTP proxy active on /api-proxy/**`); |
|
|
console.log(`WebSocket proxy active on /api-proxy/**`); |
|
|
}); |
|
|
|
|
|
|
|
|
const wss = new WebSocket.Server({ noServer: true }); |
|
|
|
|
|
server.on('upgrade', (request, socket, head) => { |
|
|
const requestUrl = new URL(request.url, `http://${request.headers.host}`); |
|
|
const pathname = requestUrl.pathname; |
|
|
|
|
|
if (pathname.startsWith('/api-proxy/')) { |
|
|
if (!apiKey) { |
|
|
console.error("WebSocket proxy: API key not configured. Closing connection."); |
|
|
socket.destroy(); |
|
|
return; |
|
|
} |
|
|
|
|
|
wss.handleUpgrade(request, socket, head, (clientWs) => { |
|
|
console.log('Client WebSocket connected to proxy for path:', pathname); |
|
|
|
|
|
const targetPathSegment = pathname.substring('/api-proxy'.length); |
|
|
const clientQuery = new URLSearchParams(requestUrl.search); |
|
|
clientQuery.set('key', apiKey); |
|
|
const targetGeminiWsUrl = `${externalWsBaseUrl}${targetPathSegment}?${clientQuery.toString()}`; |
|
|
console.log(`Attempting to connect to target WebSocket: ${targetGeminiWsUrl}`); |
|
|
|
|
|
const geminiWs = new WebSocket(targetGeminiWsUrl, { |
|
|
protocol: request.headers['sec-websocket-protocol'], |
|
|
}); |
|
|
|
|
|
const messageQueue = []; |
|
|
|
|
|
geminiWs.on('open', () => { |
|
|
console.log('Proxy connected to Gemini WebSocket'); |
|
|
|
|
|
while (messageQueue.length > 0) { |
|
|
const message = messageQueue.shift(); |
|
|
if (geminiWs.readyState === WebSocket.OPEN) { |
|
|
|
|
|
geminiWs.send(message); |
|
|
} else { |
|
|
|
|
|
console.warn('Gemini WebSocket not open when trying to send queued message. Re-queuing.'); |
|
|
messageQueue.unshift(message); |
|
|
break; |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
geminiWs.on('message', (message) => { |
|
|
|
|
|
if (clientWs.readyState === WebSocket.OPEN) { |
|
|
clientWs.send(message); |
|
|
} |
|
|
}); |
|
|
|
|
|
geminiWs.on('close', (code, reason) => { |
|
|
console.log(`Gemini WebSocket closed: ${code} ${reason.toString()}`); |
|
|
if (clientWs.readyState === WebSocket.OPEN || clientWs.readyState === WebSocket.CONNECTING) { |
|
|
clientWs.close(code, reason.toString()); |
|
|
} |
|
|
}); |
|
|
|
|
|
geminiWs.on('error', (error) => { |
|
|
console.error('Error on Gemini WebSocket connection:', error); |
|
|
if (clientWs.readyState === WebSocket.OPEN || clientWs.readyState === WebSocket.CONNECTING) { |
|
|
clientWs.close(1011, 'Upstream WebSocket error'); |
|
|
} |
|
|
}); |
|
|
|
|
|
clientWs.on('message', (message) => { |
|
|
if (geminiWs.readyState === WebSocket.OPEN) { |
|
|
|
|
|
geminiWs.send(message); |
|
|
} else if (geminiWs.readyState === WebSocket.CONNECTING) { |
|
|
|
|
|
messageQueue.push(message); |
|
|
} else { |
|
|
console.warn('Client sent message but Gemini WebSocket is not open or connecting. Message dropped.'); |
|
|
} |
|
|
}); |
|
|
|
|
|
clientWs.on('close', (code, reason) => { |
|
|
console.log(`Client WebSocket closed: ${code} ${reason.toString()}`); |
|
|
if (geminiWs.readyState === WebSocket.OPEN || geminiWs.readyState === WebSocket.CONNECTING) { |
|
|
geminiWs.close(code, reason.toString()); |
|
|
} |
|
|
}); |
|
|
|
|
|
clientWs.on('error', (error) => { |
|
|
console.error('Error on client WebSocket connection:', error); |
|
|
if (geminiWs.readyState === WebSocket.OPEN || geminiWs.readyState === WebSocket.CONNECTING) { |
|
|
geminiWs.close(1011, 'Client WebSocket error'); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
} else { |
|
|
console.log(`WebSocket upgrade request for non-proxy path: ${pathname}. Closing connection.`); |
|
|
socket.destroy(); |
|
|
} |
|
|
}); |
|
|
|