Last active
August 27, 2021 23:49
-
-
Save morungos/f468e00dfb20d63f6ea9300fdc92df43 to your computer and use it in GitHub Desktop.
Run a proxy to a php-fpm server for testing php applications
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const express = require('express'); | |
const morgan = require('morgan'); | |
const path = require('path'); | |
const fastCgi = require('fastcgi-client'); | |
const log4js = require("log4js"); | |
const logger = log4js.getLogger(); | |
logger.level = "debug"; | |
const args = require('minimist')(process.argv.slice(2)); | |
/** | |
* Script options: | |
* --fpm-host - the hostname, defaults to 127.0.0.1 | |
* --fpm-port - the port, defaults to 9000 | |
* --port - the port to serve on, defaults to 4000 | |
* --root - the base directory | |
* --debug - enable debug mode | |
* --header=X=Y -- send an additional server value of X=Y | |
*/ | |
const options = { | |
host: args['fpm-host'] || '127.0.0.1', | |
port: args['fpm-port'] || 9000, | |
documentRoot: path.resolve(args['root'] || '.'), | |
skipCheckServer: true, | |
debug: !! args['debug'] | |
}; | |
const additionalHeaders = {}; | |
const headers = (typeof args['header'] === 'string' ? [args['header']] : args['header']) || []; | |
for(let header of headers) { | |
const [key, value] = header.split("=", 2); | |
additionalHeaders[key.trim()] = value.trim(); | |
} | |
const fpm = new Promise((resolve, reject) => { | |
const loader = fastCgi(options); | |
loader.on('ready', () => resolve(loader)); | |
loader.on('error', reject); | |
}); | |
const phpFpm = async function (req, res) { | |
let params = { uri: req.url }; | |
if (!params.uri || !params.uri.startsWith('/')) { | |
throw new Error('invalid uri'); | |
} | |
if (options.rewrite) { | |
const rules = Array.isArray(options.rewrite) | |
? options.rewrite : [options.rewrite]; | |
for (const rule of rules) { | |
const match = params.uri.match(rule.search || /.*/); | |
if (match) { | |
let result = rule.replace; | |
for (const index in match) { | |
const selector = new RegExp(`\\$${index}`, 'g'); | |
result = result.replace(selector, match[index]); | |
} | |
params.outerUri = params.uri; | |
params.uri = result; | |
break; | |
} | |
} | |
} | |
if (params.uri.indexOf('?') !== -1) { | |
params.document = params.uri.split('?')[0]; | |
params.query = params.uri | |
.slice(params.document.length + 1) | |
.replace(/\?/g, '&'); | |
} | |
if (!params.script) { | |
params.script = path.posix.join(options.documentRoot, params.document || params.uri); | |
} | |
const headers = Object.assign({}, { | |
REQUEST_METHOD: req.method, | |
CONTENT_TYPE: req.headers['content-type'], | |
CONTENT_LENGTH: req.headers['content-length'], | |
CONTENT_DISPOSITION: req.headers['content-disposition'], | |
DOCUMENT_ROOT: options.documentRoot, | |
SCRIPT_FILENAME: params.script, | |
SCRIPT_NAME: params.script.split('/').pop(), | |
REQUEST_URI: params.outerUri || params.uri, | |
DOCUMENT_URI: params.document || params.uri, | |
QUERY_STRING: params.query, | |
REQUEST_SCHEME: req.protocol, | |
HTTPS: req.protocol === 'https' ? 'on' : void 0, | |
REMOTE_ADDR: req.connection.remoteAddress, | |
REMOTE_PORT: req.connection.remotePort, | |
SERVER_NAME: req.connection.domain, | |
SERVER_PROTOCOL: 'HTTP/1.1', | |
GATEWAY_INTERFACE: 'CGI/1.1', | |
SERVER_SOFTWARE: 'php-fpm for Node', | |
REDIRECT_STATUS: 200 | |
}, additionalHeaders); | |
for (const header in headers) { | |
if (typeof headers[header] === 'undefined') { delete headers[header]; } | |
} | |
for (let header in req.headers) { | |
headers['HTTP_' + header.toUpperCase().replace(/-/g, '_')] = req.headers[header]; | |
} | |
if (options.debug) { | |
console.log(headers); | |
} | |
const php = await fpm; | |
php.request(headers, function (err, request) { | |
if (err) { throw err; } | |
let buffer = null; | |
req.pipe(request.stdin); | |
request.stdout.on('data', function (data) { | |
if (buffer === false) { | |
res.write(data); | |
return; | |
} else if (buffer === null) { | |
buffer = data; | |
} else { | |
buffer = Buffer.concat([buffer, data]); // eslint-disable-line | |
} | |
const delimiter = buffer.indexOf("\r\n\r\n"); | |
if (delimiter === -1) { | |
return; | |
} | |
// OK, now we can decode the headers | |
const head = buffer.toString('latin1', 0, delimiter); | |
buffer = buffer.slice(delimiter + 4); | |
const parseHead = head.split('\r\n').filter((_) => _); | |
const responseHeaders = {}; | |
let statusCode = 200; | |
let statusMessage = ''; | |
for (const item of parseHead) { | |
const pair = item.split(': '); | |
if (pair.length > 1 && pair[0] && pair[1]) { | |
if (pair[0] in responseHeaders) { | |
responseHeaders[pair[0]].push(pair[1]); | |
} else { | |
responseHeaders[pair[0]] = [ pair[1] ]; | |
} | |
if (pair[0] === 'Status') { | |
const match = pair[1].match(/(\d+) (.*)/); | |
statusCode = parseInt(match[1]); | |
statusMessage = match[2]; | |
} | |
} | |
} | |
res.writeHead(statusCode, statusMessage, responseHeaders); | |
res.write(buffer); | |
buffer = false; | |
}); | |
request.stderr.on('data', function (data) { | |
const strings = data.toString('binary').split("PHP message: "); | |
for(let string of strings) { | |
string = string.trim(); | |
if (string.length == 0) continue; | |
if (string.startsWith("DEBUG:")) { | |
logger.debug("FPM:", string); | |
} else if (string.startsWith("INFO") || string.startsWith("NOTICE")) { | |
logger.info("FPM:", string); | |
} else { | |
logger.error("FPM:", string); | |
} | |
} | |
}); | |
request.stdout.on('end', function () { | |
if (buffer === null) { | |
return res.status(500).send("Incomplete response"); | |
} | |
res.end(); | |
return true; | |
}); | |
}); | |
}; | |
const app = express(); | |
const staticMiddleware = express.static(options.documentRoot, {}); | |
app.use(morgan('dev')); | |
app.use(async function(req, res, next) { | |
if (req.url == '/') { | |
return res.redirect('/index.php'); | |
} | |
const parsed = new URL(req.url, "http://example.com"); | |
if (parsed.pathname.endsWith(".php")) { | |
return next(); | |
} | |
return staticMiddleware(req, res, next); | |
}); | |
app.use(phpFpm); | |
const port = args['port'] || 4000; | |
logger.info(`Starting listen on port: ${port}`); | |
logger.info(`Serving from: ${options.documentRoot}`); | |
app.listen(port); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment