Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor http server; support content-disposition #1039

Merged
merged 1 commit into from Feb 10, 2017
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

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: brave/browser-laptop#6737
  • Loading branch information
feross committed Feb 10, 2017
commit dd86f016e4bf104d690f6809f9becc29304ddd3f
@@ -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)
}
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.