From 12981d74be6930e2ca96cfab169684bfacfa7471 Mon Sep 17 00:00:00 2001 From: Oliver Moran Date: Wed, 21 Apr 2021 00:28:35 +0100 Subject: [PATCH 01/11] enable --cgi argument --- bin/http-server | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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) { From a488876d89c62749c3930e2ccebc5ddf79356c11 Mon Sep 17 00:00:00 2001 From: Oliver Moran Date: Wed, 21 Apr 2021 00:29:09 +0100 Subject: [PATCH 02/11] add handler for CGI --- lib/http-server.js | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/lib/http-server.js b/lib/http-server.js index 8bafdf8e9..2e7863cc0 100644 --- a/lib/http-server.js +++ b/lib/http-server.js @@ -7,7 +7,10 @@ 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'); // a hacky and direct workaround to fix https://github.com/http-party/http-server/issues/525 function getCaller() { @@ -149,6 +152,36 @@ 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, req.url); + if (fs.existsSync(script) && fs.lstatSync(script).isFile() && executable.sync(script)){ + + var stderr = new Stream.Writable({ + write: function(chunk, encoding, next) { + res.statusCode = 500; + res.setHeader('Content-Type', 'text/plain'); + res.end(chunk); + stderr.end(); + } + }); + + cgi(script, {stderr: stderr})(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, From 904e1df244d5538864ae584e13edf4b3f25d37c8 Mon Sep 17 00:00:00 2001 From: Oliver Moran Date: Wed, 21 Apr 2021 00:30:04 +0100 Subject: [PATCH 03/11] add cgi and executable packages --- package-lock.json | 67 +++++++++++++++++++++++++++++++++++++++++++++-- package.json | 6 +++++ 2 files changed, 71 insertions(+), 2 deletions(-) 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", From 51d4541ab3904b3acf9045314171f486da94d865 Mon Sep 17 00:00:00 2001 From: Oliver Moran Date: Wed, 21 Apr 2021 00:30:33 +0100 Subject: [PATCH 04/11] add example CGI script --- public/cgi-bin/script.js | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100755 public/cgi-bin/script.js diff --git a/public/cgi-bin/script.js b/public/cgi-bin/script.js new file mode 100755 index 000000000..eafe8ba5c --- /dev/null +++ b/public/cgi-bin/script.js @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +// On Linux and Mac set this file to executable using: +// chmod +x script.js + +console.log('Content-Type: text/plain') +console.log(''); +console.log(`Hello, ${process.env.REMOTE_ADDR}!`); From 8c6c50701e6b496b65bd6c21f48b63ba67c3c743 Mon Sep 17 00:00:00 2001 From: Oliver Moran Date: Wed, 21 Apr 2021 19:29:33 +0100 Subject: [PATCH 05/11] add --cgi to documentation --- doc/http-server.1 | 4 ++++ 1 file changed, 4 insertions(+) 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. From 9ea70ac7b4566e798eed115c77038d30f2a2a09e Mon Sep 17 00:00:00 2001 From: Oliver Moran Date: Wed, 21 Apr 2021 19:30:43 +0100 Subject: [PATCH 06/11] improve robustness for error and file handling --- lib/http-server.js | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/lib/http-server.js b/lib/http-server.js index 2e7863cc0..1b78f7c9b 100644 --- a/lib/http-server.js +++ b/lib/http-server.js @@ -10,7 +10,8 @@ var fs = require('fs'), secureCompare = require('secure-compare'), cgi = require('cgi'), Stream = require('stream'), - executable = require('executable'); + executable = require('executable'), + url = require('url'); // a hacky and direct workaround to fix https://github.com/http-party/http-server/issues/525 function getCaller() { @@ -156,19 +157,44 @@ function HttpServer(options) { 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, req.url); - if (fs.existsSync(script) && fs.lstatSync(script).isFile() && executable.sync(script)){ + var script = path.join(_that.root, 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.setHeader('Content-Type', 'text/plain'); - res.end(chunk); - stderr.end(); + res.write(chunk); + next(); } }); - cgi(script, {stderr: stderr})(req, res, function(){ + 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": + // OK on Windows + if (process.platform === "win32") 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'); }); From 7a5d1a03e3912803e1e8fa4bf3b7141e71a1e946 Mon Sep 17 00:00:00 2001 From: Oliver Moran Date: Wed, 21 Apr 2021 19:31:03 +0100 Subject: [PATCH 07/11] more useful example --- public/cgi-bin/script.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/public/cgi-bin/script.js b/public/cgi-bin/script.js index eafe8ba5c..75ecdf127 100755 --- a/public/cgi-bin/script.js +++ b/public/cgi-bin/script.js @@ -1,8 +1,10 @@ #!/usr/bin/env node -// On Linux and Mac set this file to executable using: -// chmod +x script.js - -console.log('Content-Type: text/plain') +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]); +} From 2dc2b1fdb28a8c82c75b987e22c8400211373eb8 Mon Sep 17 00:00:00 2001 From: Oliver Moran Date: Wed, 21 Apr 2021 19:31:23 +0100 Subject: [PATCH 08/11] add --cgi to readme docs --- README.md | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) 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 ``` From d82c1cabc18387c6a30dc85dcfc5fe92eb01b0ed Mon Sep 17 00:00:00 2001 From: Oliver Moran Date: Wed, 21 Apr 2021 22:03:19 +0100 Subject: [PATCH 09/11] decode URI components to create path --- lib/http-server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/http-server.js b/lib/http-server.js index 1b78f7c9b..72ceff4e1 100644 --- a/lib/http-server.js +++ b/lib/http-server.js @@ -157,7 +157,7 @@ function HttpServer(options) { 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, url.parse(req.url).pathname); + 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({ From 5f6f2dcca4d3dd3d320617a8544009134769fd98 Mon Sep 17 00:00:00 2001 From: Oliver Moran Date: Fri, 23 Apr 2021 00:14:11 +0100 Subject: [PATCH 10/11] match coding style to project lint --- lib/http-server.js | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/http-server.js b/lib/http-server.js index 72ceff4e1..86bc11ab9 100644 --- a/lib/http-server.js +++ b/lib/http-server.js @@ -156,19 +156,18 @@ 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) { + 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()){ - + if (fs.existsSync(script) && fs.lstatSync(script).isFile()) { var stderr = new Stream.Writable({ - write: function(chunk, encoding, next) { + write: function (chunk, encoding, next) { res.statusCode = 500; res.write(chunk); next(); } }); - stderr.on('finish', function() { + stderr.on('finish', function () { res.end(); stderr.end(); }); @@ -176,33 +175,37 @@ function HttpServer(options) { var args = []; var cmd = script; - if (!executable.sync(script) || process.platform === "win32"){ + if (!executable.sync(script) || process.platform === 'win32') { switch (path.extname(script).toLowerCase()) { - case ".js": + case '.js': // we can execute using Node - cmd = "node" + cmd = 'node'; args = [script]; break; - case ".bat": - case ".cmd": - case ".exe": - // OK on Windows - if (process.platform === "win32") 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(){ + cgi(cmd, { stderr: stderr, args: args })(req, res, function () { res.emit('next'); }); - - } else { + } + else { // not a script so allow default behavior res.emit('next'); } - } else { + } + else { res.emit('next'); } }); From 879a005fa17dfc9ece699edc7cc6c6f81a3e4d3f Mon Sep 17 00:00:00 2001 From: Oliver Moran Date: Fri, 23 Apr 2021 00:16:30 +0100 Subject: [PATCH 11/11] add tests --- test/fixtures/root/cgi-bin/broken.js | 3 ++ test/fixtures/root/cgi-bin/file.js | 3 ++ test/http-server-test.js | 57 ++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 test/fixtures/root/cgi-bin/broken.js create mode 100644 test/fixtures/root/cgi-bin/file.js 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);