├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── index.js └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | root = true 3 | 4 | # All files 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | charset = utf-8 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ivan Filho - https://www.ivanfilho.com/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-php-fpm 2 | 3 | [![NPM Version][npm-image]][npm-url] 4 | [![Github License][license-image]](LICENSE) 5 | [![NPM Downloads][downloads-image]][npm-url] 6 | 7 | ## Install 8 | 9 | This is a [Node.js](https://nodejs.org/en/) module available through the 10 | [npm registry](https://www.npmjs.com/). Installation is done using the 11 | [`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): 12 | 13 | ```sh 14 | $ npm install php-fpm 15 | ``` 16 | 17 | ## Examples 18 | 19 | ### Using with `http` 20 | 21 | ```js 22 | const http = require('http') 23 | const phpFpm = require('php-fpm') 24 | const serveStatic = require('serve-static') 25 | 26 | const php = phpFpm() // Optional: parameters for fastcgi-client 27 | const serve = serveStatic(__dirname) 28 | 29 | const server = http.createServer(function (req, res) { 30 | if (req.url.match(/\.php(\?.*)?$/)) { 31 | php(req, res) 32 | } else { 33 | serve(req, res) 34 | } 35 | }) 36 | 37 | server.listen(8080) 38 | ``` 39 | 40 | ### Using with `express` 41 | 42 | ```js 43 | const express = require('express') 44 | const phpFpm = require('php-fpm') 45 | 46 | const app = express() 47 | 48 | app.use(phpFpm()) 49 | 50 | app.listen(8080) 51 | ``` 52 | 53 | ## API 54 | 55 | The available parameters for `php-fpm` are: 56 | 57 | ```js 58 | phpFpm( 59 | userOptions = { 60 | // Parameters for fastcgi-client 61 | host: '127.0.0.1', 62 | port: 9000, 63 | documentRoot: __dirname, 64 | skipCheckServer: true 65 | }, 66 | customParams = { 67 | // Headers for php-fpm (automatically set) 68 | uri, // REQUEST_URI 69 | document, // DOCUMENT_URI 70 | query, // QUERY_STRING 71 | script // SCRIPT_FILENAME 72 | } 73 | ) 74 | ``` 75 | 76 | Parameters for `fastcgi-client` are available [here](https://github.com/LastLeaf/node-fastcgi-client#api). 77 | 78 | ## `rewrite` option 79 | 80 | There is an implementation of the `rewrite` module to handle routes the traditional nginx/Apache way. 81 | 82 | ```js 83 | const phpFpm = require('php-fpm') 84 | 85 | // Route rewrite for Phalcon framework 86 | const php = phpFpm({ 87 | rewrite: [ 88 | { 89 | rule: /.*/, // Default rule, can be omitted 90 | replace: '/index.php?_url=$0' 91 | } 92 | ] 93 | }) 94 | ``` 95 | 96 | ## License 97 | 98 | [MIT](LICENSE) 99 | 100 | [npm-image]: https://img.shields.io/npm/v/php-fpm.svg 101 | [license-image]: https://img.shields.io/github/license/ivanslf/node-php-fpm.svg 102 | [downloads-image]: https://img.shields.io/npm/dm/php-fpm.svg 103 | [npm-url]: https://npmjs.org/package/php-fpm 104 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fastCgi = require('fastcgi-client') 3 | const defaultOptions = { 4 | host: '127.0.0.1', 5 | port: 9000, 6 | documentRoot: path.dirname(require.main.filename || '.'), 7 | skipCheckServer: true 8 | } 9 | 10 | module.exports = function (userOptions = {}, customParams = {}) { 11 | const options = Object.assign({}, defaultOptions, userOptions) 12 | const fpm = new Promise((resolve, reject) => { 13 | const loader = fastCgi(options) 14 | loader.on('ready', () => resolve(loader)) 15 | loader.on('error', reject) 16 | }) 17 | 18 | return async function (req, res) { 19 | let params = Object.assign({}, customParams, { 20 | uri: req.url 21 | }) 22 | 23 | if (!params.uri || !params.uri.startsWith('/')) { 24 | throw new Error('invalid uri') 25 | } 26 | 27 | if (options.rewrite) { 28 | const rules = Array.isArray(options.rewrite) 29 | ? options.rewrite : [options.rewrite] 30 | for (const rule of rules) { 31 | const match = params.uri.match(rule.search || /.*/) 32 | if (match) { 33 | let result = rule.replace 34 | for (const index in match) { 35 | const selector = new RegExp(`\\$${index}`, 'g') 36 | result = result.replace(selector, match[index]) 37 | } 38 | params.outerUri = params.uri 39 | params.uri = result 40 | break 41 | } 42 | } 43 | } 44 | 45 | if (params.uri.indexOf('?') !== -1) { 46 | params.document = params.uri.split('?')[0] 47 | params.query = params.uri 48 | .slice(params.document.length + 1) 49 | .replace(/\?/g, '&') 50 | } 51 | 52 | if (!params.script) { 53 | params.script = path.posix.join(options.documentRoot, params.document || params.uri) 54 | } 55 | 56 | const headers = { 57 | REQUEST_METHOD: req.method, 58 | CONTENT_TYPE: req.headers['content-type'], 59 | CONTENT_LENGTH: req.headers['content-length'], 60 | CONTENT_DISPOSITION: req.headers['content-disposition'], 61 | DOCUMENT_ROOT: options.documentRoot, 62 | SCRIPT_FILENAME: params.script, 63 | SCRIPT_NAME: params.script.split('/').pop(), 64 | REQUEST_URI: params.outerUri || params.uri, 65 | DOCUMENT_URI: params.document || params.uri, 66 | QUERY_STRING: params.query, 67 | REQUEST_SCHEME: req.protocol, 68 | HTTPS: req.protocol === 'https' ? 'on' : undefined, 69 | REMOTE_ADDR: req.connection.remoteAddress, 70 | REMOTE_PORT: req.connection.remotePort, 71 | SERVER_NAME: req.connection.domain, 72 | SERVER_PROTOCOL: 'HTTP/1.1', 73 | GATEWAY_INTERFACE: 'CGI/1.1', 74 | SERVER_SOFTWARE: 'php-fpm for Node', 75 | REDIRECT_STATUS: 200 76 | } 77 | 78 | for (const header in headers) { 79 | if (typeof headers[header] === 'undefined') { delete headers[header] } 80 | } 81 | 82 | for (header in req.headers) { 83 | headers['HTTP_' + header.toUpperCase().replace(/-/g, '_')] = req.headers[header]; 84 | } 85 | 86 | if (options.debug) { 87 | console.log(headers) 88 | } 89 | 90 | const php = await fpm 91 | return new Promise(function (resolve, reject) { 92 | php.request(headers, function (err, request) { 93 | if (err) { return reject(err) } 94 | var output = '' 95 | var errors = '' 96 | 97 | req.pipe(request.stdin) 98 | 99 | request.stdout.on('data', function (data) { 100 | output += data.toString('utf8') 101 | }) 102 | 103 | request.stderr.on('data', function (data) { 104 | errors += data.toString('utf8') 105 | }) 106 | 107 | request.stdout.on('end', function () { 108 | if (errors) { return reject(new Error(errors)) } 109 | 110 | const head = output.match(/^[\s\S]*?\r\n\r\n/)[0] 111 | const parseHead = head.split('\r\n').filter(_ => _) 112 | const responseHeaders = {} 113 | let statusCode = 200 114 | let statusMessage = '' 115 | 116 | for (const item of parseHead) { 117 | const pair = item.split(': ') 118 | 119 | if (pair.length > 1 && pair[0] && pair[1]) { 120 | if (pair[0] in responseHeaders) { 121 | responseHeaders[pair[0]].push(pair[1]) 122 | } else { 123 | responseHeaders[pair[0]] = [ pair[1] ] 124 | } 125 | 126 | if (pair[0] === 'Status') { 127 | const match = pair[1].match(/(\d+) (.*)/) 128 | statusCode = parseInt(match[1]) 129 | statusMessage = match[2] 130 | } 131 | } 132 | } 133 | 134 | res.writeHead(statusCode, statusMessage, responseHeaders) 135 | const body = output.slice(head.length) 136 | res.write(body) 137 | res.end() 138 | 139 | resolve({ headers, body }) 140 | }) 141 | }) 142 | }) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-fpm", 3 | "version": "1.1.0", 4 | "description": "Simple, middleware-style PHP binding", 5 | "main": "index.js", 6 | "scripts": { 7 | "pretest": "standard | snazzy", 8 | "test": "tap test/*.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/ivanslf/node-php-fpm.git" 13 | }, 14 | "keywords": [ 15 | "php", 16 | "fpm", 17 | "phpfpm", 18 | "php-fpm", 19 | "apache", 20 | "nginx", 21 | "php7" 22 | ], 23 | "author": "Ivan Filho", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/ivanslf/node-php-fpm/issues" 27 | }, 28 | "homepage": "https://github.com/ivanslf/node-php-fpm#readme", 29 | "dependencies": { 30 | "fastcgi-client": "0.0.1" 31 | }, 32 | "devDependencies": { 33 | "snazzy": "^7.0.0", 34 | "standard": "^10.0.3", 35 | "tap": "^10.7.2" 36 | } 37 | } 38 | --------------------------------------------------------------------------------