diff options
author | Feross Aboukhadijeh <feross@feross.org> | 2017-02-10 07:15:59 +0300 |
---|---|---|
committer | Feross Aboukhadijeh <feross@feross.org> | 2017-02-10 07:15:59 +0300 |
commit | dd86f016e4bf104d690f6809f9becc29304ddd3f (patch) | |
tree | 6b84c9076951e0e8f692d98d9bcc7cefe0ebee26 | |
parent | dfea7406ae643d91f9d77c4a5e5f1374d7faf37e (diff) |
Refactor http server; support content-disposition
Refactored the server into many smaller functions to make it easier to
understand all the different code paths.
- added a Content-Disposition header, which tells the browser the
file's name, since we use urls like http://localhost:port/0 <-- no
human-readable file name
- Server returns valid HTML documents (with all the required tags) now.
- Return 204 status for OPTIONS request
- reduce access-control-max-age to chromium max of 600s
- respond to OPTIONS requests that lack
'access-control-request-headers' (before they were treated as GET)
- return '405 invalid verb' for all other verbs
For: https://github.com/brave/browser-laptop/issues/6737
-rw-r--r-- | lib/server.js | 140 |
1 files changed, 107 insertions, 33 deletions
diff --git a/lib/server.js b/lib/server.js index a812eab..af46b85 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1,7 +1,6 @@ module.exports = Server var arrayRemove = require('unordered-array-remove') -var debug = require('debug')('webtorrent:server') var http = require('http') var mime = require('mime') var pump = require('pump') @@ -51,59 +50,108 @@ function Server (torrent, opts) { } function onRequest (req, res) { - debug('onRequest') + var pathname = url.parse(req.url).pathname + + if (pathname === '/favicon.ico') { + return serve404Page() + } + + // Allow CORS requests to read responses + if (req.headers.origin) { + res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*') + } + + // Prevent browser mime-type sniffing + res.setHeader('X-Content-Type-Options', 'nosniff') // Allow CORS requests to specify arbitrary headers, e.g. 'Range', // by responding to the OPTIONS preflight request with the specified // origin and requested headers. - if (req.method === 'OPTIONS' && req.headers['access-control-request-headers']) { - res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS') - res.setHeader( - 'Access-Control-Allow-Headers', - req.headers['access-control-request-headers'] - ) - res.setHeader('Access-Control-Max-Age', '1728000') - return res.end() + if (req.method === 'OPTIONS') { + return serveOptionsRequest() } - if (req.headers.origin) { - res.setHeader('Access-Control-Allow-Origin', req.headers.origin) + if (req.method === 'GET' || req.method === 'HEAD') { + if (torrent.ready) { + handleRequest() + } else { + pendingReady.push(onReady) + torrent.once('ready', onReady) + } + return } - var pathname = url.parse(req.url).pathname - if (pathname === '/favicon.ico') return res.end() + return serveMethodNotAllowed() + + function serveOptionsRequest () { + res.statusCode = 204 // no content + res.setHeader('Access-Control-Max-Age', '600') + res.setHeader('Access-Control-Allow-Methods', 'GET,HEAD,PUT,PATCH,POST,DELETE') - if (torrent.ready) { - onReady() - } else { - pendingReady.push(onReady) - torrent.once('ready', onReady) + if (req.headers['access-control-request-headers']) { + res.setHeader( + 'Access-Control-Allow-Headers', + req.headers['access-control-request-headers'] + ) + } + res.end() } function onReady () { arrayRemove(pendingReady, pendingReady.indexOf(onReady)) + handleRequest() + } + + function handleRequest () { if (pathname === '/') { - res.setHeader('Content-Type', 'text/html') - var listHtml = torrent.files.map(function (file, i) { - return '<li><a download="' + file.name + '" href="/' + i + '">' + file.path + '</a> ' + - '(' + file.length + ' bytes)</li>' - }).join('<br>') - - var html = '<h1>' + torrent.name + '</h1><ol>' + listHtml + '</ol>' - return res.end(html) + return serveIndexPage() } var index = Number(pathname.slice(1)) if (Number.isNaN(index) || index >= torrent.files.length) { - res.statusCode = 404 - return res.end('404 Not Found') + return serve404Page() } var file = torrent.files[index] + serveFile(file) + } - res.setHeader('Accept-Ranges', 'bytes') - res.setHeader('Content-Type', mime.lookup(file.name)) + function serveIndexPage () { res.statusCode = 200 + res.setHeader('Content-Type', 'text/html') + + var listHtml = torrent.files.map(function (file, i) { + return '<li><a download="' + file.name + '" href="/' + i + '">' + file.path + '</a> ' + + '(' + file.length + ' bytes)</li>' + }).join('<br>') + + var html = getPageHTML( + torrent.name + ' - WebTorrent', + '<h1>' + torrent.name + '</h1><ol>' + listHtml + '</ol>' + ) + res.end(html) + } + + function serve404Page () { + res.statusCode = 404 + res.setHeader('Content-Type', 'text/html') + + var html = getPageHTML('404 - Not Found', '<h1>404 - Not Found</h1>') + res.end(html) + } + + function serveFile (file) { + res.statusCode = 200 + res.setHeader('Content-Type', mime.lookup(file.name)) + + // Support range-requests + res.setHeader('Accept-Ranges', 'bytes') + + // Set name of file (for "Save Page As..." dialog) + res.setHeader( + 'Content-Disposition', + 'inline; filename*=UTF-8\'\'' + encodeRFC5987(file.name) + ) // Support DLNA streaming res.setHeader('transferMode.dlna.org', 'Streaming') @@ -117,11 +165,11 @@ function Server (torrent, opts) { var range = rangeParser(file.length, req.headers.range || '') if (Array.isArray(range)) { + res.statusCode = 206 // indicates that range-request was understood + // no support for multi-range request, just use the first range range = range[0] - res.statusCode = 206 - debug('range %s', JSON.stringify(range)) res.setHeader( 'Content-Range', 'bytes ' + range.start + '-' + range.end + '/' + file.length @@ -138,7 +186,33 @@ function Server (torrent, opts) { pump(file.createReadStream(range), res) } + + function serveMethodNotAllowed () { + res.statusCode = 405 + res.setHeader('Content-Type', 'text/html') + var html = getPageHTML('405 - Method Not Allowed', '<h1>405 - Method Not Allowed</h1>') + res.end(html) + } } return server } + +function getPageHTML (title, pageHtml) { + return '<!DOCTYPE html><html lang="en"><head>' + + '<meta charset="utf-8">' + + '<title>' + title + '</title>' + + '</head><body>' + pageHtml + '</body></html>' +} + +// From https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent +function encodeRFC5987 (str) { + return encodeURIComponent(str) + // Note that although RFC3986 reserves "!", RFC5987 does not, + // so we do not need to escape it + .replace(/['()]/g, escape) // i.e., %27 %28 %29 + .replace(/\*/g, '%2A') + // The following are not required for percent-encoding per RFC5987, + // so we can allow for a little better readability over the wire: |`^ + .replace(/%(?:7C|60|5E)/g, unescape) +} |