import {NPAS_Options} from "../index"; import {PHPServer} from "./phpserver"; import {NextFunction, Request, Response} from "express"; import {Agent, IncomingMessage} from "http"; import {Utils} from "./utils"; export class Workers { npas: { [key: number]: PHPServer } = {}; utils: Utils = new Utils(); constructor(options: NPAS_Options) { const cluster = require('cluster'); const chalk = require('chalk'); /** * Express Server */ let express = require('express'); let request = require('request'); let router = express.Router(); let app = express(); let workersPID: number[] = []; process.env.NODE_NPAS_WORKER_PID = process.pid.toString(); process.env.NODE_NPAS_WORKER_PIDS = JSON.stringify(workersPID); let createPort = +process.pid; let onlinePHPInstances: number = 0; let phpPorts: number[] = []; let lastUsedPort: any = process.pid; for (let i = 0; i < options.phpPerCluster; i++) { /** * Start PHP Server */ process.env.NODE_NPAS_ROUTER = options.router; this.npas[i] = Workers.createPHPInstance(createPort, options, cluster, (i + 1)); const phpInstance = this.npas[i].php; phpInstance.on('close', () => { console.log(chalk.red.bold(`PHP #${phpInstance.instanceNr} stopped ... eliminating Worker ${phpInstance.workerId} ${phpInstance.instanceNr > 1 ? `and his friends ` : ``}...`)); try { process.kill(phpInstance.workerPid); } catch (e) { } }); /** * Update port with PID from the previous PHP Instance * @type {number} */ phpPorts.push(+createPort); createPort = this.npas[i].php.pid; /** * Signal Master that this worker is ready */ this.npas[i].on('connect', (php) => { ++onlinePHPInstances; let connectInfo = { message: 'php.server.online', worker: { text: `Worker ${cluster.worker.id} hired - PID ${process.pid}`, docRoot: options.root, router: options.router, workerId: php.workerId, workerPID: php.pid, phpPorts: phpPorts, workerPort: createPort } }; /** * All PHP Instances are up. * Time to start worker server and tell MasterCluster that we're starting workers. */ if (options.phpPerCluster == onlinePHPInstances) { router.use((req: Request, res: Response, next: NextFunction) => { let [fallbackHost, fallbackPort] = req.headers['host'].toString().split(":"); let { npas_express_hostname: incomingHost, npas_express_hostname_port: incomingPort, npas_express_target_port: phpPort, user_agent: userAgent } = req.headers; /** * In case of "undefined" port, usually encountered when bypassing MASTER cluster * and targeting a worker PORT (e.g. http://127.0.0.1:NODE_NPAS_MASTER_PID/) * but most importantly if you have Apache/Nginx on port 80 * the invalid url (http://127.0.0.1:undefined/) will fallback * to http://127.0.0.1 and these web-servers will try to resolve the path. * * Solution: Assign a port from the Worker's phpPorts list */ if (typeof phpPort === 'undefined') { phpPort = Utils.getNextAfter(phpPorts, lastUsedPort).toString(); req.headers['npas_express_target_port'] = phpPort; } lastUsedPort = phpPort; let requestOptions: any = { url: `${req.protocol}://127.0.0.1:${phpPort}${req.url}`, headers: { /** * NPAS_SET_HEADER_* - will set a header when PHP is loaded */ 'NPAS_SET_HEADER_WORKER_ID': cluster.worker.id, 'NPAS_SET_HEADER_X_POWERED_BY': 'Node PHP Awesome Server', 'HOST': `${incomingHost || fallbackHost}:${incomingPort || fallbackPort}`, 'NPAS_EXPRESS_HOSTNAME': incomingHost || fallbackHost, 'NPAS_EXPRESS_HOSTNAME_PORT': incomingPort || fallbackPort, 'NPAS_EXPRESS_TARGET_PORT': phpPort, 'NPAS_WORKER_ID': cluster.worker.id, 'USER_AGENT': userAgent, }, followRedirect: false, jar:true, agent: new Agent({keepAlive: true, maxSockets: 1}), encoding: null, gzip: true }; /** * IncommingMessage * @type {NodeJS.ReadableStream} */ let phpPipe = req.pipe(request(requestOptions)); /** * ServerResponse * @type {NodeJS.WritableStream} */ phpPipe .on('response', (response: IncomingMessage) => { if (response.statusCode === 0) { response.statusCode = res.statusCode; } /** * Make sure worker_id is present * It gets lost during php redirects */ if (typeof response.headers['worker-id'] === 'undefined') { response.headers['worker-id'] = cluster.worker.id; } res.header('worker-id', response.headers['worker-id'].toString()); phpPipe.pipe(res); phpPipe.on('end', () => { next(); }); }) .on('error', (err: any) => { res.set("Content-Type", "application/json"); let customMessages = []; customMessages.push("Error from workers."); customMessages.push("Usually this kind of errors occurs from a corrupted output."); customMessages.push("Check the following headers to be correct: Content-Type, Content-Length."); let message = { npas: customMessages, requestOptions: requestOptions, error: err.message, headers: res.getHeaders() }; res.send(JSON.stringify(message)); }); }); app.set('trust proxy'); app.use(router); app.listen(process.env.NODE_NPAS_MASTER_PID, '127.0.0.1', () => { process.send(JSON.stringify(connectInfo)); }).on('error', (err: any) => { process.send(JSON.stringify({ message: 'kill.master', err: err.message })); }); } }) } } private static createPHPInstance(createPort: number, options: NPAS_Options, cluster: any, instanceNr: number): PHPServer { return new PHPServer({ ...options, ...{ host: '127.0.0.1', port: createPort } }, cluster.worker.id, cluster.worker.pid, instanceNr); } } new Workers(JSON.parse(process.env.NODE_NPAS_OPTIONS));