diff --git a/README.md b/README.md index 272a2f35..918e03f3 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ with the provided Dockerfile. |`-b` or `--brotli`|When enabled it will serve `./public/some-file.js.br` in place of `./public/some-file.js` when a brotli compressed version of the file exists and the request accepts `br` encoding. If gzip is also enabled, it will try to serve brotli first. |`false`| |`-e` or `--ext` |Default file extension if none supplied |`html` | |`-s` or `--silent` |Suppress log messages from output | | +|`--coop` |Enable COOP via the `Cross-Origin-Opener-Policy` header | | |`--cors` |Enable CORS via the `Access-Control-Allow-Origin` header | | |`-H` or `--header` |Add an extra response header (can be used several times) | | |`-o [path]` |Open browser window after starting the server. Optionally provide a URL path to open. e.g.: -o /other/dir/ | | @@ -136,6 +137,7 @@ This is what should be output if successful: Starting up http-server, serving ./ through https http-server settings: +COOP: disabled CORS: disabled Cache: 3600 seconds Connection Timeout: 120 seconds diff --git a/bin/http-server b/bin/http-server index a5b79f2f..3886615b 100755 --- a/bin/http-server +++ b/bin/http-server @@ -35,6 +35,8 @@ if (argv.h || argv.help) { ' If both brotli and gzip are enabled, brotli takes precedence', ' -e --ext Default file extension if none supplied [none]', ' -s --silent Suppress log messages from output', + ' --coop[=mode] Enable COOP via the "Cross-Origin-Opener-Policy" header', + ' Optionally provide COOP mode.', ' --cors[=headers] Enable CORS via the "Access-Control-Allow-Origin" header', ' Optionally provide CORS headers list separated by commas', ' -H', @@ -173,6 +175,13 @@ function listen(port) { } } + if (argv.coop) { + options.coop = true; + if (typeof argv.coop === 'string') { + options.coopHeader = argv.coop; + } + } + if (argv.cors) { options.cors = true; if (typeof argv.cors === 'string') { @@ -236,6 +245,7 @@ function listen(port) { logger.info([ chalk.yellow('\nhttp-server settings: '), + ([chalk.yellow('COOP: '), argv.coop ? chalk.cyan(argv.coop) : chalk.red('disabled')].join('')), ([chalk.yellow('CORS: '), argv.cors ? chalk.cyan(argv.cors) : chalk.red('disabled')].join('')), ([chalk.yellow('Cache: '), argv.c ? (argv.c === '-1' ? chalk.red('disabled') : chalk.cyan(argv.c + ' seconds')) : chalk.cyan('3600 seconds')].join('')), ([chalk.yellow('Connection Timeout: '), argv.t === '0' ? chalk.red('disabled') : (argv.t ? chalk.cyan(argv.t + ' seconds') : chalk.cyan('120 seconds'))].join('')), diff --git a/doc/http-server.1 b/doc/http-server.1 index 1337c5a2..860192fd 100644 --- a/doc/http-server.1 +++ b/doc/http-server.1 @@ -57,6 +57,12 @@ Default file extension is none is provided. .BI \-s ", " \-\-silent Suppress log messages from output. +.TP +.BI \-\-coop " " [\fIMODE\fR] +Enable COOP via the "Cross-Origin-Opener-Policy" header and sets +the "Cross-Origin-Embedder-Policy" header to "require-corp". +Optionally provide COOP mode which defaults to "same-origin". + .TP .BI \-\-cors " " [\fIHEADERS\fR] Enable CORS via the "Access-Control-Allow-Origin" header. diff --git a/lib/core/aliases.json b/lib/core/aliases.json index 53a22a56..229a16d5 100644 --- a/lib/core/aliases.json +++ b/lib/core/aliases.json @@ -6,6 +6,7 @@ "hidePermissions": ["hidePermissions", "hidepermissions", "hide-permissions"], "si": [ "si", "index" ], "handleError": [ "handleError", "handleerror" ], + "coop": [ "coop", "COOP" ], "cors": [ "cors", "CORS" ], "headers": [ "H", "header", "headers" ], "contentType": [ "contentType", "contenttype", "content-type" ], diff --git a/lib/core/defaults.json b/lib/core/defaults.json index d919f292..63a1a9aa 100644 --- a/lib/core/defaults.json +++ b/lib/core/defaults.json @@ -6,6 +6,7 @@ "hidePermissions": false, "si": false, "cache": "max-age=3600", + "coop": false, "cors": false, "gzip": true, "brotli": false, diff --git a/lib/core/opts.js b/lib/core/opts.js index ec1b2cbc..097ab18b 100644 --- a/lib/core/opts.js +++ b/lib/core/opts.js @@ -117,6 +117,14 @@ module.exports = (opts) => { return false; }); + aliases.coop.forEach((k) => { + if (isDeclared(k) && opts[k]) { + handleOptionsMethod = true; + headers['Cross-Origin-Opener-Policy'] = 'same-origin'; + headers['Cross-Origin-Embedder-Policy'] = 'require-corp'; + } + }); + aliases.cors.forEach((k) => { if (isDeclared(k) && opts[k]) { handleOptionsMethod = true; diff --git a/lib/http-server.js b/lib/http-server.js index 0e946ac7..2948510d 100644 --- a/lib/http-server.js +++ b/lib/http-server.js @@ -98,6 +98,11 @@ function HttpServer(options) { }); } + if (options.coop) { + this.headers['Cross-Origin-Opener-Policy'] = options.coopHeader || 'same-origin'; + this.headers['Cross-Origin-Embedder-Policy'] = 'require-corp'; + } + if (options.cors) { this.headers['Access-Control-Allow-Origin'] = '*'; this.headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Range'; diff --git a/test/coop.test.js b/test/coop.test.js new file mode 100644 index 00000000..375df0cf --- /dev/null +++ b/test/coop.test.js @@ -0,0 +1,118 @@ +'use strict'; + +const test = require('tap').test; +const server = require('../lib/core'); +const http = require('http'); +const path = require('path'); +const request = require('request'); + +const root = path.join(__dirname, 'public'); + +test('coop defaults to false', (t) => { + t.plan(4); + + const httpServer = http.createServer( + server({ + root, + autoIndex: true, + defaultExt: 'html', + }) + ); + + httpServer.listen(() => { + const port = httpServer.address().port; + const uri = `http://localhost:${port}/subdir/index.html`; + + request.get({ uri }, (err, res) => { + t.error(err); + t.equal(res.statusCode, 200); + t.type(res.headers['cross-origin-opener-policy'], 'undefined'); + t.type(res.headers['cross-origin-embedder-policy'], 'undefined'); + }); + }); + t.once('end', () => { + httpServer.close(); + }); +}); + +test('coop set to false', (t) => { + t.plan(4); + + const httpServer = http.createServer( + server({ + root, + coop: false, + autoIndex: true, + defaultExt: 'html', + }) + ); + + httpServer.listen(() => { + const port = httpServer.address().port; + const uri = `http://localhost:${port}/subdir/index.html`; + + request.get({ uri }, (err, res) => { + t.error(err); + t.equal(res.statusCode, 200); + t.type(res.headers['cross-origin-opener-policy'], 'undefined'); + t.type(res.headers['cross-origin-embedder-policy'], 'undefined'); + }); + }); + t.once('end', () => { + httpServer.close(); + }); +}); + +test('coop set to true', (t) => { + t.plan(4); + + const httpServer = http.createServer( + server({ + root, + coop: true, + autoIndex: true, + defaultExt: 'html', + }) + ); + + httpServer.listen(() => { + const port = httpServer.address().port; + const uri = `http://localhost:${port}/subdir/index.html`; + request.get({ uri }, (err, res) => { + t.error(err); + t.equal(res.statusCode, 200); + t.equal(res.headers['cross-origin-opener-policy'], 'same-origin'); + t.equal(res.headers['cross-origin-embedder-policy'], 'require-corp'); + }); + }); + t.once('end', () => { + httpServer.close(); + }); +}); + +test('COOP set to true', (t) => { + t.plan(4); + + const httpServer = http.createServer( + server({ + root, + COOP: true, + autoIndex: true, + defaultExt: 'html', + }) + ); + + httpServer.listen(() => { + const port = httpServer.address().port; + const uri = `http://localhost:${port}/subdir/index.html`; + request.get({ uri }, (err, res) => { + t.error(err); + t.equal(res.statusCode, 200); + t.equal(res.headers['cross-origin-opener-policy'], 'same-origin'); + t.equal(res.headers['cross-origin-embedder-policy'], 'require-corp'); + }); + }); + t.once('end', () => { + httpServer.close(); + }); +}); diff --git a/test/main.test.js b/test/main.test.js index 3e3e4a63..fde6d423 100644 --- a/test/main.test.js +++ b/test/main.test.js @@ -26,6 +26,7 @@ test('http-server main', (t) => { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'true' }, + coop: true, cors: true, corsHeaders: 'X-Test', ext: true, @@ -63,6 +64,8 @@ test('http-server main', (t) => { // Custom headers t.equal(res.headers['access-control-allow-origin'], '*'); t.equal(res.headers['access-control-allow-credentials'], 'true'); + t.equal(res.headers['cross-origin-opener-policy'], 'same-origin'); + t.equal(res.headers['cross-origin-embedder-policy'], 'require-corp'); }).catch(err => t.fail(err.toString())), // Get robots