module.exports = WebConn var BitField = require('bitfield') var Buffer = require('safe-buffer').Buffer var debug = require('debug')('webtorrent:webconn') var get = require('simple-get') var inherits = require('inherits') var sha1 = require('simple-sha1') var Wire = require('bittorrent-protocol') var VERSION = require('../package.json').version inherits(WebConn, Wire) /** * Converts requests for torrent blocks into http range requests. * @param {string} url web seed url * @param {Object} torrent */ function WebConn (url, torrent) { Wire.call(this) this.url = url this.webPeerId = sha1.sync(url) this._torrent = torrent this._init() } WebConn.prototype._init = function () { var self = this self.setKeepAlive(true) self.once('handshake', function (infoHash, peerId) { if (self.destroyed) return self.handshake(infoHash, self.webPeerId) var numPieces = self._torrent.pieces.length var bitfield = new BitField(numPieces) for (var i = 0; i <= numPieces; i++) { bitfield.set(i, true) } self.bitfield(bitfield) }) self.once('interested', function () { debug('interested') self.unchoke() }) self.on('uninterested', function () { debug('uninterested') }) self.on('choke', function () { debug('choke') }) self.on('unchoke', function () { debug('unchoke') }) self.on('bitfield', function () { debug('bitfield') }) self.on('request', function (pieceIndex, offset, length, callback) { debug('request pieceIndex=%d offset=%d length=%d', pieceIndex, offset, length) self.httpRequest(pieceIndex, offset, length, callback) }) } WebConn.prototype.httpRequest = function (pieceIndex, offset, length, cb) { var self = this var pieceOffset = pieceIndex * self._torrent.pieceLength var rangeStart = pieceOffset + offset /* offset within whole torrent */ var rangeEnd = rangeStart + length - 1 // Web seed URL format: // For single-file torrents, make HTTP range requests directly to the web seed URL // For multi-file torrents, add the torrent folder and file name to the URL var files = self._torrent.files var requests if (files.length <= 1) { requests = [{ url: self.url, start: rangeStart, end: rangeEnd }] } else { var requestedFiles = files.filter(function (file) { return file.offset <= rangeEnd && (file.offset + file.length) > rangeStart }) if (requestedFiles.length < 1) { return cb(new Error('Could not find file corresponnding to web seed range request')) } requests = requestedFiles.map(function (requestedFile) { var fileEnd = requestedFile.offset + requestedFile.length - 1 var url = self.url + (self.url[self.url.length - 1] === '/' ? '' : '/') + requestedFile.path return { url: url, fileOffsetInRange: Math.max(requestedFile.offset - rangeStart, 0), start: Math.max(rangeStart - requestedFile.offset, 0), end: Math.min(fileEnd, rangeEnd - requestedFile.offset) } }) } // Now make all the HTTP requests we need in order to load this piece // Usually that's one requests, but sometimes it will be multiple // Send requests in parallel and wait for them all to come back var numRequestsSucceeded = 0 var hasError = false var ret if (requests.length > 1) { ret = Buffer.alloc(length) } requests.forEach(function (request) { var url = request.url var start = request.start var end = request.end debug( 'Requesting url=%s pieceIndex=%d offset=%d length=%d start=%d end=%d', url, pieceIndex, offset, length, start, end ) var opts = { url: url, method: 'GET', headers: { 'user-agent': 'WebTorrent/' + VERSION + ' (https://webtorrent.io)', range: 'bytes=' + start + '-' + end } } function onResponse (res, data) { if (res.statusCode < 200 || res.statusCode >= 300) { hasError = true return cb(new Error('Unexpected HTTP status code ' + res.statusCode)) } debug('Got data of length %d', data.length) if (requests.length === 1) { // Common case: fetch piece in a single HTTP request, return directly cb(null, data) } else { // Rare case: reconstruct multiple HTTP requests across 2+ files into one // piece buffer data.copy(ret, request.fileOffsetInRange) if (++numRequestsSucceeded === requests.length) { cb(null, ret) } } } get.concat(opts, function (err, res, data) { if (hasError) return if (err) { // Browsers allow HTTP redirects for simple cross-origin // requests but not for requests that require preflight. // Use a simple request to unravel any redirects and get the // final URL. Retry the original request with the new URL if // it's different. // // This test is imperfect but it's simple and good for common // cases. It catches all cross-origin cases but matches a few // same-origin cases too. if (typeof window === 'undefined' || url.startsWith(window.location.origin + '/')) { hasError = true return cb(err) } return get.head(url, function (errHead, res) { if (hasError) return if (errHead) { hasError = true return cb(errHead) } if (res.statusCode < 200 || res.statusCode >= 300) { hasError = true return cb(new Error('Unexpected HTTP status code ' + res.statusCode)) } if (res.url === url) { hasError = true return cb(err) } opts.url = res.url get.concat(opts, function (err, res, data) { if (hasError) return if (err) { hasError = true return cb(err) } onResponse(res, data) }) }) } onResponse(res, data) }) }) } WebConn.prototype.destroy = function () { Wire.prototype.destroy.call(this) this._torrent = null }