first commit
This commit is contained in:
227
src/server.js
Normal file
227
src/server.js
Normal file
@@ -0,0 +1,227 @@
|
||||
import http from "node:http";
|
||||
import { join } from "node:path";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
import vm from "node:vm";
|
||||
import axios from "axios";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { create as xmlBuilder } from "xmlbuilder2";
|
||||
import { logger } from "./logger.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const globalVMContext = {
|
||||
URLSearchParams,
|
||||
URL,
|
||||
console: logger,
|
||||
crypto,
|
||||
axios,
|
||||
uuidv4,
|
||||
jwt,
|
||||
xmlBuilder,
|
||||
};
|
||||
|
||||
let globalVariableContext = {};
|
||||
|
||||
/**
|
||||
* Load all files from globalVariables/ directory into globalVariableContext
|
||||
* Pattern: globalVariables/filename.{json|js} -> globalVariableContext['filename']
|
||||
*/
|
||||
function loadGlobalVariables() {
|
||||
const globalDir = join(__dirname, "globalVariables");
|
||||
const jsonFiles = [];
|
||||
const jsFiles = [];
|
||||
|
||||
// Scan and categorize files in one pass
|
||||
readdirSync(globalDir).forEach((file) => {
|
||||
if (file.includes(".example")) return;
|
||||
if (file.endsWith(".json")) jsonFiles.push(file);
|
||||
else if (file.endsWith(".js")) jsFiles.push(file);
|
||||
});
|
||||
|
||||
// Load JSON files first (data)
|
||||
jsonFiles.forEach((file) => {
|
||||
const varName = file.replace(".json", "");
|
||||
const data = JSON.parse(readFileSync(join(globalDir, file), "utf-8"));
|
||||
globalVariableContext[varName] = data;
|
||||
logger.info({
|
||||
message: `Loaded global data: ${varName}`,
|
||||
keys: Object.keys(data)
|
||||
});
|
||||
});
|
||||
|
||||
// Load JS files second (functions can reference JSON data)
|
||||
jsFiles.forEach((file) => {
|
||||
const varName = file.replace(".js", "");
|
||||
const code = readFileSync(join(globalDir, file), "utf-8");
|
||||
|
||||
// Wrap the literal function body in a function and execute
|
||||
const wrappedCode = `(function() {\n${code}\n})()`;
|
||||
const script = new vm.Script(wrappedCode, { filename: file });
|
||||
const context = vm.createContext({ ...globalVMContext, ...globalVariableContext });
|
||||
|
||||
// Execute script and capture returned object
|
||||
const returnedObject = script.runInContext(context);
|
||||
globalVariableContext[varName] = returnedObject;
|
||||
|
||||
logger.info({
|
||||
message: `Loaded global functions: ${varName}`,
|
||||
type: typeof returnedObject,
|
||||
isObject: typeof returnedObject === 'object' && returnedObject !== null,
|
||||
keys: returnedObject ? Object.keys(returnedObject).length : 0
|
||||
});
|
||||
});
|
||||
|
||||
logger.info({
|
||||
message: `Loaded ${jsonFiles.length + jsFiles.length} global variables`,
|
||||
json: jsonFiles.length,
|
||||
js: jsFiles.length
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration from config/default.json and merge with environment variables
|
||||
*/
|
||||
function loadConfig() {
|
||||
const configPath = join(__dirname, "..", "config", "default.json");
|
||||
const configData = readFileSync(configPath, "utf-8");
|
||||
const config = JSON.parse(configData);
|
||||
|
||||
// Merge environment variables (ENV takes precedence)
|
||||
return {
|
||||
...config,
|
||||
server: {
|
||||
...config.server,
|
||||
port: process.env.PORT ? parseInt(process.env.PORT, 10) : config.server.port,
|
||||
host: process.env.HOST || config.server.host,
|
||||
},
|
||||
proxy: {
|
||||
...config.proxy,
|
||||
},
|
||||
logging: {
|
||||
...config.logging,
|
||||
level: process.env.LOG_LEVEL || config.logging.level,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate configuration
|
||||
*/
|
||||
function validateConfig(config) {
|
||||
if (!config.server.port || config.server.port < 1 || config.server.port > 65535) {
|
||||
throw new Error("Invalid server.port (must be 1-65535)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the HTTP server
|
||||
*/
|
||||
async function startServer() {
|
||||
try {
|
||||
// Load configuration into global.config
|
||||
global.config = loadConfig();
|
||||
|
||||
// Load all global variables (JSON data + JS function modules)
|
||||
loadGlobalVariables();
|
||||
|
||||
logger.info("Starting Proxy Script Server...");
|
||||
logger.info({
|
||||
message: "Configuration loaded",
|
||||
port: global.config.server.port,
|
||||
host: global.config.server.host,
|
||||
logLevel: global.config.logging.level,
|
||||
proxyPrefix: global.config.proxy ?
|
||||
`${global.config.proxy.pathPrefix}${global.config.proxy.workspaceId}/${global.config.proxy.branch}/${global.config.proxy.routeName}` :
|
||||
'(none)'
|
||||
});
|
||||
|
||||
// Validate configuration
|
||||
validateConfig(global.config);
|
||||
logger.info("Configuration validated successfully");
|
||||
|
||||
const proxyPath = join(__dirname, "proxyScripts", "proxy.js");
|
||||
const proxyCode = readFileSync(proxyPath, "utf-8");
|
||||
const script = new vm.Script(proxyCode, { filename: "proxy.js" });
|
||||
|
||||
// Create HTTP server that delegates all requests to proxy
|
||||
const server = http.createServer((req, res) => {
|
||||
try {
|
||||
// Extract proxy routing metadata from config (not parsed from URL)
|
||||
// and attach to req.params if URL matches the configured prefix
|
||||
if (global.config.proxy) {
|
||||
const { pathPrefix, workspaceId, branch, routeName } = global.config.proxy;
|
||||
const fullPrefix = `${pathPrefix.replace(/\/$/, '')}/${workspaceId}/${branch}/${routeName}`;
|
||||
|
||||
// Check if URL starts with proxy prefix
|
||||
if (req.url.startsWith(fullPrefix)) {
|
||||
// Add routing metadata to request for proxy.js
|
||||
// All values come from config, not parsed from URL
|
||||
req.params = {
|
||||
"0": req.url, // Original full path
|
||||
workspaceId, // From config.proxy.workspaceId
|
||||
branch, // From config.proxy.branch
|
||||
route: routeName // From config.proxy.routeName (renamed to 'route' for consistency)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const context = vm.createContext({
|
||||
...globalVMContext,
|
||||
...globalVariableContext,
|
||||
req,
|
||||
res,
|
||||
});
|
||||
script.runInContext(context);
|
||||
} catch (error) {
|
||||
logger.error({
|
||||
message: "Request handling failed",
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
res.statusCode = 500;
|
||||
res.end("Internal Server Error");
|
||||
}
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
const shutdown = () => {
|
||||
logger.info("\nShutting down gracefully...");
|
||||
server.close(() => {
|
||||
logger.info("Server closed");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Force shutdown after 10 seconds
|
||||
setTimeout(() => {
|
||||
logger.error("Forced shutdown after timeout");
|
||||
process.exit(1);
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
process.on("SIGTERM", shutdown);
|
||||
process.on("SIGINT", shutdown);
|
||||
|
||||
// Start listening
|
||||
server.listen(global.config.server.port, global.config.server.host, () => {
|
||||
logger.info({
|
||||
message: "Server listening",
|
||||
port: global.config.server.port,
|
||||
host: global.config.server.host
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error({
|
||||
message: "Failed to start server",
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Start the server
|
||||
startServer();
|
||||
Reference in New Issue
Block a user