quartz_torrent 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. data/bin/quartztorrent_download +127 -0
  2. data/bin/quartztorrent_download_curses +841 -0
  3. data/bin/quartztorrent_magnet_from_torrent +32 -0
  4. data/bin/quartztorrent_show_info +62 -0
  5. data/lib/quartz_torrent.rb +2 -0
  6. data/lib/quartz_torrent/bitfield.rb +314 -0
  7. data/lib/quartz_torrent/blockstate.rb +354 -0
  8. data/lib/quartz_torrent/classifiedpeers.rb +95 -0
  9. data/lib/quartz_torrent/extension.rb +37 -0
  10. data/lib/quartz_torrent/filemanager.rb +543 -0
  11. data/lib/quartz_torrent/formatter.rb +92 -0
  12. data/lib/quartz_torrent/httptrackerclient.rb +121 -0
  13. data/lib/quartz_torrent/interruptiblesleep.rb +27 -0
  14. data/lib/quartz_torrent/log.rb +132 -0
  15. data/lib/quartz_torrent/magnet.rb +92 -0
  16. data/lib/quartz_torrent/memprofiler.rb +27 -0
  17. data/lib/quartz_torrent/metainfo.rb +221 -0
  18. data/lib/quartz_torrent/metainfopiecestate.rb +265 -0
  19. data/lib/quartz_torrent/peer.rb +145 -0
  20. data/lib/quartz_torrent/peerclient.rb +1627 -0
  21. data/lib/quartz_torrent/peerholder.rb +123 -0
  22. data/lib/quartz_torrent/peermanager.rb +170 -0
  23. data/lib/quartz_torrent/peermsg.rb +502 -0
  24. data/lib/quartz_torrent/peermsgserialization.rb +102 -0
  25. data/lib/quartz_torrent/piecemanagerrequestmetadata.rb +12 -0
  26. data/lib/quartz_torrent/rate.rb +58 -0
  27. data/lib/quartz_torrent/ratelimit.rb +48 -0
  28. data/lib/quartz_torrent/reactor.rb +949 -0
  29. data/lib/quartz_torrent/regionmap.rb +124 -0
  30. data/lib/quartz_torrent/semaphore.rb +43 -0
  31. data/lib/quartz_torrent/trackerclient.rb +271 -0
  32. data/lib/quartz_torrent/udptrackerclient.rb +70 -0
  33. data/lib/quartz_torrent/udptrackermsg.rb +250 -0
  34. data/lib/quartz_torrent/util.rb +100 -0
  35. metadata +195 -0
@@ -0,0 +1,123 @@
1
+ require 'quartz_torrent/peer.rb'
2
+ require 'quartz_torrent/util'
3
+
4
+ module QuartzTorrent
5
+ class PeerHolder
6
+ def initialize
7
+ @peersById = {}
8
+ @peersByAddr = {}
9
+ @peersByInfoHash = {}
10
+ @log = LogManager.getLogger("peerholder")
11
+ end
12
+
13
+ def findById(peerId)
14
+ @peersById[peerId]
15
+ end
16
+
17
+ def findByAddr(ip, port)
18
+ @peersByAddr[ip + port.to_s]
19
+ end
20
+
21
+ def findByInfoHash(infoHash)
22
+ l = @peersByInfoHash[infoHash]
23
+ l = [] if ! l
24
+ l
25
+ end
26
+
27
+ def add(peer)
28
+ raise "Peer must have it's infoHash set." if ! peer.infoHash
29
+
30
+ # Do not add if peer is already present by address
31
+ if @peersByAddr.has_key?(byAddrKey(peer))
32
+ @log.debug "Not adding peer #{peer} since it already exists by #{@peersById.has_key?(peer.trackerPeer.id) ? "id" : "addr"}."
33
+ return
34
+ end
35
+
36
+ if peer.trackerPeer.id
37
+ @peersById.pushToList(peer.trackerPeer.id, peer)
38
+
39
+ # If id is null, this is probably a peer received from the tracker that has no ID.
40
+ end
41
+
42
+ @peersByAddr[byAddrKey(peer)] = peer
43
+
44
+ @peersByInfoHash.pushToList(peer.infoHash, peer)
45
+ end
46
+
47
+ # This peer, which previously had no id, has finished handshaking and now has an ID.
48
+ def idSet(peer)
49
+ @peersById.each do |e|
50
+ return if e.eql?(peer)
51
+ end
52
+ @peersById.pushToList(peer.trackerPeer.id, peer)
53
+ end
54
+
55
+ def delete(peer)
56
+ @peersByAddr.delete byAddrKey(peer)
57
+
58
+ list = @peersByInfoHash[peer.infoHash]
59
+ if list
60
+ list.collect! do |p|
61
+ if !p.eql?(peer)
62
+ peer
63
+ else
64
+ nil
65
+ end
66
+ end
67
+ list.compact!
68
+ end
69
+
70
+ if peer.trackerPeer.id
71
+ list = @peersById[peer.trackerPeer.id]
72
+ if list
73
+ list.collect! do |p|
74
+ if !p.eql?(peer)
75
+ peer
76
+ else
77
+ nil
78
+ end
79
+ end
80
+ list.compact!
81
+ end
82
+ end
83
+ end
84
+
85
+ def all
86
+ @peersByAddr.values
87
+ end
88
+
89
+ def size
90
+ @peersByAddr.size
91
+ end
92
+
93
+ def to_s(infoHash = nil)
94
+ def makeFlags(peer)
95
+ s = "["
96
+ s << "c" if peer.amChoked
97
+ s << "i" if peer.peerInterested
98
+ s << "C" if peer.peerChoked
99
+ s << "I" if peer.amInterested
100
+ s << "]"
101
+ s
102
+ end
103
+
104
+ if infoHash
105
+ s = "Peers: \n"
106
+ peers = @peersByInfoHash[infoHash]
107
+ if peers
108
+ peers.each do |peer|
109
+ s << " #{peer.to_s} #{makeFlags(peer)}\n"
110
+ end
111
+ end
112
+ else
113
+ "PeerHolder"
114
+ end
115
+ s
116
+ end
117
+
118
+ private
119
+ def byAddrKey(peer)
120
+ peer.trackerPeer.ip + peer.trackerPeer.port.to_s
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,170 @@
1
+ require 'quartz_torrent/classifiedpeers.rb'
2
+
3
+ module QuartzTorrent
4
+ class ManagePeersResult
5
+ def initialize
6
+ @unchoke = []
7
+ @choke = []
8
+ end
9
+
10
+ # List of peers to unchoke
11
+ attr_accessor :unchoke
12
+ # List of peers to choke
13
+ attr_accessor :choke
14
+
15
+ def to_s
16
+ s = "Peers to unchoke: "
17
+ s << unchoke.collect{ |p| p.nil? ? "'nil'" : "'#{p}'" }.join(" ")
18
+ s << "\n"
19
+ s << "Peers to choke: "
20
+ s << choke.collect{ |p| p.nil? ? "'nil'" : "'#{p}'" }.join(" ")
21
+ s << "\n"
22
+ end
23
+ end
24
+
25
+ # This class is used internally by PeerClient (The bittorrent protocol object) to
26
+ # choke, unchoke, and connect to peers for a specific torrent.
27
+ class PeerManager
28
+
29
+ def initialize
30
+ @logger = LogManager.getLogger("peer_manager")
31
+ @targetActivePeerCount = 50
32
+ @targetUnchokedPeerCount = 4
33
+ @cachedHandshakingAndEstablishedCount = 0
34
+ # An array of Peers that we are allowing to download.
35
+ @downloaders = []
36
+ @optimisticUnchokePeer = nil
37
+ # A peer is considered newly connected when the number of seconds it has had it's connection established
38
+ # is below this number.
39
+ @newlyConnectedDuration = 60
40
+ @optimisticPeerChangeDuration = 30
41
+ @lastOptimisticPeerChangeTime = nil
42
+ end
43
+
44
+ # Determine if we need to connect to more peers.
45
+ # Returns a list of peers to connect to.
46
+ def manageConnections(classifiedPeers)
47
+
48
+ n = classifiedPeers.handshakingPeers.size + classifiedPeers.establishedPeers.size
49
+ if n < @targetActivePeerCount
50
+ result = classifiedPeers.disconnectedPeers.shuffle.first(@targetActivePeerCount - n)
51
+ @logger.debug "There are #{n} peers connected or in handshaking. Will establish #{result.size} more connections to peers."
52
+ result
53
+ else
54
+ []
55
+ end
56
+ end
57
+
58
+ # Given a list of Peer objects (torrent peers), calculate the actions to
59
+ # take.
60
+ def managePeers(classifiedPeers)
61
+ result = ManagePeersResult.new
62
+
63
+ @logger.debug "Manage peers: #{classifiedPeers.disconnectedPeers.size} disconnected, #{classifiedPeers.handshakingPeers.size} handshaking, #{classifiedPeers.establishedPeers.size} established"
64
+ @logger.debug "Manage peers: #{classifiedPeers}"
65
+
66
+ # Unchoke some peers. According to the specification:
67
+ #
68
+ # "...unchoking the four peers which have the best upload rate and are interested. These four peers are referred to as downloaders, because they are interested in downloading from the client."
69
+ # "Peers which have a better upload rate (as compared to the downloaders) but aren't interested get unchoked. If they become interested, the downloader with the worst upload rate gets choked.
70
+ # If a client has a complete file, it uses its upload rate rather than its download rate to decide which peers to unchoke."
71
+ # "at any one time there is a single peer which is unchoked regardless of its upload rate (if interested, it counts as one of the four allowed downloaders). Which peer is optimistically
72
+ # unchoked rotates every 30 seconds. Newly connected peers are three times as likely to start as the current optimistic unchoke as anywhere else in the rotation. This gives them a decent chance
73
+ # of getting a complete piece to upload."
74
+ #
75
+ # This doesn't define initial rampup; On rampup we have no peer upload rate information.
76
+
77
+ # We want to end up with:
78
+ # - At most 4 peers that are both interested and unchoked. These are Downloaders. They should be the ones with
79
+ # the best upload rate.
80
+ # - All peers that have a better upload rate than Downloaders and are not interested are unchoked.
81
+ # - One random peer that is unchoked. If it is interested, it is one of the 4 downloaders.
82
+ # When choosing this random peer, peers that have connected in the last N seconds should be 3 times more
83
+ # likely to be chosen. This peer only changes every 30 seconds.
84
+
85
+ # Step 1: Pick the optimistic unchoke peer
86
+
87
+ selectOptimisticPeer(classifiedPeers)
88
+
89
+ # Step 2: Update the downloaders to be the interested peers with the best upload rate.
90
+
91
+ if classifiedPeers.interestedPeers.size > 0
92
+ bestUploadInterested = classifiedPeers.interestedPeers.sort{ |a,b| b.uploadRate.value <=> a.uploadRate.value}.first(@targetUnchokedPeerCount)
93
+
94
+ # If the optimistic unchoke peer is interested, he counts as a downloader.
95
+ if @optimisticUnchokePeer && @optimisticUnchokePeer.peerInterested
96
+ peerAlreadyIsDownloader = false
97
+ bestUploadInterested.each do |peer|
98
+ if peer.eql?(@optimisticUnchokePeer)
99
+ peerAlreadyIsDownloader = true
100
+ break
101
+ end
102
+ end
103
+ bestUploadInterested[bestUploadInterested.size-1] = @optimisticUnchokePeer if ! peerAlreadyIsDownloader
104
+ end
105
+
106
+ # If one of the downloaders has changed, choke the peer
107
+ downloadersMap = {}
108
+ @downloaders.each{ |d| downloadersMap[d.trackerPeer] = d }
109
+ bestUploadInterested.each do |peer|
110
+ if downloadersMap.delete peer.trackerPeer
111
+ # This peer was already a downloader. No changes.
112
+ else
113
+ # This peer wasn't a downloader before. Now it is; unchoke it
114
+ result.unchoke.push peer if peer.peerChoked
115
+ end
116
+ end
117
+ # Any peers remaining in the map are no longer a downloader. Choke them.
118
+ result.choke = result.choke.concat(downloadersMap.values)
119
+
120
+ @downloaders = bestUploadInterested
121
+ end
122
+
123
+ # Step 3: Unchoke all peers that have a better upload rate but are not interested.
124
+ # However, if we just started up, only unchoke targetUnchokedPeerCount peers.
125
+ if @downloaders.size > 0
126
+ if classifiedPeers.uninterestedPeers.size > 0
127
+ classifiedPeers.uninterestedPeers.each do |peer|
128
+ if peer.uploadRate.value > @downloaders[0].uploadRate.value && peer.peerChoked
129
+ result.unchoke.push peer
130
+ end
131
+ if peer.uploadRate.value < @downloaders[0].uploadRate.value && ! peer.peerChoked && ! peer.eql?(@optimisticUnchokePeer)
132
+ result.choke.push peer
133
+ end
134
+ end
135
+ end
136
+ else
137
+ # No downloaders yet, so we can't tell who is fast or not. Unchoke some
138
+ result.unchoke = result.unchoke.concat(classifiedPeers.uninterestedPeers.first(@targetUnchokedPeerCount))
139
+ end
140
+
141
+ @logger.debug "Manage peers result: #{result}"
142
+
143
+ result
144
+ end
145
+
146
+ private
147
+ # Choose a peer that we will optimistically unchoke.
148
+ def selectOptimisticPeer(classifiedPeers)
149
+ # "at any one time there is a single peer which is unchoked regardless of its upload rate (if interested, it counts as one of the four allowed downloaders). Which peer is optimistically
150
+ # unchoked rotates every 30 seconds. Newly connected peers are three times as likely to start as the current optimistic unchoke as anywhere else in the rotation. This gives them a decent chance
151
+ # of getting a complete piece to upload."
152
+
153
+ if !@lastOptimisticPeerChangeTime || (Time.new - @lastOptimisticPeerChangeTime > @optimisticPeerChangeDuration)
154
+ list = []
155
+ classifiedPeers.establishedPeers.each do |peer|
156
+ if (Time.new - peer.firstEstablishTime) < @newlyConnectedDuration
157
+ 3.times{ list.push peer }
158
+ else
159
+ list.push peer
160
+ end
161
+ end
162
+ @optimisticUnchokePeer = list[rand(list.size)]
163
+ if @optimisticUnchokePeer
164
+ @logger.info "Optimistically unchoked peer set to #{@optimisticUnchokePeer.trackerPeer}"
165
+ @lastOptimisticPeerChangeTime = Time.new
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,502 @@
1
+ require 'quartz_torrent/bitfield.rb'
2
+ require 'bencode'
3
+ module QuartzTorrent
4
+
5
+ # Represents a bittorrent peer protocol generic request message (not the specific piece request message).
6
+ class PeerRequest
7
+ end
8
+
9
+ # Represents a bittorrent peer protocol handshake message.
10
+ class PeerHandshake
11
+ ProtocolName = "BitTorrent protocol"
12
+ InfoHashLen = 20
13
+ PeerIdLen = 20
14
+
15
+ def initialize
16
+ @infoHash = nil
17
+ @peerId = nil
18
+ @reserved = nil
19
+ end
20
+
21
+ def peerId=(v)
22
+ raise "PeerId is not #{PeerIdLen} bytes long" if v.length != PeerIdLen
23
+ @peerId = v
24
+ end
25
+
26
+ def infoHash=(v)
27
+ raise "InfoHash is not #{InfoHashLen} bytes long" if v.length != InfoHashLen
28
+ @infoHash = v
29
+ end
30
+
31
+ attr_accessor :peerId
32
+ attr_accessor :infoHash
33
+ attr_accessor :reserved
34
+
35
+ # Serialize this PeerHandshake message to the passed io object. Throws exceptions on failure.
36
+ def serializeTo(io)
37
+ raise "PeerId is not set" if ! @peerId
38
+ raise "InfoHash is not set" if ! @infoHash
39
+ result = [ProtocolName.length].pack("C")
40
+ result << ProtocolName
41
+ result << [0,0,0,0,0,0x10,0,0].pack("C8") # Reserved. 0x10 means we support extensions (BEP 10).
42
+ result << @infoHash
43
+ result << @peerId
44
+
45
+ io.write result
46
+ end
47
+
48
+ # Unserialize a PeerHandshake message from the passed io object and
49
+ # return it. Throws exceptions on failure.
50
+ def self.unserializeFrom(io)
51
+ result = PeerHandshake.new
52
+ len = io.read(1).unpack("C")[0]
53
+ proto = io.read(len)
54
+ raise "Unrecognized peer protocol name '#{proto}'" if proto != ProtocolName
55
+ result.reserved = io.read(8) # reserved
56
+ result.infoHash = io.read(InfoHashLen)
57
+ result.peerId = io.read(PeerIdLen)
58
+ result
59
+ end
60
+
61
+ # Unserialize the first part of a PeerHandshake message from the passed io object
62
+ # up to but not including the peer id. This is needed when handling a handshake from
63
+ # the tracker which doesn't have a peer id.
64
+ def self.unserializeExceptPeerIdFrom(io)
65
+ result = PeerHandshake.new
66
+ len = io.read(1).unpack("C")[0]
67
+ proto = io.read(len)
68
+ raise "Unrecognized peer protocol name '#{proto}'" if proto != ProtocolName
69
+ result.reserved = io.read(8) # reserved
70
+ result.infoHash = io.read(InfoHashLen)
71
+ result
72
+ end
73
+ end
74
+
75
+ # Represents a bittorrent peer protocol wire message (a non-handshake message).
76
+ # All messages other than handshake have a 4-byte length, 1-byte message id, and payload.
77
+ class PeerWireMessage
78
+ MessageKeepAlive = -1
79
+ MessageChoke = 0
80
+ MessageUnchoke = 1
81
+ MessageInterested = 2
82
+ MessageUninterested = 3
83
+ MessageHave = 4
84
+ MessageBitfield = 5
85
+ MessageRequest = 6
86
+ MessagePiece = 7
87
+ MessageCancel = 8
88
+ MessageExtended = 20
89
+
90
+ def initialize(messageId)
91
+ @messageId = messageId
92
+ end
93
+
94
+ attr_reader :messageId
95
+
96
+ def serializeTo(io)
97
+ io.write [payloadLength+1].pack("N")
98
+ io.write [@messageId].pack("C")
99
+ end
100
+
101
+ # Subclasses must implement this method. It should return an integer.
102
+ def payloadLength
103
+ raise "Subclasses of PeerWireMessage must implement payloadLength but #{self.class} didn't"
104
+ end
105
+
106
+ # Total message length
107
+ def length
108
+ payloadLength + 5
109
+ end
110
+
111
+ def unserialize(payload)
112
+ raise "Subclasses of PeerWireMessage must implement unserialize but #{self.class} didn't"
113
+ end
114
+
115
+ def to_s
116
+ "#{self.class} message"
117
+ end
118
+ end
119
+
120
+ # KeepAlive message. Sent periodically to ensure peer is available.
121
+ class KeepAlive < PeerWireMessage
122
+ def initialize
123
+ super(MessageKeepAlive)
124
+ end
125
+
126
+ def length
127
+ 4
128
+ end
129
+
130
+ def serializeTo(io)
131
+ # A KeepAlive is just a 4byte length set to 0.
132
+ io.write [0].pack("N")
133
+ end
134
+
135
+ def unserialize(payload)
136
+ end
137
+ end
138
+
139
+ # Choke message. Sent to tell peer they are choked.
140
+ class Choke < PeerWireMessage
141
+ def initialize
142
+ super(MessageChoke)
143
+ end
144
+
145
+ def payloadLength
146
+ 0
147
+ end
148
+
149
+ def unserialize(payload)
150
+ end
151
+ end
152
+
153
+ # Unchoke message. Sent to tell peer they are unchoked.
154
+ class Unchoke < PeerWireMessage
155
+ def initialize
156
+ super(MessageUnchoke)
157
+ end
158
+ def payloadLength
159
+ 0
160
+ end
161
+ def unserialize(payload)
162
+ end
163
+ end
164
+
165
+ # Interested message. Sent to tell peer we are interested in some piece they have.
166
+ class Interested < PeerWireMessage
167
+ def initialize
168
+ super(MessageInterested)
169
+ end
170
+ def payloadLength
171
+ 0
172
+ end
173
+ def unserialize(payload)
174
+ end
175
+ end
176
+
177
+ # Uninterested message. Sent to tell peer we are not interested in any piece they have.
178
+ class Uninterested < PeerWireMessage
179
+ def initialize
180
+ super(MessageUninterested)
181
+ end
182
+ def payloadLength
183
+ 0
184
+ end
185
+ def unserialize(payload)
186
+ end
187
+ end
188
+
189
+ # Have message. Sent to all connected peers to notify that we have completed downloading the specified piece.
190
+ class Have < PeerWireMessage
191
+ def initialize
192
+ super(MessageHave)
193
+ end
194
+
195
+ attr_accessor :pieceIndex
196
+
197
+ def payloadLength
198
+ 4
199
+ end
200
+
201
+ def serializeTo(io)
202
+ super(io)
203
+ io.write [@pieceIndex].pack("N")
204
+ end
205
+
206
+ def unserialize(payload)
207
+ @pieceIndex = payload.unpack("N")[0]
208
+ end
209
+
210
+ def to_s
211
+ s = super
212
+ s + ": piece index=#{@pieceIndex}"
213
+ end
214
+ end
215
+
216
+ # Bitfield message. Sent on initial handshake to notify peer of what pieces we have.
217
+ class BitfieldMessage < PeerWireMessage
218
+ def initialize
219
+ super(MessageBitfield)
220
+ end
221
+
222
+ attr_accessor :bitfield
223
+
224
+ def payloadLength
225
+ bitfield.byteLength
226
+ end
227
+
228
+ def serializeTo(io)
229
+ super(io)
230
+ io.write @bitfield.serialize
231
+ end
232
+
233
+ def unserialize(payload)
234
+ @bitfield = Bitfield.new(payload.length*8) if ! @bitfield
235
+ @bitfield.unserialize(payload)
236
+ end
237
+ end
238
+
239
+ # Request message. Request a block within a piece.
240
+ class Request < PeerWireMessage
241
+ def initialize
242
+ super(MessageRequest)
243
+ end
244
+
245
+ attr_accessor :pieceIndex
246
+ attr_accessor :blockOffset
247
+ attr_accessor :blockLength
248
+
249
+ def payloadLength
250
+ 12
251
+ end
252
+
253
+ def serializeTo(io)
254
+ super(io)
255
+ io.write [@pieceIndex, @blockOffset, @blockLength].pack("NNN")
256
+ end
257
+
258
+ def unserialize(payload)
259
+ @pieceIndex, @blockOffset, @blockLength = payload.unpack("NNN")
260
+ end
261
+
262
+ def to_s
263
+ s = super
264
+ s + ": piece index=#{@pieceIndex}, block offset=#{@blockOffset}, block length=#{@blockLength}"
265
+ end
266
+ end
267
+
268
+ # Piece message. Response to a Request message containing the block of data within a piece.
269
+ class Piece < PeerWireMessage
270
+ def initialize
271
+ super(MessagePiece)
272
+ end
273
+
274
+ attr_accessor :pieceIndex
275
+ attr_accessor :blockOffset
276
+ attr_accessor :data
277
+
278
+ def payloadLength
279
+ 8 + @data.length
280
+ end
281
+
282
+ def serializeTo(io)
283
+ super(io)
284
+ io.write [@pieceIndex, @blockOffset, @data].pack("NNa*")
285
+ end
286
+
287
+ def unserialize(payload)
288
+ @pieceIndex, @blockOffset, @data = payload.unpack("NNa*")
289
+ end
290
+
291
+ def to_s
292
+ s = super
293
+ s + ": piece index=#{@pieceIndex}, block offset=#{@blockOffset}"
294
+ end
295
+ end
296
+
297
+ # Cancel message. Cancel an outstanding request.
298
+ class Cancel < PeerWireMessage
299
+ def initialize
300
+ super(MessageCancel)
301
+ end
302
+
303
+ attr_accessor :pieceIndex
304
+ attr_accessor :blockOffset
305
+ attr_accessor :blockLength
306
+
307
+ def payloadLength
308
+ 12
309
+ end
310
+
311
+ def serializeTo(io)
312
+ super(io)
313
+ io.write [@pieceIndex, @blockOffset, @blockLength].pack("NNN")
314
+ end
315
+
316
+ def unserialize(payload)
317
+ @pieceIndex, @blockOffset, @blockLength = payload.unpack("NNN")
318
+ end
319
+
320
+ def to_s
321
+ s = super
322
+ s + ": piece index=#{@pieceIndex}, block offset=#{@blockOffset}, block length=#{@blockLength}"
323
+ end
324
+ end
325
+
326
+ # Extended message. These are extra messages not defined in the base protocol.
327
+ class Extended < PeerWireMessage
328
+ def initialize
329
+ super(MessageExtended)
330
+ end
331
+
332
+ attr_accessor :extendedMessageId
333
+
334
+ def payloadLength
335
+ 1 + extendedMsgPayloadLength
336
+ end
337
+
338
+ def unserialize(payload)
339
+ @extendedMessageId = payload.unpack("C")
340
+ end
341
+
342
+ def serializeTo(io)
343
+ super(io)
344
+ io.write [@extendedMessageId].pack("C")
345
+ end
346
+
347
+ def to_s
348
+ s = super
349
+ s + ": extendedMessageId=#{@extendedMessageId}"
350
+ end
351
+
352
+ protected
353
+ def extendedMsgPayloadLength
354
+ raise "Subclasses of Extended must implement extendedMsgPayloadLength"
355
+ end
356
+
357
+ private
358
+ # Given an extended message id, return the subclass of Extended for that message.
359
+ # peerExtendedMessageList should be an array indexed by extended message id that returns a subclass of Extended
360
+ def self.classForMessage(id, peerExtendedMessageList)
361
+ return ExtendedHandshake if id == 0
362
+
363
+ raise "Unknown extended peer message id #{id}" if id > peerExtendedMessageList
364
+ peerExtendedMessageMap[id]
365
+ end
366
+ end
367
+
368
+ # An Extended Handshake message. Used to negotiate supported extensions.
369
+ class ExtendedHandshake < Extended
370
+ def initialize
371
+ super()
372
+ @dict = {}
373
+ @extendedMessageId = 0
374
+ end
375
+
376
+ attr_accessor :dict
377
+
378
+ def unserialize(payload)
379
+ super(payload)
380
+ payload = payload[1,payload.length]
381
+ begin
382
+ @dict = payload.bdecode
383
+ rescue
384
+ e = RuntimeError.new("Error bdecoding payload '#{payload}' (payload length = #{payload.length})")
385
+ e.set_backtrace($!.backtrace)
386
+ raise e
387
+ end
388
+ end
389
+
390
+ def serializeTo(io)
391
+ super(io)
392
+ io.write dict.bencode
393
+ end
394
+
395
+ private
396
+ def extendedMsgPayloadLength
397
+ dict.bencode.length
398
+ end
399
+ end
400
+
401
+ # An Extended Metainfo message. Used to request metadata pieces, or provide responses to those requests.
402
+ class ExtendedMetaInfo < Extended
403
+ def initialize
404
+ super()
405
+ @extendedMessageId = 0
406
+ @msgType = nil
407
+ @piece = nil
408
+ @totalSize = nil
409
+ @data = nil
410
+ @dict = {}
411
+ end
412
+
413
+ attr_accessor :dict
414
+ # Message type as a symbol. One of :request, :data, or :reject
415
+ attr_accessor :msgType
416
+ attr_accessor :piece
417
+ # This field is only set if the msgType is :piece
418
+ attr_accessor :totalSize
419
+ # This field is only set if the msgType is :piece. It contains the data for the piece.
420
+ attr_accessor :data
421
+
422
+ def unserialize(payload)
423
+ # Unserialize extended message id
424
+ super(payload)
425
+
426
+ # Rest of the message is a bencoded dictionary.
427
+ # Piece messages of this class are encoded in an interesting way: the bencoded dictionary
428
+ # is concatenated with the arbitrary binary data of the piece at the end. To decode this
429
+ # we need to know the position of when we've finished reading the dictionary. We do this by
430
+ # using a Parser object from the bencode library which maintains a stream object which happens
431
+ # to know the current offset of the parsing.
432
+
433
+ payload = payload[1,payload.length]
434
+ parser = BEncode::Parser.new payload
435
+
436
+ begin
437
+ @dict = parser.parse!
438
+ rescue
439
+ e = RuntimeError.new("Error bdecoding payload '#{payload}' (payload length = #{payload.length})")
440
+ e.set_backtrace($!.backtrace)
441
+ raise e
442
+ end
443
+
444
+ @msgType = @dict['msg_type']
445
+ raise "Extended Metainfo message contained no 'msg_type' key." if ! @msgType
446
+ if @msgType == 0
447
+ @msgType = :request
448
+ elsif @msgType == 1
449
+ @msgType = :piece
450
+ elsif @msgType == 2
451
+ @msgType = :reject
452
+ else
453
+ raise "Unknown message type '#{@msgType}' in Extended Metainfo message"
454
+ end
455
+
456
+ @piece = @dict['piece']
457
+ raise "Extended Metainfo message contained no 'piece' key." if ! @piece
458
+
459
+ @totalSize = @dict['total_size'] if @msgType == :piece
460
+
461
+ # If this is a piece message, read the data after the dictionary.
462
+ @data = parser.stream.read if @msgType == :piece
463
+
464
+ end
465
+
466
+ def serializeTo(io)
467
+ super(io)
468
+ updateDictFromProps
469
+ io.write @dict.bencode
470
+ raise "Extended metainfo piece messages must have piece data. This one's data was nil" if ! @data && dict['msg_type'] == 1
471
+ io.write @data if dict['msg_type'] == 1
472
+ end
473
+
474
+ private
475
+ def extendedMsgPayloadLength
476
+ updateDictFromProps
477
+ len = @dict.bencode.length
478
+ len += @data.length if dict['msg_type'] == 1
479
+ len
480
+ end
481
+
482
+ def updateDictFromProps
483
+ @dict['msg_type'] = msgTypeSymToVal(@msgType) if ! @dict.has_key?('msg_type') && @msgType
484
+ @dict['piece'] = @piece if ! @dict.has_key?('piece')
485
+ @dict['total_size'] = @data.length if @data
486
+ end
487
+
488
+ def msgTypeSymToVal(sym)
489
+ if sym == :request
490
+ 0
491
+ elsif sym == :piece
492
+ 1
493
+ elsif sym == :reject
494
+ 2
495
+ else
496
+ raise "Unknown msg type #{sym}"
497
+ end
498
+ end
499
+ end
500
+
501
+ end
502
+