From dd86f016e4bf104d690f6809f9becc29304ddd3f Mon Sep 17 00:00:00 2001 From: Feross Aboukhadijeh Date: Thu, 9 Feb 2017 20:15:59 -0800 Subject: 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 --- lib/server.js | 140 ++++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 107 insertions(+), 33 deletions(-) (limited to 'lib/server.js') 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 '
  • ' + file.path + ' ' + - '(' + file.length + ' bytes)
  • ' - }).join('
    ') - - var html = '

    ' + torrent.name + '

      ' + listHtml + '
    ' - 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 '
  • ' + file.path + ' ' + + '(' + file.length + ' bytes)
  • ' + }).join('
    ') + + var html = getPageHTML( + torrent.name + ' - WebTorrent', + '

    ' + torrent.name + '

      ' + listHtml + '
    ' + ) + res.end(html) + } + + function serve404Page () { + res.statusCode = 404 + res.setHeader('Content-Type', 'text/html') + + var html = getPageHTML('404 - Not Found', '

    404 - Not Found

    ') + 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', '

    405 - Method Not Allowed

    ') + res.end(html) + } } return server } + +function getPageHTML (title, pageHtml) { + return '' + + '' + + '' + title + '' + + '' + pageHtml + '' +} + +// 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) +} -- cgit v1.2.3