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.
- data/bin/quartztorrent_download +127 -0
- data/bin/quartztorrent_download_curses +841 -0
- data/bin/quartztorrent_magnet_from_torrent +32 -0
- data/bin/quartztorrent_show_info +62 -0
- data/lib/quartz_torrent.rb +2 -0
- data/lib/quartz_torrent/bitfield.rb +314 -0
- data/lib/quartz_torrent/blockstate.rb +354 -0
- data/lib/quartz_torrent/classifiedpeers.rb +95 -0
- data/lib/quartz_torrent/extension.rb +37 -0
- data/lib/quartz_torrent/filemanager.rb +543 -0
- data/lib/quartz_torrent/formatter.rb +92 -0
- data/lib/quartz_torrent/httptrackerclient.rb +121 -0
- data/lib/quartz_torrent/interruptiblesleep.rb +27 -0
- data/lib/quartz_torrent/log.rb +132 -0
- data/lib/quartz_torrent/magnet.rb +92 -0
- data/lib/quartz_torrent/memprofiler.rb +27 -0
- data/lib/quartz_torrent/metainfo.rb +221 -0
- data/lib/quartz_torrent/metainfopiecestate.rb +265 -0
- data/lib/quartz_torrent/peer.rb +145 -0
- data/lib/quartz_torrent/peerclient.rb +1627 -0
- data/lib/quartz_torrent/peerholder.rb +123 -0
- data/lib/quartz_torrent/peermanager.rb +170 -0
- data/lib/quartz_torrent/peermsg.rb +502 -0
- data/lib/quartz_torrent/peermsgserialization.rb +102 -0
- data/lib/quartz_torrent/piecemanagerrequestmetadata.rb +12 -0
- data/lib/quartz_torrent/rate.rb +58 -0
- data/lib/quartz_torrent/ratelimit.rb +48 -0
- data/lib/quartz_torrent/reactor.rb +949 -0
- data/lib/quartz_torrent/regionmap.rb +124 -0
- data/lib/quartz_torrent/semaphore.rb +43 -0
- data/lib/quartz_torrent/trackerclient.rb +271 -0
- data/lib/quartz_torrent/udptrackerclient.rb +70 -0
- data/lib/quartz_torrent/udptrackermsg.rb +250 -0
- data/lib/quartz_torrent/util.rb +100 -0
- 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
|
+
|