Skip to content

Commit 20cb182

Browse files
committed
Merge pull request webtorrent#45 from feross/refactor
Refactor the server-side code
2 parents 28189a0 + c97e423 commit 20cb182

File tree

7 files changed

+435
-370
lines changed

7 files changed

+435
-370
lines changed

lib/common.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,18 @@
44

55
var querystring = require('querystring')
66

7+
exports.NUM_ANNOUNCE_PEERS = 50
8+
exports.MAX_ANNOUNCE_PEERS = 82
9+
710
exports.CONNECTION_ID = Buffer.concat([ toUInt32(0x417), toUInt32(0x27101980) ])
811
exports.ACTIONS = { CONNECT: 0, ANNOUNCE: 1, SCRAPE: 2, ERROR: 3 }
912
exports.EVENTS = { update: 0, completed: 1, started: 2, stopped: 3 }
13+
exports.EVENT_IDS = {
14+
0: 'update',
15+
1: 'completed',
16+
2: 'started',
17+
3: 'stopped'
18+
};
1019

1120
function toUInt32 (n) {
1221
var buf = new Buffer(4)

lib/parse_http.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
var common = require('./common')
2+
3+
var REMOVE_IPV6_RE = /^::ffff:/
4+
5+
module.exports = parseHttpRequest
6+
7+
function parseHttpRequest (req, options) {
8+
var s = req.url.split('?')
9+
var params = common.querystringParse(s[1])
10+
11+
if (s[0] === '/announce') {
12+
params.action = common.ACTIONS.ANNOUNCE
13+
14+
params.peer_id = typeof params.peer_id === 'string' && common.binaryToUtf8(params.peer_id)
15+
params.port = Number(params.port)
16+
17+
if (typeof params.info_hash !== 'string') throw new Error('invalid info_hash')
18+
if (params.info_hash.length !== 20) throw new Error('invalid info_hash length')
19+
if (typeof params.peer_id !== 'string') throw new Error('invalid peer_id')
20+
if (params.peer_id.length !== 20) throw new Error('invalid peer_id length')
21+
if (!params.port) throw new Error('invalid port')
22+
23+
params.left = Number(params.left)
24+
params.compact = Number(params.compact)
25+
26+
params.ip = options.trustProxy
27+
? req.headers['x-forwarded-for'] || req.connection.remoteAddress
28+
: req.connection.remoteAddress.replace(REMOVE_IPV6_RE, '') // force ipv4
29+
params.addr = params.ip + ':' + params.port // TODO: ipv6 brackets?
30+
31+
params.numwant = Math.min(
32+
Number(params.numwant) || common.NUM_ANNOUNCE_PEERS,
33+
common.MAX_ANNOUNCE_PEERS
34+
)
35+
36+
return params
37+
} else if (s[0] === '/scrape') { // unofficial scrape message
38+
params.action = common.ACTIONS.SCRAPE
39+
40+
if (typeof params.info_hash === 'string') {
41+
params.info_hash = [ params.info_hash ]
42+
}
43+
44+
if (params.info_hash) {
45+
if (!Array.isArray(params.info_hash)) throw new Error('invalid info_hash array')
46+
47+
params.info_hash.forEach(function (infoHash) {
48+
if (infoHash.length !== 20) {
49+
throw new Error('invalid info_hash')
50+
}
51+
})
52+
}
53+
54+
return params
55+
} else {
56+
return null
57+
}
58+
}

lib/parse_udp.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
var bufferEqual = require('buffer-equal')
2+
var ipLib = require('ip')
3+
var common = require('./common')
4+
5+
6+
module.exports = parseUdpRequest
7+
8+
function parseUdpRequest (msg, rinfo) {
9+
if (msg.length < 16) {
10+
throw new Error('received packet is too short')
11+
}
12+
13+
if (rinfo.family !== 'IPv4') {
14+
throw new Error('udp tracker does not support IPv6')
15+
}
16+
17+
var params = {
18+
connectionId: msg.slice(0, 8), // 64-bit
19+
action: msg.readUInt32BE(8),
20+
transactionId: msg.readUInt32BE(12)
21+
}
22+
23+
// TODO: randomize:
24+
if (!bufferEqual(params.connectionId, common.CONNECTION_ID)) {
25+
throw new Error('received packet with invalid connection id')
26+
}
27+
28+
if (params.action === common.ACTIONS.CONNECT) {
29+
// No further params
30+
} else if (params.action === common.ACTIONS.ANNOUNCE) {
31+
params.info_hash = msg.slice(16, 36).toString('binary') // 20 bytes
32+
params.peer_id = msg.slice(36, 56).toString('utf8') // 20 bytes
33+
params.downloaded = fromUInt64(msg.slice(56, 64)) // TODO: track this?
34+
params.left = fromUInt64(msg.slice(64, 72))
35+
params.uploaded = fromUInt64(msg.slice(72, 80)) // TODO: track this?
36+
params.event = msg.readUInt32BE(80)
37+
params.event = common.EVENT_IDS[params.event]
38+
if (!params.event) throw new Error('invalid event') // early return
39+
params.ip = msg.readUInt32BE(84) // optional
40+
params.ip = params.ip ?
41+
ipLib.toString(params.ip) :
42+
rinfo.address
43+
params.key = msg.readUInt32BE(88) // TODO: what is this for?
44+
params.numwant = msg.readUInt32BE(92) // optional
45+
// never send more than MAX_ANNOUNCE_PEERS or else the UDP packet will get bigger than
46+
// 512 bytes which is not safe
47+
params.numwant = Math.min(params.numwant || common.NUM_ANNOUNCE_PEERS, common.MAX_ANNOUNCE_PEERS)
48+
params.port = msg.readUInt16BE(96) || rinfo.port // optional
49+
params.addr = params.ip + ':' + params.port // TODO: ipv6 brackets
50+
params.compact = 1 // udp is always compact
51+
52+
} else if (params.action === common.ACTIONS.SCRAPE) { // scrape message
53+
params.info_hash = msg.slice(16, 36).toString('binary') // 20 bytes
54+
55+
// TODO: support multiple info_hash scrape
56+
if (msg.length > 36) {
57+
throw new Error('multiple info_hash scrape not supported')
58+
}
59+
} else {
60+
throw new Error('Invalid action in UDP packet: ' + params.action)
61+
}
62+
63+
return params
64+
}
65+
66+
// HELPER FUNCTIONS
67+
68+
var TWO_PWR_32 = (1 << 16) * 2
69+
70+
/**
71+
* Return the closest floating-point representation to the buffer value. Precision will be
72+
* lost for big numbers.
73+
*/
74+
function fromUInt64 (buf) {
75+
var high = buf.readUInt32BE(0) | 0 // force
76+
var low = buf.readUInt32BE(4) | 0
77+
var lowUnsigned = (low >= 0) ? low : TWO_PWR_32 + low
78+
79+
return high * TWO_PWR_32 + lowUnsigned
80+
}

lib/swarm.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
var debug = require('debug')('bittorrent-tracker')
2+
3+
module.exports = Swarm
4+
5+
// Regard this as the default implementation of an interface that you
6+
// need to support when overriding Server.getSwarm()
7+
function Swarm (infoHash, server) {
8+
this.peers = {}
9+
this.complete = 0
10+
this.incomplete = 0
11+
this.emit = server.emit.bind(server)
12+
}
13+
14+
Swarm.prototype.announce = function (params, cb) {
15+
var self = this
16+
var peer = self.peers[params.addr]
17+
18+
// Dispatch announce event
19+
if (!params.event || params.event === 'empty') params.event = 'update'
20+
var fn = '_onAnnounce_' + params.event
21+
if (self[fn]) {
22+
self[fn](params, peer, function (err) {
23+
// event processed, prepare response:
24+
25+
if (params.left === 0 && peer) peer.complete = true
26+
27+
// send peers
28+
var peers = self._getPeers(params.numwant)
29+
30+
cb(null, {
31+
complete: self.complete,
32+
incomplete: self.incomplete,
33+
peers: peers
34+
})
35+
})
36+
} else {
37+
cb(new Error('invalid event'))
38+
}
39+
}
40+
41+
Swarm.prototype._onAnnounce_started = function (params, peer, cb) {
42+
if (peer) {
43+
debug('unexpected `started` event from peer that is already in swarm')
44+
return this._onAnnounce_update() // treat as an update
45+
}
46+
if (params.left === 0) this.complete += 1
47+
else this.incomplete += 1
48+
peer = this.peers[params.addr] = {
49+
ip: params.ip,
50+
port: params.port,
51+
peerId: params.peer_id
52+
}
53+
this.emit('start', params.addr)
54+
55+
cb()
56+
}
57+
58+
Swarm.prototype._onAnnounce_stopped = function (params, peer, cb) {
59+
if (!peer) {
60+
debug('unexpected `stopped` event from peer that is not in swarm')
61+
return // do nothing
62+
}
63+
if (peer.complete) this.complete -= 1
64+
else this.incomplete -= 1
65+
this.peers[params.addr] = null
66+
this.emit('stop', params.addr)
67+
68+
cb()
69+
}
70+
71+
Swarm.prototype._onAnnounce_completed = function (params, peer, cb) {
72+
if (!peer) {
73+
debug('unexpected `completed` event from peer that is not in swarm')
74+
return start() // treat as a start
75+
}
76+
if (peer.complete) {
77+
debug('unexpected `completed` event from peer that is already marked as completed')
78+
return // do nothing
79+
}
80+
this.complete += 1
81+
this.incomplete -= 1
82+
peer.complete = true
83+
this.emit('complete', params.addr)
84+
85+
cb()
86+
}
87+
88+
Swarm.prototype._onAnnounce_update = function (params, peer, cb) {
89+
if (!peer) {
90+
debug('unexpected `update` event from peer that is not in swarm')
91+
return start() // treat as a start
92+
}
93+
this.emit('update', params.addr)
94+
95+
cb()
96+
}
97+
98+
Swarm.prototype._getPeers = function (numwant) {
99+
var peers = []
100+
for (var peerId in this.peers) {
101+
if (peers.length >= numwant) break
102+
var peer = this.peers[peerId]
103+
if (!peer) continue // ignore null values
104+
peers.push({
105+
'peer id': peer.peerId,
106+
ip: peer.ip,
107+
port: peer.port
108+
})
109+
}
110+
return peers
111+
}
112+
113+
Swarm.prototype.scrape = function (infoHash, params, cb) {
114+
cb(null, {
115+
complete: this.complete,
116+
incomplete: this.incomplete
117+
})
118+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"ip": "^0.3.0",
2727
"once": "^1.3.0",
2828
"portfinder": "^0.2.1",
29+
"run-series": "^1.0.2",
2930
"string2compact": "^1.1.1"
3031
},
3132
"devDependencies": {

0 commit comments

Comments
 (0)