diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..5d8c7c2 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,27 @@ +{ + "env": { + "node": true + }, + "extends": "eslint:recommended", + "ignore": [ + "node_modules" + ], + "rules": { + "indent": [ + "error", + 2 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/COPYING b/LICENSE similarity index 100% rename from COPYING rename to LICENSE diff --git a/README.md b/README.md index 14cc35c..516ac6d 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,33 @@ -
![Node PHP](https://raw2.github.com/mkschreder/siteboot_php/master/node_php_small.jpg)
NodePHP - run wordpress (and other php scripts) with node ---------------------------------- +--------------------------------------------------------- With Node PHP you can leverage the speed of node js and run all of the widely available php scripts directly inside your express site. Installation ------------ - npm install node-php - +``` +npm install node-php +``` + Usage ----- To run wordpress with node js and express do this: - var express = require('express'); - var php = require("node-php"); - var path = require("path"); - - var app = express(); - - app.use("/", php.cgi("/path/to/wordpress")); +```javascript +var express = require('express'); +var php = require("node-php"); +var path = require("path"); + +var app = express(); + +app.use("/", php.cgi("/path/to/wordpress")); - app.listen(9090); +app.listen(9090); - console.log("Server listening!"); +console.log("Server listening!"); +``` Explanation ----------- @@ -41,24 +44,6 @@ Dependencies License ------- -This software is distributed under MIT license. - -Copyright (c) 2014-2016 Martin K. Schröder - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +Copyright © 2014-2016 Martin K. Schröder -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +The MIT License (MIT) - See [LICENSE](./LICENSE) for further details. \ No newline at end of file diff --git a/main.js b/main.js index 63c69f5..bec0bf0 100644 --- a/main.js +++ b/main.js @@ -1,138 +1,231 @@ // License: MIT // Copyright (c) 2014-2016 Martin K. Schröder -var url = require("url"); -var child = require("child_process"); -var path = require("path"); -var fs = require("fs"); - -function runPHP(req, response, next, phpdir){ - var parts = url.parse(req.url); - var query = parts.query; - - var file = path.join(phpdir, parts.pathname); - - // get file stats asynchronously - fs.stat(file, function(err,stat){ - - if(err){ // file does not exist - file = path.join(phpdir, "index.php"); - } else if(stat.isDirectory()){ - file = path.join(file, "index.php"); - } - - var pathinfo = ""; - var i = req.url.indexOf(".php"); - if(i > 0) pathinfo = parts.pathname.substring(i+4); - else pathinfo = parts.pathname; - - var env = { - SERVER_SIGNATURE: "NodeJS server at localhost", - PATH_INFO: pathinfo, //The extra path information, as given in the requested URL. In fact, scripts can be accessed by their virtual path, followed by extra information at the end of this path. The extra information is sent in PATH_INFO. - PATH_TRANSLATED: "", //The virtual-to-real mapped version of PATH_INFO. - SCRIPT_NAME: parts.pathname, //The virtual path of the script being executed. - SCRIPT_FILENAME: file, - REQUEST_FILENAME: file, //The real path of the script being executed. - SCRIPT_URI: req.url, //The full URL to the current object requested by the client. - URL: req.url, //The full URI of the current request. It is made of the concatenation of SCRIPT_NAME and PATH_INFO (if available.) - SCRIPT_URL: req.url, - REQUEST_URI: req.url, //The original request URI sent by the client. - REQUEST_METHOD: req.method, //The method used by the current request; usually set to GET or POST. - QUERY_STRING: parts.query||"", //The information which follows the ? character in the requested URL. - CONTENT_TYPE: req.get("Content-type")||"", //"multipart/form-data", //"application/x-www-form-urlencoded", //The MIME type of the request body; set only for POST or PUT requests. - CONTENT_LENGTH: req.get("Content-Length") || 0, //The length in bytes of the request body; set only for POST or PUT requests. - AUTH_TYPE: "", //The authentication type if the client has authenticated itself to access the script. - AUTH_USER: "", - REMOTE_USER: "", //The name of the user as issued by the client when authenticating itself to access the script. - ALL_HTTP: Object.keys(req.headers).map(function(x){return "HTTP_"+x.toUpperCase().replace("-", "_")+": "+req.headers[x];}).reduce(function(a, b){return a+b+"\n";}, ""), //All HTTP headers sent by the client. Headers are separated by carriage return characters (ASCII 13 - \n) and each header name is prefixed by HTTP_, transformed to upper cases, and - characters it contains are replaced by _ characters. - ALL_RAW: Object.keys(req.headers).map(function(x){return x+": "+req.headers[x];}).reduce(function(a, b){return a+b+"\n";}, ""), //All HTTP headers as sent by the client in raw form. No transformation on the header names is applied. - SERVER_SOFTWARE: "NodeJS", //The web server's software identity. - SERVER_NAME: "localhost", //The host name or the IP address of the computer running the web server as given in the requested URL. - SERVER_ADDR: "127.0.0.1", //The IP address of the computer running the web server. - SERVER_PORT: 8011, //The port to which the request was sent. - GATEWAY_INTERFACE: "CGI/1.1", //The CGI Specification version supported by the web server; always set to CGI/1.1. - SERVER_PROTOCOL: "", //The HTTP protocol version used by the current request. - REMOTE_ADDR: "", //The IP address of the computer that sent the request. - REMOTE_PORT: "", //The port from which the request was sent. - DOCUMENT_ROOT: "", //The absolute path of the web site files. It has the same value as Documents Path. - INSTANCE_ID: "", //The numerical identifier of the host which served the request. On Abyss Web Server X1, it is always set to 1 since there is only a single host. - APPL_MD_PATH: "", //The virtual path of the deepest alias which contains the request URI. If no alias contains the request URI, the variable is set to /. - APPL_PHYSICAL_PATH: "", //The real path of the deepest alias which contains the request URI. If no alias contains the request URI, the variable is set to the same value as DOCUMENT_ROOT. - IS_SUBREQ: "", //It is set to true if the current request is a subrequest, i.e. a request not directly invoked by a client. Otherwise, it is set to true. Subrequests are generated by the server for internal processing. XSSI includes for example result in subrequests. - REDIRECT_STATUS: 1 - }; - - Object.keys(req.headers).map(function(x){return env["HTTP_"+x.toUpperCase().replace("-", "_")] = req.headers[x];}); - - if(/.*?\.php$/.test(file)){ - var res = "", err = ""; - - var php = child.spawn("php-cgi", [], { - env: env - }); - - //php.stdin.resume(); - //console.log(req.rawBody); - //(new Stream(req.rawBody)).pipe(php.stdin); - php.stdin.on("error", function(){}); - req.pipe(php.stdin); // pipe request stream directly into the php process - req.resume(); - //php.stdin.write("\n"); - - //php.stdin.end(); - - php.stdout.on("data", function(data){ - //console.log(data.toString()); - res += data.toString(); - }); - php.stderr.on("data", function(data){ - err += err.toString(); - }); - php.on("error", function(err){ - console.error(err); - }); - php.on("exit", function(){ - // extract headers - php.stdin.end(); - - var lines = res.split("\r\n"); - var line = 0; - var html = ""; - if(lines.length){ - do { - var m = lines[line].split(": "); - if(m[0] === "") break; - - //console.log("HEADER: "+m[0]+": "+m[1]); - if(m[0] == "Status"){ - response.statusCode = parseInt(m[1]); - } - if(m.length == 2){ - response.setHeader(m[0], m[1]); - } - line++; - } while(lines[line] !== ""); - - html = lines.splice(line+1).join("\n"); - } else { - html = res; - } - //console.log("STATUS: "+response.statusCode); - //console.log(html); - response.status(response.statusCode).send(html); - response.end(); - }); - - } else { - response.sendFile(file); - //response.end(); - //next(); - } - }); +/* eslint no-console: 0 */ + +var URL = require('url'); +var child = require('child_process'); +var path = require('path'); +var fs = require('fs'); +var shell = require('shelljs'); + +var PHP_CGI = shell.which('php-cgi'); + +if (!PHP_CGI) { + throw new Error('"php-cgi" cannot be found'); } -exports.cgi = function(phproot){ - return function(req, res, next){ - req.pause(); // stop stream until child-process is opened - runPHP(req, res, next, phproot); - } + +function findFile(url, phpdir, callback) { + var file = path.join(phpdir, url.pathname); + + fs.stat(file, function (err, stat) { + + // file does not exist + if (err || stat.isDirectory()) { + file = path.join(err ? phpdir : file, 'index.php'); + fs.exists(file, function (exists) { + callback(exists && file); + }); + } + // file found + else { + callback(file); + } + + }); +} + +function runPHP(req, response, next, url, file) { + var pathinfo = ''; + var i = req.url.indexOf('.php'); + if (i > 0) pathinfo = url.pathname.substring(i + 4); + else pathinfo = url.pathname; + + var env = { + SERVER_SIGNATURE: 'NodeJS server at localhost', + + // The extra path information, as given in the requested URL. In fact, scripts can be accessed by their virtual path, followed by extra information at the end of this path. The extra information is sent in PATH_INFO. + PATH_INFO: pathinfo, + + // The virtual-to-real mapped version of PATH_INFO. + PATH_TRANSLATED: '', + + // The virtual path of the script being executed. + SCRIPT_NAME: url.pathname, + + SCRIPT_FILENAME: file, + + // The real path of the script being executed. + REQUEST_FILENAME: file, + + // The full URL to the current object requested by the client. + SCRIPT_URI: req.url, + + // The full URI of the current request. It is made of the concatenation of SCRIPT_NAME and PATH_INFO (if available.) + URL: req.url, + + SCRIPT_URL: req.url, + + // The original request URI sent by the client. + REQUEST_URI: req.url, + + // The method used by the current request; usually set to GET or POST. + REQUEST_METHOD: req.method, + + // The information which follows the ? character in the requested URL. + QUERY_STRING: url.query || '', + + // 'multipart/form-data', //'application/x-www-form-urlencoded', //The MIME type of the request body; set only for POST or PUT requests. + CONTENT_TYPE: req.get('Content-Type') || '', + + // The length in bytes of the request body; set only for POST or PUT requests. + CONTENT_LENGTH: req.get('Content-Length') || 0, + + // The authentication type if the client has authenticated itself to access the script. + AUTH_TYPE: '', + + AUTH_USER: '', + + // The name of the user as issued by the client when authenticating itself to access the script. + REMOTE_USER: '', + + // All HTTP headers sent by the client. Headers are separated by carriage return characters (ASCII 13 - \n) and each header name is prefixed by HTTP_, transformed to upper cases, and - characters it contains are replaced by _ characters. + ALL_HTTP: Object.keys(req.headers).map(function (x) { + return 'HTTP_' + x.toUpperCase().replace('-', '_') + ': ' + req.headers[x]; + }).reduce(function (a, b) { + return a + b + '\n'; + }, ''), + + // All HTTP headers as sent by the client in raw form. No transformation on the header names is applied. + ALL_RAW: Object.keys(req.headers).map(function (x) { + return x + ': ' + req.headers[x]; + }).reduce(function (a, b) { + return a + b + '\n'; + }, ''), + + // The web server's software identity. + SERVER_SOFTWARE: 'NodeJS', + + // The host name or the IP address of the computer running the web server as given in the requested URL. + SERVER_NAME: 'localhost', + + // The IP address of the computer running the web server. + SERVER_ADDR: '127.0.0.1', + + // The port to which the request was sent. + SERVER_PORT: 8011, + + // The CGI Specification version supported by the web server; always set to CGI/1.1. + GATEWAY_INTERFACE: 'CGI/1.1', + + // The HTTP protocol version used by the current request. + SERVER_PROTOCOL: '', + + // The IP address of the computer that sent the request. + REMOTE_ADDR: req.ip || '', + + // The port from which the request was sent. + REMOTE_PORT: '', + + // The absolute path of the web site files. It has the same value as Documents Path. + DOCUMENT_ROOT: '', + + // The numerical identifier of the host which served the request. On Abyss Web Server X1, it is always set to 1 since there is only a single host. + INSTANCE_ID: '', + + // The virtual path of the deepest alias which contains the request URI. If no alias contains the request URI, the variable is set to /. + APPL_MD_PATH: '', + + // The real path of the deepest alias which contains the request URI. If no alias contains the request URI, the variable is set to the same value as DOCUMENT_ROOT. + APPL_PHYSICAL_PATH: '', + + // It is set to true if the current request is a subrequest, i.e. a request not directly invoked by a client. Otherwise, it is set to true. Subrequests are generated by the server for internal processing. XSSI includes for example result in subrequests. + IS_SUBREQ: '', + + REDIRECT_STATUS: 1 + }; + + Object.keys(req.headers).map(function (x) { return env['HTTP_' + x.toUpperCase().replace('-', '_')] = req.headers[x]; }); + + if (/.*?\.php$/.test(file)) { + var res = '', err = ''; + + var php = child.spawn(PHP_CGI, [], { + env: env + }); + + // php.stdin.resume(); + // console.log(req.rawBody); + // (new Stream(req.rawBody)).pipe(php.stdin); + php.stdin.on('error', function () { }); + req.pipe(php.stdin); // pipe request stream directly into the php process + req.resume(); + // php.stdin.write('\n'); + + // php.stdin.end(); + + php.stdout.on('data', function (data) { + // console.log(data.toString()); + res += data.toString(); + }); + php.stderr.on('data', function (data) { + err += data.toString(); + }); + php.on('error', function (err) { + console.error(err); + }); + php.on('exit', function () { + // extract headers + php.stdin.end(); + + var lines = res.split('\r\n'); + var line = 0; + var html = ''; + if (lines.length) { + do { + var m = lines[line].split(': '); + if (m[0] === '') break; + + // console.log('HEADER: '+m[0]+': '+m[1]); + if (m[0] == 'Status') { + response.statusCode = parseInt(m[1]); + } + if (m.length == 2) { + response.setHeader(m[0], m[1]); + } + line++; + } while (lines[line] !== ''); + + html = lines.splice(line + 1).join('\n'); + } else { + html = res; + } + // console.log('STATUS: '+response.statusCode); + // console.log(html); + response.status(response.statusCode).send(html); + response.end(); + }); + + } else { + response.sendFile(file); + //response.end(); + //next(); + } } + +exports.cgi = function (phproot) { + return function (req, res, next) { + req.pause(); // stop stream until child-process is opened + + var url = URL.parse(req.url); + findFile(url, phproot, function (file) { + + if (file) { + runPHP(req, res, next, url, file); + } else { + next(); + } + + }); + }; +}; diff --git a/node_php_small.jpg b/node_php_small.jpg deleted file mode 100644 index 2b2bb1d..0000000 Binary files a/node_php_small.jpg and /dev/null differ diff --git a/package.json b/package.json index ca5cd60..ba47a65 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,23 @@ { - "author": { - "name": "Martin K. Schröder" - }, "name": "node-php", "description": "Node JS modules for running wordpress in node js", "version": "0.0.1", + "author": { + "name": "Martin K. Schröder", + "email": "mkschreder.uk@gmail.com" + }, "repository": { - "url": "https://github.com/mkschreder/siteboot_php.git" + "type" : "git", + "url": "https://github.com/mkschreder/node-php.git" }, "main": "./main.js", - "bin": {}, "dependencies": { + "shelljs": "^0.6.0" }, "devDependencies": { - "cluster": "*", - "express": "*", - "request": "*" - }, - "optionalDependencies": {}, - "engines": { - "node": "*" - }, - "scripts": { - "test": "nodeunit tests" + "eslint": "*", + "express": "*", + "mocha": "*", + "supertest": "*" } -} +} \ No newline at end of file diff --git a/test/php/index.php b/test/php/index.php new file mode 100644 index 0000000..5b9f382 --- /dev/null +++ b/test/php/index.php @@ -0,0 +1,4 @@ + $_SERVER)); +?> \ No newline at end of file diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..051303f --- /dev/null +++ b/test/test.js @@ -0,0 +1,46 @@ +/* eslint-env mocha */ + +var request = require('supertest'); +var assert = require('chai').assert; +var express = require('express'); +var php = require('../main'); + +var app = express(); + +app.use('/', php.cgi(__dirname + '/php')); + +describe('GET /', function() { + it('should respond with /index.php', function(done) { + request(app) + .get('/') + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200) + .end(function(err, res) { + if (err) { + done(err); + } else { + assert.match(res.body.$_SERVER.SCRIPT_FILENAME, /index.php$/); + } + done(); + }); + }); +}); + +describe('Get /index.php', function() { + it('should return a valid $_SERVER variable', function(done) { + request(app) + .get('/index.php') + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200) + .end(function(err, res) { + if (err) { + done(err); + } else { + assert.match(res.body.$_SERVER.REMOTE_ADDR, /127.0.0.1|::1/); + } + done(); + }); + }); +}); diff --git a/tests/express.js b/tests/express.js deleted file mode 100644 index ed450b9..0000000 --- a/tests/express.js +++ /dev/null @@ -1,27 +0,0 @@ -var express = require('express'); -var php = require("php"); -var path = require("path"); -var cluster = require("cluster"); -var request = require("request"); - -if(cluster.isMaster){ - cluster.fork().on("message", function(){ - process.exit(); - }); - - var app = express(); - - app.use("/", php.cgi(__dirname)); - - app.listen(9090); -} else { - request("http://localhost:9090/test.php", function(err, res){ - if(res.body == "Hello World!"){ - console.log("Success!"); - } else { - console.error("Expected 'Hello World!', got: "+res.body); - } - process.send("exit"); - }); -} - diff --git a/tests/test.php b/tests/test.php deleted file mode 100644 index b113c61..0000000 --- a/tests/test.php +++ /dev/null @@ -1 +0,0 @@ -