Spaces:
Running
Running
File size: 4,773 Bytes
95f4e64 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
import BitField from 'bitfield'
import debugFactory from 'debug'
import fetch from 'cross-fetch-ponyfill'
import ltDontHave from 'lt_donthave'
import { hash, concat } from 'uint8-util'
import Wire from 'bittorrent-protocol'
import once from 'once'
import VERSION from '../version.cjs'
const debug = debugFactory('webtorrent:webconn')
const SOCKET_TIMEOUT = 60000
const RETRY_DELAY = 10000
/**
* Converts requests for torrent blocks into http range requests.
* @param {string} url web seed url
* @param {Object} torrent
*/
export default class WebConn extends Wire {
constructor (url, torrent) {
super()
this.url = url
this.connId = url // Unique id to deduplicate web seeds
this._torrent = torrent
this._init(url)
}
_init (url) {
this.setKeepAlive(true)
this.use(ltDontHave())
this.once('handshake', async (infoHash, peerId) => {
const hex = await hash(url, 'hex') // Used as the peerId for this fake remote peer
if (this.destroyed) return
this.handshake(infoHash, hex)
const numPieces = this._torrent.pieces.length
const bitfield = new BitField(numPieces)
for (let i = 0; i <= numPieces; i++) {
bitfield.set(i, true)
}
this.bitfield(bitfield)
})
this.once('interested', () => {
debug('interested')
this.unchoke()
})
this.on('uninterested', () => { debug('uninterested') })
this.on('choke', () => { debug('choke') })
this.on('unchoke', () => { debug('unchoke') })
this.on('bitfield', () => { debug('bitfield') })
this.lt_donthave.on('donthave', () => { debug('donthave') })
this.on('request', (pieceIndex, offset, length, callback) => {
debug('request pieceIndex=%d offset=%d length=%d', pieceIndex, offset, length)
this.httpRequest(pieceIndex, offset, length, (err, data) => {
if (err) {
// Cancel all in progress requests for this piece
this.lt_donthave.donthave(pieceIndex)
// Wait a little while before saying the webseed has the failed piece again
const retryTimeout = setTimeout(() => {
if (this.destroyed) return
this.have(pieceIndex)
}, RETRY_DELAY)
if (retryTimeout.unref) retryTimeout.unref()
}
callback(err, data)
})
})
}
async httpRequest (pieceIndex, offset, length, cb) {
cb = once(cb)
const pieceOffset = pieceIndex * this._torrent.pieceLength
const rangeStart = pieceOffset + offset /* offset within whole torrent */
const 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
const files = this._torrent.files
let requests
if (files.length <= 1) {
requests = [{
url: this.url,
start: rangeStart,
end: rangeEnd
}]
} else {
const requestedFiles = files.filter(file => file.offset <= rangeEnd && (file.offset + file.length) > rangeStart)
if (requestedFiles.length < 1) {
return cb(new Error('Could not find file corresponding to web seed range request'))
}
requests = requestedFiles.map(requestedFile => {
const fileEnd = requestedFile.offset + requestedFile.length - 1
const url = this.url +
(this.url[this.url.length - 1] === '/' ? '' : '/') +
requestedFile.path.replace(this._torrent.path, '')
return {
url,
fileOffsetInRange: Math.max(requestedFile.offset - rangeStart, 0),
start: Math.max(rangeStart - requestedFile.offset, 0),
end: Math.min(fileEnd, rangeEnd - requestedFile.offset)
}
})
}
let chunks
try {
chunks = await Promise.all(requests.map(async ({ start, end, url }) => {
debug(
'Requesting url=%s pieceIndex=%d offset=%d length=%d start=%d end=%d',
url, pieceIndex, offset, length, start, end
)
const res = await fetch(url, {
cache: 'no-store',
method: 'GET',
headers: {
'Cache-Control': 'no-store',
'user-agent': `WebTorrent/${VERSION} (https://webtorrent.io)`,
range: `bytes=${start}-${end}`
},
signal: AbortSignal.timeout(SOCKET_TIMEOUT)
})
if (!res.ok) throw new Error(`Unexpected HTTP status code ${res.status}`)
const data = new Uint8Array(await res.arrayBuffer())
debug('Got data of length %d', data.length)
return data
}))
} catch (e) {
return cb(e)
}
cb(null, concat(chunks))
}
destroy () {
super.destroy()
this._torrent = null
}
}
|