Skip to content

Commit 14ee237

Browse files
committed
Update TunnelService.ts
1 parent a6494d4 commit 14ee237

File tree

1 file changed

+58
-219
lines changed

1 file changed

+58
-219
lines changed

src/services/TunnelService.ts

Lines changed: 58 additions & 219 deletions
Original file line numberDiff line numberDiff line change
@@ -141,76 +141,9 @@ export class TunnelService extends EventEmitter {
141141
}
142142

143143
private async validateTargetService(target: string, port: number): Promise<boolean> {
144-
const maxRetries = 3;
145-
const retryDelay = 1000; // 1 second
146-
147-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
148-
try {
149-
logger.info(`Validating target service (attempt ${attempt}/${maxRetries})`, {
150-
target,
151-
port
152-
});
153-
154-
const isAvailable = await new Promise<boolean>((resolve) => {
155-
const socket = new net.Socket();
156-
157-
socket.setTimeout(2000); // 2 second timeout
158-
159-
socket.on('connect', () => {
160-
logger.info(`Successfully connected to target service`, {
161-
target,
162-
port,
163-
attempt
164-
});
165-
socket.destroy();
166-
resolve(true);
167-
});
168-
169-
socket.on('timeout', () => {
170-
logger.warn(`Connection attempt timed out`, {
171-
target,
172-
port,
173-
attempt
174-
});
175-
socket.destroy();
176-
resolve(false);
177-
});
178-
179-
socket.on('error', (err) => {
180-
logger.warn(`Connection attempt failed`, {
181-
target,
182-
port,
183-
attempt,
184-
error: err.message
185-
});
186-
socket.destroy();
187-
resolve(false);
188-
});
189-
190-
const host = target.replace(/^https?:\/\//, '').split(':')[0];
191-
logger.info(`Attempting to connect to ${host}:${port}`);
192-
socket.connect(port, host);
193-
});
194-
195-
if (isAvailable) {
196-
return true;
197-
}
198-
199-
if (attempt < maxRetries) {
200-
logger.info(`Retrying connection after ${retryDelay}ms...`);
201-
await new Promise(resolve => setTimeout(resolve, retryDelay));
202-
}
203-
} catch (error) {
204-
logger.error(`Error during service validation`, {
205-
target,
206-
port,
207-
attempt,
208-
error
209-
});
210-
}
211-
}
212-
213-
return false;
144+
// In a VM setup, we can't directly validate the client's local port
145+
// Instead, we'll trust that the client has verified its local port
146+
return true;
214147
}
215148

216149
public async proxyRequestWrapper(req: IncomingMessage, res: ServerResponse): Promise<void> {
@@ -241,168 +174,74 @@ export class TunnelService extends EventEmitter {
241174
}
242175

243176
try {
244-
const target = tunnelConfig.targetUrl || (tunnelConfig.targetPort ? `http://localhost:${tunnelConfig.targetPort}` : undefined);
245-
if (!target) {
246-
logger.error(`No target URL or port found for tunnel: ${subdomain}`, {
247-
tunnel: {
248-
subdomain: tunnelConfig.subdomain,
249-
targetPort: tunnelConfig.targetPort,
250-
targetUrl: tunnelConfig.targetUrl
251-
}
252-
});
253-
res.writeHead(502);
254-
res.end(JSON.stringify({ error: 'Tunnel target not configured' }));
255-
return;
256-
}
257-
258-
// Validate target service availability
259-
const isAvailable = await this.validateTargetService(target, tunnelConfig.targetPort!);
260-
if (!isAvailable) {
261-
logger.error(`Target service not available: ${target}`, {
262-
subdomain,
263-
port: tunnelConfig.targetPort
264-
});
265-
res.writeHead(502);
266-
res.end(JSON.stringify({
267-
error: 'Target service not available',
268-
details: `Could not connect to service on port ${tunnelConfig.targetPort}. Make sure your service is running.`
269-
}));
270-
return;
271-
}
177+
// In a VM setup, we need to use WebSocket to forward the request to the client
178+
const message = {
179+
type: 'request',
180+
method: req.method,
181+
path: req.url,
182+
headers: req.headers,
183+
body: await this.getRequestBody(req)
184+
};
272185

273-
logger.info(`Forwarding request to target: ${target}`, {
186+
logger.info(`Forwarding request via WebSocket`, {
274187
subdomain,
275188
method: req.method,
276-
url: req.url
189+
url: req.url,
190+
targetPort: tunnelConfig.targetPort
277191
});
278-
279-
await this.handleProxyRequest(req, res, target, tunnelConfig);
280-
} catch (error) {
281-
logger.error('Error in proxyRequest', { error, subdomain });
282-
res.statusCode = 500;
283-
res.end(JSON.stringify({ error: 'Proxy request failed' }));
284-
}
285-
}
286192

287-
private async handleProxyRequest(req: IncomingMessage, res: ServerResponse, target: string, tunnelConfig: TunnelConfig) {
288-
try {
289-
const proxy = httpProxy.createProxyServer({});
290-
291-
// Add error handler for proxy errors
292-
proxy.on('error', (err: Error, req: IncomingMessage, res: ServerResponse) => {
293-
logger.error('Proxy error', {
294-
error: err.message,
295-
target,
296-
headers: req.headers,
297-
method: req.method,
298-
url: req.url,
299-
stack: err.stack
300-
});
193+
// Send the request to the client via WebSocket
194+
tunnelConfig.ws.send(JSON.stringify(message));
301195

302-
// Handle specific error cases
303-
if (err.message.includes('ECONNREFUSED')) {
304-
logger.error('Target service not available', {
305-
target,
306-
error: 'Connection refused',
307-
port: tunnelConfig.targetPort
308-
});
309-
res.writeHead(502);
310-
res.end(JSON.stringify({
311-
error: 'Target service not available',
312-
details: `Connection refused to port ${tunnelConfig.targetPort}. Make sure your service is running and listening on the correct port.`
313-
}));
314-
return;
315-
}
196+
// Wait for the response from the client
197+
const response = await this.waitForResponse(tunnelConfig.ws);
316198

317-
if (err.message.includes('ECONNRESET')) {
318-
logger.error('Connection reset by target', {
319-
target,
320-
error: 'Connection reset'
321-
});
322-
res.writeHead(504);
323-
res.end(JSON.stringify({
324-
error: 'Connection reset',
325-
details: 'The target service unexpectedly closed the connection.'
326-
}));
327-
return;
328-
}
199+
// Forward the response back to the original requester
200+
res.writeHead(response.statusCode, response.headers);
201+
res.end(response.body);
329202

330-
if (err.message.includes('ETIMEDOUT')) {
331-
logger.error('Connection timed out', {
332-
target,
333-
error: 'Timeout'
334-
});
335-
res.writeHead(504);
336-
res.end(JSON.stringify({
337-
error: 'Gateway timeout',
338-
details: 'The target service took too long to respond.'
339-
}));
340-
return;
341-
}
203+
} catch (error) {
204+
logger.error('Error in proxyRequest', { error, subdomain });
205+
res.statusCode = 502;
206+
res.end(JSON.stringify({
207+
error: 'Bad Gateway',
208+
details: 'Error communicating with the client tunnel'
209+
}));
210+
}
211+
}
342212

343-
// Default error response
344-
res.writeHead(500);
345-
res.end(JSON.stringify({
346-
error: 'Proxy error',
347-
details: err.message,
348-
code: err.name
349-
}));
213+
private getRequestBody(req: IncomingMessage): Promise<string> {
214+
return new Promise((resolve) => {
215+
let body = '';
216+
req.on('data', chunk => {
217+
body += chunk.toString();
350218
});
351-
352-
proxy.web(req, res, {
353-
target,
354-
secure: false,
355-
changeOrigin: true,
356-
selfHandleResponse: true,
357-
ws: false // Disable WebSocket upgrade for HTTP requests
219+
req.on('end', () => {
220+
resolve(body);
358221
});
222+
});
223+
}
359224

360-
proxy.on('proxyRes', (proxyRes: IncomingMessage, req: IncomingMessage, res: ServerResponse) => {
361-
const chunks: Buffer[] = [];
362-
363-
proxyRes.on('data', (chunk: Buffer) => {
364-
chunks.push(chunk);
365-
});
366-
367-
proxyRes.on('end', async () => {
368-
try {
369-
const buffer = Buffer.concat(chunks);
370-
const encoding = proxyRes.headers['content-encoding'];
371-
372-
// Copy all headers except content-length (let Node calculate it)
373-
Object.keys(proxyRes.headers).forEach(key => {
374-
if (key.toLowerCase() !== 'content-length') {
375-
res.setHeader(key, proxyRes.headers[key]!);
376-
}
377-
});
378-
379-
// Set status code
380-
res.statusCode = proxyRes.statusCode || 200;
381-
382-
// Log response details
383-
logger.info('Proxying response', {
384-
statusCode: proxyRes.statusCode,
385-
contentEncoding: encoding,
386-
contentLength: buffer.length,
387-
headers: proxyRes.headers,
388-
target
389-
});
390-
391-
// Send the response
392-
res.end(buffer);
393-
394-
} catch (error) {
395-
logger.error('Error processing proxy response', { error, target });
396-
res.statusCode = 500;
397-
res.end(JSON.stringify({ error: 'Error processing response' }));
225+
private waitForResponse(ws: WebSocket): Promise<any> {
226+
return new Promise((resolve, reject) => {
227+
const timeout = setTimeout(() => {
228+
reject(new Error('Response timeout'));
229+
}, 30000); // 30 second timeout
230+
231+
const messageHandler = (data: WebSocket.Data) => {
232+
try {
233+
const response = JSON.parse(data.toString());
234+
if (response.type === 'response') {
235+
clearTimeout(timeout);
236+
ws.removeListener('message', messageHandler);
237+
resolve(response);
398238
}
399-
});
400-
});
239+
} catch (error) {
240+
logger.error('Error parsing response', { error });
241+
}
242+
};
401243

402-
} catch (error) {
403-
logger.error('Error in handleProxyRequest', { error, target });
404-
res.statusCode = 500;
405-
res.end(JSON.stringify({ error: 'Proxy request failed' }));
406-
}
244+
ws.on('message', messageHandler);
245+
});
407246
}
408247
}

0 commit comments

Comments
 (0)