Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
```
Expand Down
5 changes: 4 additions & 1 deletion bin/http-server
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions doc/http-server.1
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
64 changes: 63 additions & 1 deletion lib/http-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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,
Expand Down
67 changes: 65 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions public/cgi-bin/script.js
Original file line number Diff line number Diff line change
@@ -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]);
}
3 changes: 3 additions & 0 deletions test/fixtures/root/cgi-bin/broken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
console.log('Content-Type: text/plain');
console.log('');
throw('cgi like the 90s');
3 changes: 3 additions & 0 deletions test/fixtures/root/cgi-bin/file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
console.log('Content-Type: text/plain');
console.log('');
console.log('cgi like the 90s');
57 changes: 57 additions & 0 deletions test/http-server-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);