diff --git a/README.md b/README.md index eeb3f9868..7a33bbb7a 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ This will install `http-server` globally so that it may be run from the command Using `npx` you can run the script without installing it first: npx http-server [path] [options] - + #### As a dependency in your `npm` package: npm install http-server @@ -80,6 +80,8 @@ Using `npx` you can run the script without installing it first: `-K` or `--key` Path to ssl key file (default: `key.pem`). +`--cgi` Enable CGI scripts in /cgi-bin/. + `-r` or `--robots` Provide a /robots.txt (whose content defaults to `User-agent: *\nDisallow: /`) `--no-dotfiles` Do not show dotfiles @@ -132,11 +134,32 @@ Available on: Hit CTRL-C to stop the server ``` +## CGI scripts + +If you need some server-side scripting, you can enable CGI scripts with `--cgi`. Place your scripts in `/cgi-bin/` and call them using something like `http://127.0.0.1:8080/cgi-bin/script.js?query=string`. + +On POSIX systems (e.g. Linux and Mac), you can set any script in `/cgi-bin/` to be executable using something like `chmod +x script.sh`. Files ending in `.js` will be executing using Node.js, even if they are not set to be executable. On Windows, scripts must be JavaScript (`.js`), batch (`.bat`), command (`.cmd`) or executable (`.exe`) files. + +An example CGI script written in JavaScript is: + +``` js +#!/usr/bin/env node + +console.log('Content-Type: text/plain') +console.log(''); +console.log(`Hello, ${process.env.REMOTE_ADDR}!`); +console.log(''); + +for (var v in process.env) { + console.log(v, process.env[v]); +} +``` + # Development Checkout this repository locally, then: -```sh +``` sh $ npm i $ node bin/http-server ``` diff --git a/bin/http-server b/bin/http-server index 42ebf651f..8108f7ad0 100755 --- a/bin/http-server +++ b/bin/http-server @@ -49,6 +49,8 @@ if (argv.h || argv.help) { ' -C --cert Path to ssl cert file (default: cert.pem).', ' -K --key Path to ssl key file (default: key.pem).', '', + ' --cgi Enable CGI scripts in /cgi-bin/', + '', ' -r --robots Respond to /robots.txt [User-agent: *\\nDisallow: /]', ' --no-dotfiles Do not show dotfiles', ' -h --help Print this list and exit.', @@ -128,7 +130,8 @@ function listen(port) { proxy: proxy, showDotfiles: argv.dotfiles, username: argv.username || process.env.NODE_HTTP_SERVER_USERNAME, - password: argv.password || process.env.NODE_HTTP_SERVER_PASSWORD + password: argv.password || process.env.NODE_HTTP_SERVER_PASSWORD, + cgi: argv.cgi }; if (argv.cors) { diff --git a/doc/http-server.1 b/doc/http-server.1 index 751d931d4..1833e1138 100644 --- a/doc/http-server.1 +++ b/doc/http-server.1 @@ -111,6 +111,10 @@ If not specified, uses cert.pem. Path to SSL key file. If not specified, uses key.pem. +.TP +.BI \-\-cgi +Enable CGI scripts in /cgi-bin/. + .TP .BI \-r ", " \-\-robots " " [\fIUSER\-AGENT\fR] Respond to /robots.txt request. diff --git a/lib/http-server.js b/lib/http-server.js index 8bafdf8e9..86bc11ab9 100644 --- a/lib/http-server.js +++ b/lib/http-server.js @@ -7,7 +7,11 @@ var fs = require('fs'), httpProxy = require('http-proxy'), corser = require('corser'), path = require('path'), - secureCompare = require('secure-compare'); + secureCompare = require('secure-compare'), + cgi = require('cgi'), + Stream = require('stream'), + executable = require('executable'), + url = require('url'); // a hacky and direct workaround to fix https://github.com/http-party/http-server/issues/525 function getCaller() { @@ -149,6 +153,64 @@ function HttpServer(options) { }); } + if (options.cgi) { + var _that = this; // remember what this is + before.push(function (req, res) { + if (req.url.indexOf('/cgi-bin/') === 0) { + var script = path.join(_that.root, decodeURIComponent(url.parse(req.url).pathname)); + if (fs.existsSync(script) && fs.lstatSync(script).isFile()) { + var stderr = new Stream.Writable({ + write: function (chunk, encoding, next) { + res.statusCode = 500; + res.write(chunk); + next(); + } + }); + + stderr.on('finish', function () { + res.end(); + stderr.end(); + }); + + var args = []; + var cmd = script; + + if (!executable.sync(script) || process.platform === 'win32') { + switch (path.extname(script).toLowerCase()) { + case '.js': + // we can execute using Node + cmd = 'node'; + args = [script]; + break; + case '.bat': + case '.cmd': + case '.exe': + // Only OK on Windows + if (process.platform !== 'win32') { + return res.emit('next'); + } + break; + default: + // don't know how to execute script + return res.emit('next'); + } + } + + cgi(cmd, { stderr: stderr, args: args })(req, res, function () { + res.emit('next'); + }); + } + else { + // not a script so allow default behavior + res.emit('next'); + } + } + else { + res.emit('next'); + } + }); + } + before.push(ecstatic({ root: this.root, cache: this.cache, diff --git a/package-lock.json b/package-lock.json index acb4dfd60..49ad5492d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -370,6 +370,16 @@ "integrity": "sha1-eEp5eRWjjq0nutRWtVcstLuqeME=", "dev": true }, + "bufferjs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bufferjs/-/bufferjs-3.0.1.tgz", + "integrity": "sha1-BpLoKcsQoQVQ5kc5CwNesGw46O8=" + }, + "bufferlist": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bufferlist/-/bufferlist-0.1.0.tgz", + "integrity": "sha1-Qr7y2JVztA+hCGuzng9TEBcNHd0=" + }, "camelcase": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", @@ -392,6 +402,32 @@ "lazy-cache": "^1.0.3" } }, + "cgi": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cgi/-/cgi-0.3.1.tgz", + "integrity": "sha1-h1HaZKHPGEnREFYxi3YNGs+6R9w=", + "requires": { + "debug": "2", + "extend": "~2.0.0", + "header-stack": "~0.0.2", + "stream-stack": "~1.1.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "extend": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-2.0.2.tgz", + "integrity": "sha512-AgFD4VU+lVLP6vjnlNfF7OeInLTyeyckCNPEsuxz1vi786UuK/nk6ynPuhn/h+Ju9++TQyr5EpLRI14fc1QtTQ==" + } + } + }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", @@ -1139,6 +1175,14 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==" }, + "executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "requires": { + "pify": "^2.2.0" + } + }, "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -1354,6 +1398,16 @@ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, + "header-stack": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/header-stack/-/header-stack-0.0.2.tgz", + "integrity": "sha1-Rg1ysW04ZSzkUeIyU2lxsx6E1g8=", + "requires": { + "bufferjs": ">= 0.2.3", + "bufferlist": ">= 0.0.6", + "stream-stack": ">= 1.1.1" + } + }, "home-or-tmp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-1.0.0.tgz", @@ -1918,8 +1972,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "mute-stream": { "version": "0.0.8", @@ -2092,6 +2145,11 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, "pkginfo": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.4.1.tgz", @@ -2513,6 +2571,11 @@ "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", "dev": true }, + "stream-stack": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/stream-stack/-/stream-stack-1.1.4.tgz", + "integrity": "sha1-cIRgQrqwGFAI5Qnt/h93+TYcumk=" + }, "string.prototype.trimleft": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", diff --git a/package.json b/package.json index 19ef25a30..bf1a06406 100644 --- a/package.json +++ b/package.json @@ -81,13 +81,19 @@ { "name": "Jade Michael Thornton", "email": "jade@jmthornton.net" + }, + { + "name": "Oliver Moran", + "email": "oliver.moran@gmail.com" } ], "dependencies": { "basic-auth": "^1.0.3", + "cgi": "^0.3.1", "colors": "^1.4.0", "corser": "^2.0.1", "ecstatic": "^3.3.2", + "executable": "^4.1.1", "http-proxy": "^1.18.0", "minimist": "^1.2.5", "opener": "^1.5.1", diff --git a/public/cgi-bin/script.js b/public/cgi-bin/script.js new file mode 100755 index 000000000..75ecdf127 --- /dev/null +++ b/public/cgi-bin/script.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +console.log('Content-Type: text/plain'); +console.log(''); +console.log(`Hello, ${process.env.REMOTE_ADDR}!`); +console.log(''); + +for (var v in process.env) { + console.log(v, process.env[v]); +} diff --git a/test/fixtures/root/cgi-bin/broken.js b/test/fixtures/root/cgi-bin/broken.js new file mode 100644 index 000000000..a21161eb5 --- /dev/null +++ b/test/fixtures/root/cgi-bin/broken.js @@ -0,0 +1,3 @@ +console.log('Content-Type: text/plain'); +console.log(''); +throw('cgi like the 90s'); diff --git a/test/fixtures/root/cgi-bin/file.js b/test/fixtures/root/cgi-bin/file.js new file mode 100644 index 000000000..979198134 --- /dev/null +++ b/test/fixtures/root/cgi-bin/file.js @@ -0,0 +1,3 @@ +console.log('Content-Type: text/plain'); +console.log(''); +console.log('cgi like the 90s'); diff --git a/test/http-server-test.js b/test/http-server-test.js index 6d5fce462..264dd3070 100644 --- a/test/http-server-test.js +++ b/test/http-server-test.js @@ -514,5 +514,62 @@ vows.describe('http-server').addBatch({ teardown: function (server) { server.close(); } + }, + 'When http-server is listening on 8087 with CGI enabled,\n': { + topic: function () { + var server = httpServer.createServer({ + root: root, + cgi: true + }); + + server.listen(8087); + this.callback(null, server); + }, + 'a file served from the cgi-bin directory': { + topic: function () { + request('http://127.0.0.1:8087/cgi-bin/file.js', this.callback); + }, + 'should be executed as a script': function (error, response, body) { + assert.equal(body.trim(), 'cgi like the 90s'); + }, + 'and a file with that throws an error': { + topic: function () { + request('http://127.0.0.1:8087/cgi-bin/broken.js', this.callback); + }, + 'status code should be 500': function (res) { + assert.equal(res.statusCode, 500); + }, + 'and a non-existant file': { + topic: function () { + request('http://127.0.0.1:8087/cgi-bin/nothing.js', this.callback); + }, + 'status code should be 404': function (res) { + assert.equal(res.statusCode, 404); + }, + 'it should serve files from root directory': { + topic: function () { + request('http://127.0.0.1:8087/file', this.callback); + }, + 'status code should be 200': function (res) { + assert.equal(res.statusCode, 200); + }, + 'and file content': { + topic: function (res, body) { + var self = this; + fs.readFile(path.join(root, 'file'), 'utf8', function (err, data) { + self.callback(err, data, body); + }); + }, + 'should match content of served file': function (err, file, body) { + assert.equal(body.trim(), file.trim()); + } + } + } + } + } + }, + teardown: function (server) { + server.close(); + } } }).export(module);