Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/webtorrent/webtorrent.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/file-stream.js3
-rw-r--r--lib/file.js26
-rw-r--r--lib/rarity-map.js2
-rw-r--r--lib/server.js74
-rw-r--r--lib/torrent.js81
-rw-r--r--lib/webconn.js1
6 files changed, 131 insertions, 56 deletions
diff --git a/lib/file-stream.js b/lib/file-stream.js
index 72622b2..f6a8402 100644
--- a/lib/file-stream.js
+++ b/lib/file-stream.js
@@ -56,9 +56,10 @@ class FileStream extends stream.Readable {
this._torrent.store.get(p, (err, buffer) => {
this._notifying = false
if (this.destroyed) return
- if (err) return this._destroy(err)
debug('read %s (length %s) (err %s)', p, buffer.length, err && err.message)
+ if (err) return this._destroy(err)
+
if (this._offset) {
buffer = buffer.slice(this._offset)
this._offset = 0
diff --git a/lib/file.js b/lib/file.js
index 8bcfe22..3e75091 100644
--- a/lib/file.js
+++ b/lib/file.js
@@ -41,10 +41,12 @@ class File extends EventEmitter {
const { _startPiece: start, _endPiece: end } = this
const piece = pieces[start]
- // Calculate first piece diffrently, it sometimes have a offset
+ // First piece may have an offset, e.g. irrelevant bytes from the end of
+ // the previous file
+ const irrelevantFirstPieceBytes = this.offset % pieceLength
let downloaded = bitfield.get(start)
- ? pieceLength - (this.offset % pieceLength)
- : Math.max(piece.length - piece.missing - this.offset, 0)
+ ? pieceLength - irrelevantFirstPieceBytes
+ : Math.max(pieceLength - irrelevantFirstPieceBytes - piece.missing, 0)
for (let index = start + 1; index <= end; ++index) {
if (bitfield.get(index)) {
@@ -53,12 +55,12 @@ class File extends EventEmitter {
} else {
// "in progress" data
const piece = pieces[index]
- downloaded += piece.length - piece.missing
+ downloaded += pieceLength - piece.missing
}
}
- // We don't have a end-offset and one small file can fith in the middle
- // of one chunk, so return this.length if it's oversized
+ // We don't know the end offset, so return this.length if it's oversized.
+ // e.g. One small file can fit in the middle of a piece.
return Math.min(downloaded, this.length)
}
@@ -104,12 +106,20 @@ class File extends EventEmitter {
getBlob (cb) {
if (typeof window === 'undefined') throw new Error('browser-only method')
- streamToBlob(this.createReadStream(), this._getMimeType(), cb)
+ streamToBlob(this.createReadStream(), this._getMimeType())
+ .then(
+ blob => cb(null, blob),
+ err => cb(err)
+ )
}
getBlobURL (cb) {
if (typeof window === 'undefined') throw new Error('browser-only method')
- streamToBlobURL(this.createReadStream(), this._getMimeType(), cb)
+ streamToBlobURL(this.createReadStream(), this._getMimeType())
+ .then(
+ blobUrl => cb(null, blobUrl),
+ err => cb(err)
+ )
}
appendTo (elem, opts, cb) {
diff --git a/lib/rarity-map.js b/lib/rarity-map.js
index 7630734..af712cc 100644
--- a/lib/rarity-map.js
+++ b/lib/rarity-map.js
@@ -45,7 +45,7 @@ class RarityMap {
if (availability === min) {
candidates.push(i)
} else if (availability < min) {
- candidates = [ i ]
+ candidates = [i]
min = availability
}
}
diff --git a/lib/server.js b/lib/server.js
index 953d6a1..b2e307d 100644
--- a/lib/server.js
+++ b/lib/server.js
@@ -1,9 +1,9 @@
const arrayRemove = require('unordered-array-remove')
+const escapeHtml = require('escape-html')
const http = require('http')
const mime = require('mime')
const pump = require('pump')
const rangeParser = require('range-parser')
-const url = require('url')
function Server (torrent, opts = {}) {
const server = http.createServer()
@@ -53,13 +53,6 @@ function Server (torrent, opts = {}) {
// deny them
if (req.headers.origin == null) return false
- // If a 'hostname' string is specified, deny requests with a 'Host'
- // header that does not match the origin of the torrent server to prevent
- // DNS rebinding attacks.
- if (opts.hostname && req.headers.host !== `${opts.hostname}:${server.address().port}`) {
- return false
- }
-
// The user allowed all origins
if (opts.origin === '*') return true
@@ -76,12 +69,15 @@ function Server (torrent, opts = {}) {
}
function onRequest (req, res) {
- const pathname = url.parse(req.url).pathname
-
- if (pathname === '/favicon.ico') {
- return serve404Page()
+ // If a 'hostname' string is specified, deny requests with a 'Host'
+ // header that does not match the origin of the torrent server to prevent
+ // DNS rebinding attacks.
+ if (opts.hostname && req.headers.host !== `${opts.hostname}:${server.address().port}`) {
+ return req.destroy()
}
+ const pathname = new URL(req.url, 'http://example.com').pathname
+
// Allow cross-origin requests (CORS)
if (isOriginAllowed(req)) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
@@ -90,6 +86,13 @@ function Server (torrent, opts = {}) {
// Prevent browser mime-type sniffing
res.setHeader('X-Content-Type-Options', 'nosniff')
+ // Defense-in-depth: Set a strict Content Security Policy to mitigate XSS
+ res.setHeader('Content-Security-Policy', "base-uri 'none'; default-src 'none'; frame-ancestors 'none'; form-action 'none';")
+
+ if (pathname === '/favicon.ico') {
+ return serve404Page()
+ }
+
// Allow CORS requests to specify arbitrary headers, e.g. 'Range',
// by responding to the OPTIONS preflight request with the specified
// origin and requested headers.
@@ -147,11 +150,26 @@ function Server (torrent, opts = {}) {
res.statusCode = 200
res.setHeader('Content-Type', 'text/html')
- const listHtml = torrent.files.map((file, i) => `<li><a download="${file.name}" href="/${i}/${file.name}">${file.path}</a> (${file.length} bytes)</li>`).join('<br>')
+ const listHtml = torrent.files
+ .map((file, i) => (
+ `<li>
+ <a
+ download="${escapeHtml(file.name)}"
+ href="${escapeHtml(i)}/${escapeHtml(file.name)}"
+ >
+ ${escapeHtml(file.path)}
+ </a>
+ (${escapeHtml(file.length)} bytes)
+ </li>`
+ ))
+ .join('<br>')
const html = getPageHTML(
- `${torrent.name} - WebTorrent`,
- `<h1>${torrent.name}</h1><ol>${listHtml}</ol>`
+ `${escapeHtml(torrent.name)} - WebTorrent`,
+ `
+ <h1>${escapeHtml(torrent.name)}</h1>
+ <ol>${listHtml}</ol>
+ `
)
res.end(html)
}
@@ -160,13 +178,16 @@ function Server (torrent, opts = {}) {
res.statusCode = 404
res.setHeader('Content-Type', 'text/html')
- const html = getPageHTML('404 - Not Found', '<h1>404 - Not Found</h1>')
+ const html = getPageHTML(
+ '404 - Not Found',
+ '<h1>404 - Not Found</h1>'
+ )
res.end(html)
}
function serveFile (file) {
res.statusCode = 200
- res.setHeader('Content-Type', mime.getType(file.name))
+ res.setHeader('Content-Type', mime.getType(file.name) || 'application/octet-stream')
// Support range-requests
res.setHeader('Accept-Ranges', 'bytes')
@@ -214,7 +235,10 @@ function Server (torrent, opts = {}) {
function serveMethodNotAllowed () {
res.statusCode = 405
res.setHeader('Content-Type', 'text/html')
- const html = getPageHTML('405 - Method Not Allowed', '<h1>405 - Method Not Allowed</h1>')
+ const html = getPageHTML(
+ '405 - Method Not Allowed',
+ '<h1>405 - Method Not Allowed</h1>'
+ )
res.end(html)
}
}
@@ -222,8 +246,20 @@ function Server (torrent, opts = {}) {
return server
}
+// NOTE: Arguments must already be HTML-escaped
function getPageHTML (title, pageHtml) {
- return `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>${title}</title></head><body>${pageHtml}</body></html>`
+ 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
diff --git a/lib/torrent.js b/lib/torrent.js
index 8f667ff..77cf898 100644
--- a/lib/torrent.js
+++ b/lib/torrent.js
@@ -1,4 +1,4 @@
-/* global URL, Blob */
+/* global Blob */
const addrToIPPort = require('addr-to-ip-port')
const BitField = require('bitfield')
@@ -46,7 +46,7 @@ const RECHOKE_OPTIMISTIC_DURATION = 2 // 30 seconds
// IndexedDB chunk stores used in the browser benefit from maximum concurrency
const FILESYSTEM_CONCURRENCY = process.browser ? Infinity : 2
-const RECONNECT_WAIT = [ 1000, 5000, 15000 ]
+const RECONNECT_WAIT = [1000, 5000, 15000]
const VERSION = require('../package.json').version
const USER_AGENT = `WebTorrent/${VERSION} (https://webtorrent.io)`
@@ -169,7 +169,7 @@ class Torrent extends EventEmitter {
if (typeof window === 'undefined') throw new Error('browser-only property')
if (!this.torrentFile) return null
return URL.createObjectURL(
- new Blob([ this.torrentFile ], { type: 'application/x-bittorrent' })
+ new Blob([this.torrentFile], { type: 'application/x-bittorrent' })
)
}
@@ -277,6 +277,19 @@ class Torrent extends EventEmitter {
}
_onListening () {
+ if (this.destroyed) return
+
+ if (this.info) {
+ // if full metadata was included in initial torrent id, use it immediately. Otherwise,
+ // wait for torrent-discovery to find peers and ut_metadata to get the metadata.
+ this._onMetadata(this)
+ } else {
+ if (this.xs) this._getMetadataFromServer()
+ this._startDiscovery()
+ }
+ }
+
+ _startDiscovery () {
if (this.discovery || this.destroyed) return
let trackerOpts = this.client.tracker
@@ -334,21 +347,13 @@ class Torrent extends EventEmitter {
this.discovery.on('warning', (err) => {
this.emit('warning', err)
})
-
- if (this.info) {
- // if full metadata was included in initial torrent id, use it immediately. Otherwise,
- // wait for torrent-discovery to find peers and ut_metadata to get the metadata.
- this._onMetadata(this)
- } else if (this.xs) {
- this._getMetadataFromServer()
- }
}
_getMetadataFromServer () {
// to allow function hoisting
const self = this
- const urls = Array.isArray(this.xs) ? this.xs : [ this.xs ]
+ const urls = Array.isArray(this.xs) ? this.xs : [this.xs]
const tasks = urls.map(url => cb => {
getMetadataFromURL(url, cb)
@@ -500,11 +505,23 @@ class Torrent extends EventEmitter {
this._onWireWithMetadata(wire)
})
+ // Emit 'metadata' before 'ready' and 'done'
+ this.emit('metadata')
+
+ // User might destroy torrent in response to 'metadata' event
+ if (this.destroyed) return
+
if (this.skipVerify) {
// Skip verifying exisitng data and just assume it's correct
this._markAllVerified()
this._onStore()
} else {
+ const onPiecesVerified = (err) => {
+ if (err) return this._destroy(err)
+ this._debug('done verifying')
+ this._onStore()
+ }
+
this._debug('verifying existing torrent data')
if (this._fileModtimes && this._store === FSChunkStore) {
// don't verify if the files haven't been modified since we last checked
@@ -517,15 +534,13 @@ class Torrent extends EventEmitter {
this._markAllVerified()
this._onStore()
} else {
- this._verifyPieces()
+ this._verifyPieces(onPiecesVerified)
}
})
} else {
- this._verifyPieces()
+ this._verifyPieces(onPiecesVerified)
}
}
-
- this.emit('metadata')
}
/*
@@ -547,8 +562,8 @@ class Torrent extends EventEmitter {
})
}
- _verifyPieces () {
- parallelLimit(this.pieces.map((_, index) => cb => {
+ _verifyPieces (cb) {
+ parallelLimit(this.pieces.map((piece, index) => cb => {
if (this.destroyed) return cb(new Error('torrent is destroyed'))
this.store.get(index, (err, buf) => {
@@ -559,7 +574,7 @@ class Torrent extends EventEmitter {
if (this.destroyed) return cb(new Error('torrent is destroyed'))
if (hash === this._hashes[index]) {
- if (!this.pieces[index]) return
+ if (!this.pieces[index]) return cb(null)
this._debug('piece verified %s', index)
this._markVerified(index)
} else {
@@ -568,10 +583,21 @@ class Torrent extends EventEmitter {
cb(null)
})
})
- }), FILESYSTEM_CONCURRENCY, err => {
- if (err) return this._destroy(err)
- this._debug('done verifying')
- this._onStore()
+ }), FILESYSTEM_CONCURRENCY, cb)
+ }
+
+ rescanFiles (cb) {
+ if (this.destroyed) throw new Error('torrent is destroyed')
+ if (!cb) cb = noop
+
+ this._verifyPieces((err) => {
+ if (err) {
+ this._destroy(err)
+ return cb(err)
+ }
+
+ this._checkDone()
+ cb(null)
})
}
@@ -594,6 +620,9 @@ class Torrent extends EventEmitter {
if (this.destroyed) return
this._debug('on store')
+ // Start discovery before emitting 'ready'
+ this._startDiscovery()
+
this.ready = true
this.emit('ready')
@@ -820,7 +849,7 @@ class Torrent extends EventEmitter {
if (this.destroyed) throw new Error('torrent is destroyed')
if (start < 0 || end < start || this.pieces.length <= end) {
- throw new Error('invalid selection ', start, ':', end)
+ throw new Error(`invalid selection ${start} : ${end}`)
}
priority = Number(priority) || 0
@@ -1146,7 +1175,7 @@ class Torrent extends EventEmitter {
const self = this
if (typeof window !== 'undefined' && typeof window.requestIdleCallback === 'function') {
- window.requestIdleCallback(function () { self._updateWire(wire) })
+ window.requestIdleCallback(function () { self._updateWire(wire) }, { timeout: 250 })
} else {
self._updateWire(wire)
}
@@ -1564,7 +1593,7 @@ class Torrent extends EventEmitter {
if (this.destroyed) throw new Error('torrent is destroyed')
if (!this.ready) return this.once('ready', () => { this.load(streams, cb) })
- if (!Array.isArray(streams)) streams = [ streams ]
+ if (!Array.isArray(streams)) streams = [streams]
if (!cb) cb = noop
const readable = new MultiStream(streams)
diff --git a/lib/webconn.js b/lib/webconn.js
index 9c2b461..e99c04c 100644
--- a/lib/webconn.js
+++ b/lib/webconn.js
@@ -1,5 +1,4 @@
const BitField = require('bitfield')
-const Buffer = require('safe-buffer').Buffer
const debug = require('debug')('webtorrent:webconn')
const get = require('simple-get')
const sha1 = require('simple-sha1')