import Fastify, { FastifyRequest } from 'fastify';
import { db } from './db';
import { routingTable } from './db/schema';
import { eq } from 'drizzle-orm';
import dotenv from 'dotenv';
import Docker from 'dockerode';
import httpProxy from 'http-proxy';
import crypto from 'crypto';
import fastifyFormBody from '@fastify/formbody';
import fastifyMultipart from '@fastify/multipart';
dotenv.config();
const fastify = Fastify({
logger: true,
// Increase body size limits for proxying larger POST requests
bodyLimit: 30 * 1024 * 1024 // 30MB
});
// Register fastify body parser plugins to handle different content types
fastify.register(fastifyFormBody);
fastify.register(fastifyMultipart, {
limits: {
fileSize: 25 * 1024 * 1024 // 25MB limit for file uploads
}
});
// Create a proxy server instance
const proxy = httpProxy.createProxyServer({
changeOrigin: true,
xfwd: true, // Add X-Forwarded headers
proxyTimeout: 120000, // Increase timeout to 2 minutes
buffer: undefined, // Let the proxy handle buffering based on the request
});
// Add special handling for the proxy
proxy.on('proxyReq', (proxyReq, req, res, options) => {
// Ensure content-length is preserved for requests with bodies
if (req.headers['content-length']) {
proxyReq.setHeader('Content-Length', req.headers['content-length']);
}
// Ensure content-type is preserved
if (req.headers['content-type']) {
proxyReq.setHeader('Content-Type', req.headers['content-type']);
}
});
// Add response handling to manage incoming responses
proxy.on('proxyRes', (proxyRes, req, res) => {
fastify.log.info(`Received proxy response from target with status: ${proxyRes.statusCode}`);
});
// Handle proxy errors
// @ts-ignore - Ignore TypeScript errors for now as the http-proxy types are hard to match exactly
proxy.on('error', function(err: any, req: any, res: any) {
if (res.writeHead) {
fastify.log.error(`Proxy error: ${err.message}`);
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Proxy error: ' + err.message);
}
});
const docker = new Docker({ socketPath: '/var/run/docker.sock' });
// Define a route for handling all requests
fastify.all('*', async (request, reply) => {
const host = request.headers.host;
if (!host) {
return reply.status(400).send({ error: "Host header is required" });
}
const hostParts = host.split('.');
let subdomain = hostParts.at(0) ?? null;
if (!subdomain) {
return reply.status(400).send({ error: "Invalid host format" });
}
const useActionRunner = subdomain.includes("action");
if (useActionRunner) {
subdomain = subdomain.replace('-action', '');
}
// If the subdomain is "server", allow access to server endpoints directly
if (subdomain.toLowerCase() === 'server') {
// Skip proxying and let the request continue to other routes
return;
}
// Query PostgreSQL for the port
const result = await db
.select()
.from(routingTable)
.where(eq(routingTable.projectId, subdomain));
if (result.length === 0) {
return reply.status(404).send({ error: "Project not found" });
}
// Use http-proxy instead of reply.from()
const targetUrl = useActionRunner ? `http://localhost:${result[0].action_port}` : `http://localhost:${result[0].host_port}`;
// Set up a custom completion handler
const proxyHandler = (callback: () => void) => {
// Mark the response as handled
reply.hijack();
const options: httpProxy.ServerOptions = {
target: targetUrl,
ignorePath: false,
prependPath: false,
selfHandleResponse: false,
timeout: 120000 // Match the main proxy timeout
};
// Handle different types of requests
if (request.method === 'POST' || request.method === 'PUT' || request.method === 'PATCH') {
// For requests with bodies, ensure the content-type and body are preserved
fastify.log.info(`Proxying ${request.method} request to ${targetUrl}${request.url} with Content-Type: ${request.headers['content-type']}, Content-Length: ${request.headers['content-length']}`);
// Add special handling for POST requests to ensure the body gets forwarded properly
const startTime = Date.now();
// Set a longer timeout for the request to complete
const reqTimeout = setTimeout(() => {
fastify.log.error(`Request to ${targetUrl}${request.url} timed out after ${Date.now() - startTime}ms`);
callback();
}, 115000); // Slightly less than the main proxy timeout
// Listen for the response to complete
request.raw.on('close', () => {
clearTimeout(reqTimeout);
const duration = Date.now() - startTime;
fastify.log.info(`Request to ${targetUrl}${request.url} completed in ${duration}ms`);
callback();
});
} else {
fastify.log.info(`Proxying ${request.method} request to ${targetUrl}${request.url}`);
}
// Ensure the raw request is directly passed to the proxy
// This preserves the original request body for POST requests
proxy.web(request.raw, reply.raw, options, (err) => {
if (err) {
fastify.log.error(`Proxy error in web(): ${err.message}`);
}
callback();
});
};
// Handle completion/errors
return new Promise<void>((resolve, reject) => {
proxyHandler(() => {
resolve();
});
});
});
// Run the server!
async function start() {
try {
await fastify.listen({
port: 8080,
host: '0.0.0.0' // Listen on all network interfaces
});
} catch (err) {
fastify.log.error(err);
}
}
start();