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,124 @@
|
|
1
|
+
class Array
|
2
|
+
def binsearch(low = nil, high = nil)
|
3
|
+
return nil if length == 0
|
4
|
+
result = binsearch_index(low, high){ |x| yield x if !x.nil?}
|
5
|
+
result = at(result) if result
|
6
|
+
result
|
7
|
+
end
|
8
|
+
|
9
|
+
def binsearch_index(low = nil, high = nil)
|
10
|
+
return nil if length == 0
|
11
|
+
low = 0 if !low
|
12
|
+
high = length if !high
|
13
|
+
|
14
|
+
if low == high
|
15
|
+
if yield at(low)
|
16
|
+
return low
|
17
|
+
else
|
18
|
+
return nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
mid = (high-low)/2 + low
|
23
|
+
if yield at(mid)
|
24
|
+
# this value >= target.
|
25
|
+
result = binsearch_index(low, mid == low ? mid : mid-1){ |x| yield x if !x.nil?}
|
26
|
+
if result
|
27
|
+
return result
|
28
|
+
else
|
29
|
+
return mid
|
30
|
+
end
|
31
|
+
else
|
32
|
+
# this value < target
|
33
|
+
binsearch_index(mid == high ? mid : mid+1, high){ |x| yield x if !x.nil?}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
module QuartzTorrent
|
39
|
+
# This class is used to map consecutive integer regions to objects. The lowest end of the
|
40
|
+
# lowest region is assumed to be 0.
|
41
|
+
class RegionMap
|
42
|
+
def initialize
|
43
|
+
@map = []
|
44
|
+
@sorted = false
|
45
|
+
end
|
46
|
+
|
47
|
+
# Add a region that ends at the specified 'regionEnd' with the associated 'obj'
|
48
|
+
def add(regionEnd, obj)
|
49
|
+
@map.push [regionEnd, obj]
|
50
|
+
@sorted = false
|
51
|
+
end
|
52
|
+
|
53
|
+
# Given an integer value, find which region it falls in and return the object associated with that region.
|
54
|
+
def findValue(value)
|
55
|
+
if ! @sorted
|
56
|
+
@map.sort{ |a,b| a[0] <=> b[0] }
|
57
|
+
@sorted = true
|
58
|
+
end
|
59
|
+
|
60
|
+
@map.binsearch{|x| x[0] >= value}[1]
|
61
|
+
end
|
62
|
+
|
63
|
+
def findIndex(value)
|
64
|
+
if ! @sorted
|
65
|
+
@map.sort{ |a,b| a[0] <=> b[0] }
|
66
|
+
@sorted = true
|
67
|
+
end
|
68
|
+
|
69
|
+
@map.binsearch_index{|x| x[0] >= value}
|
70
|
+
end
|
71
|
+
|
72
|
+
# Given a value, return a list of the form [index, value, left, right, offset] where
|
73
|
+
# index is the zero-based index in this map of the region, value is the associated object,
|
74
|
+
# left is the lowest value in the region, right is the highest, and offset is the
|
75
|
+
# offset within the region of the value.
|
76
|
+
def find(value)
|
77
|
+
|
78
|
+
index = findIndex(value)
|
79
|
+
return nil if ! index
|
80
|
+
result = at(index)
|
81
|
+
|
82
|
+
if index == 0
|
83
|
+
offset = value
|
84
|
+
else
|
85
|
+
offset = value - result[1]
|
86
|
+
end
|
87
|
+
|
88
|
+
[index, result[0], result[1], result[2], offset]
|
89
|
+
end
|
90
|
+
|
91
|
+
# For the region with index i, return an array of the form [value, left, right]
|
92
|
+
def at(i)
|
93
|
+
return nil if @map.length == 0
|
94
|
+
if i == 0
|
95
|
+
left = 0
|
96
|
+
else
|
97
|
+
left = @map[i-1][0]+1
|
98
|
+
end
|
99
|
+
|
100
|
+
[@map[i][1],left,@map[i][0]]
|
101
|
+
end
|
102
|
+
|
103
|
+
def [](i)
|
104
|
+
at(i)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Return the rightmost index of the final region, or -1 if no regions have been added.
|
108
|
+
def maxIndex
|
109
|
+
@map.size-1
|
110
|
+
end
|
111
|
+
|
112
|
+
# For the final region, return an array of the form [index, value, left, right]
|
113
|
+
def last
|
114
|
+
return nil if ! @map.last
|
115
|
+
if @map.length == 1
|
116
|
+
left = 0
|
117
|
+
else
|
118
|
+
left = @map[@map.length-2][0]+1
|
119
|
+
end
|
120
|
+
[@map.length-1,@map.last[1],left,@map.last[0]]
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
|
2
|
+
# Implements a counting semaphore.
|
3
|
+
class Semaphore
|
4
|
+
# Create a new semaphore initialized to the specified count.
|
5
|
+
def initialize(count = 0)
|
6
|
+
@mutex = Mutex.new
|
7
|
+
@count = count
|
8
|
+
@sleeping = []
|
9
|
+
end
|
10
|
+
|
11
|
+
# Wait on the semaphore. If the count zero or below, the calling thread blocks.
|
12
|
+
def wait
|
13
|
+
c = nil
|
14
|
+
@mutex.synchronize do
|
15
|
+
@count -= 1
|
16
|
+
c = @count
|
17
|
+
end
|
18
|
+
|
19
|
+
if c < 0
|
20
|
+
@mutex.synchronize do
|
21
|
+
@sleeping.push Thread.current
|
22
|
+
end
|
23
|
+
Thread.stop
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Signal the semaphore. If the count is below zero the waiting threads are woken.
|
28
|
+
def signal
|
29
|
+
c = nil
|
30
|
+
@mutex.synchronize do
|
31
|
+
c = @count
|
32
|
+
@count += 1
|
33
|
+
end
|
34
|
+
if c < 0
|
35
|
+
t = nil
|
36
|
+
@mutex.synchronize do
|
37
|
+
t = @sleeping.shift
|
38
|
+
end
|
39
|
+
t.wakeup if t
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,271 @@
|
|
1
|
+
require "quartz_torrent/log"
|
2
|
+
require "quartz_torrent/metainfo"
|
3
|
+
require "quartz_torrent/udptrackermsg"
|
4
|
+
require "quartz_torrent/httptrackerclient"
|
5
|
+
require "quartz_torrent/udptrackerclient"
|
6
|
+
require "quartz_torrent/interruptiblesleep"
|
7
|
+
require "quartz_torrent/util"
|
8
|
+
require "net/http"
|
9
|
+
require "cgi"
|
10
|
+
require "thread"
|
11
|
+
# http://xbtt.sourceforge.net/udp_tracker_protocol.html
|
12
|
+
# https://wiki.theory.org/BitTorrentSpecification
|
13
|
+
# http://stackoverflow.com/questions/9669152/get-https-response
|
14
|
+
|
15
|
+
module QuartzTorrent
|
16
|
+
|
17
|
+
# Represents a peer returned by the tracker
|
18
|
+
class TrackerPeer
|
19
|
+
def initialize(ip, port, id = nil)
|
20
|
+
if ip =~ /(\d+).(\d+).(\d+).(\d+)/
|
21
|
+
@ip = ip
|
22
|
+
@port = port
|
23
|
+
@id = id
|
24
|
+
|
25
|
+
@hash = $1.to_i << 24 +
|
26
|
+
$2.to_i << 16 +
|
27
|
+
$3.to_i << 8 +
|
28
|
+
$4.to_i +
|
29
|
+
port << 32
|
30
|
+
|
31
|
+
@displayId = nil
|
32
|
+
@displayId = id.gsub(/[\x80-\xff]/,'?') if id
|
33
|
+
else
|
34
|
+
raise "Invalid IP address #{ip}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def hash
|
39
|
+
@hash
|
40
|
+
end
|
41
|
+
|
42
|
+
def eql?(o)
|
43
|
+
o.ip == @ip && o.port == @port
|
44
|
+
end
|
45
|
+
|
46
|
+
# Ip address, a string in dotted-quad notation
|
47
|
+
attr_accessor :ip
|
48
|
+
attr_accessor :port
|
49
|
+
attr_accessor :id
|
50
|
+
|
51
|
+
def to_s
|
52
|
+
"#{@displayId ? "["+@displayId+"] " : ""}#{ip}:#{port}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Dynamic parameters needed when making a request to the tracker.
|
57
|
+
class TrackerDynamicRequestParams
|
58
|
+
def initialize(dataLength = nil)
|
59
|
+
@uploaded = 0
|
60
|
+
@downloaded = 0
|
61
|
+
if dataLength
|
62
|
+
@left = dataLength.to_i
|
63
|
+
else
|
64
|
+
@left = 0
|
65
|
+
end
|
66
|
+
end
|
67
|
+
attr_accessor :uploaded
|
68
|
+
attr_accessor :downloaded
|
69
|
+
attr_accessor :left
|
70
|
+
end
|
71
|
+
|
72
|
+
# Represents the response from a tracker request
|
73
|
+
class TrackerResponse
|
74
|
+
def initialize(success, error, peers)
|
75
|
+
@success = success
|
76
|
+
@error = error
|
77
|
+
@peers = peers
|
78
|
+
@interval = nil
|
79
|
+
end
|
80
|
+
|
81
|
+
# The error message if this was not successful
|
82
|
+
attr_reader :error
|
83
|
+
|
84
|
+
# The list of peers from the response if the request was a success.
|
85
|
+
attr_reader :peers
|
86
|
+
|
87
|
+
# Refresh interval in seconds
|
88
|
+
attr_accessor :interval
|
89
|
+
|
90
|
+
# Returns true if the Tracker response was a success
|
91
|
+
def successful?
|
92
|
+
@success
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# This class represents a connection to a tracker for a specific torrent. It can be used to get
|
97
|
+
# peers for that torrent.
|
98
|
+
class TrackerClient
|
99
|
+
include QuartzTorrent
|
100
|
+
|
101
|
+
def initialize(announceUrl, infoHash, dataLength = 0, maxErrors = 20)
|
102
|
+
@peerId = "-QR0001-" # Azureus style
|
103
|
+
@peerId << Process.pid.to_s
|
104
|
+
@peerId = @peerId + "x" * (20-@peerId.length)
|
105
|
+
@stopped = false
|
106
|
+
@started = false
|
107
|
+
@peers = {}
|
108
|
+
@port = 6881
|
109
|
+
@peersMutex = Mutex.new
|
110
|
+
@errors = []
|
111
|
+
@maxErrors = @errors
|
112
|
+
@sleeper = InterruptibleSleep.new
|
113
|
+
# Event to send on the next update
|
114
|
+
@event = :started
|
115
|
+
@worker = nil
|
116
|
+
@logger = LogManager.getLogger("tracker_client")
|
117
|
+
@announceUrl = announceUrl
|
118
|
+
@infoHash = infoHash
|
119
|
+
@peersChangedListeners = []
|
120
|
+
@dynamicRequestParamsBuilder = Proc.new{ TrackerDynamicRequestParams.new(dataLength) }
|
121
|
+
end
|
122
|
+
|
123
|
+
attr_reader :peerId
|
124
|
+
attr_accessor :port
|
125
|
+
# This should be set to a Proc that when called will return a TrackerDynamicRequestParams object
|
126
|
+
# with up-to-date information.
|
127
|
+
attr_accessor :dynamicRequestParamsBuilder
|
128
|
+
|
129
|
+
def started?
|
130
|
+
@started
|
131
|
+
end
|
132
|
+
|
133
|
+
def peers
|
134
|
+
result = nil
|
135
|
+
@peersMutex.synchronize do
|
136
|
+
result = @peers.keys
|
137
|
+
end
|
138
|
+
result
|
139
|
+
end
|
140
|
+
|
141
|
+
# Add a listener that gets notified when the peers list has changed.
|
142
|
+
# This listener is called from another thread so be sure to synchronize
|
143
|
+
# if necessary. The passed listener should be a proc that takes no arguments.
|
144
|
+
def addPeersChangedListener(listener)
|
145
|
+
@peersChangedListeners.push listener
|
146
|
+
end
|
147
|
+
def removePeersChangedListener(listener)
|
148
|
+
@peersChangedListeners.delete listener
|
149
|
+
end
|
150
|
+
|
151
|
+
# Get the last N errors reported
|
152
|
+
def errors
|
153
|
+
@errors
|
154
|
+
end
|
155
|
+
|
156
|
+
# Create a new TrackerClient using the passed information.
|
157
|
+
def self.create(announceUrl, infoHash, dataLength = 0, start = true)
|
158
|
+
result = nil
|
159
|
+
if announceUrl =~ /udp:\/\//
|
160
|
+
result = UdpTrackerClient.new(announceUrl, infoHash, dataLength)
|
161
|
+
else
|
162
|
+
result = HttpTrackerClient.new(announceUrl, infoHash, dataLength)
|
163
|
+
end
|
164
|
+
result.start if start
|
165
|
+
|
166
|
+
result
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.createFromMetainfo(metainfo, start = true)
|
170
|
+
create(metainfo.announce, metainfo.infoHash, metainfo.info.dataLength, start)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Start the worker thread
|
174
|
+
def start
|
175
|
+
@stopped = false
|
176
|
+
return if @started
|
177
|
+
@started = true
|
178
|
+
@worker = Thread.new do
|
179
|
+
QuartzTorrent.initThread("trackerclient")
|
180
|
+
@logger.info "Worker thread starting"
|
181
|
+
@event = :started
|
182
|
+
trackerInterval = nil
|
183
|
+
while ! @stopped
|
184
|
+
begin
|
185
|
+
response = nil
|
186
|
+
begin
|
187
|
+
@logger.debug "Sending request"
|
188
|
+
response = request(@event)
|
189
|
+
@event = nil
|
190
|
+
trackerInterval = response.interval
|
191
|
+
rescue
|
192
|
+
addError $!
|
193
|
+
@logger.debug "Request failed due to exception: #{$!}"
|
194
|
+
@logger.debug $!.backtrace.join("\n")
|
195
|
+
end
|
196
|
+
|
197
|
+
if response && response.successful?
|
198
|
+
# Replace the list of peers
|
199
|
+
peersHash = {}
|
200
|
+
@logger.info "Response contained #{response.peers.length} peers"
|
201
|
+
response.peers.each do |p|
|
202
|
+
peersHash[p] = 1
|
203
|
+
end
|
204
|
+
@peersMutex.synchronize do
|
205
|
+
@peers = peersHash
|
206
|
+
end
|
207
|
+
if @peersChangedListeners.size > 0
|
208
|
+
@peersChangedListeners.each{ |l| l.call }
|
209
|
+
end
|
210
|
+
else
|
211
|
+
@logger.debug "Response was unsuccessful from tracker"
|
212
|
+
addError response.error if response
|
213
|
+
end
|
214
|
+
|
215
|
+
# If we have no interval from the tracker yet, and the last request didn't error out leaving us with no peers,
|
216
|
+
# then set the interval to 20 seconds.
|
217
|
+
interval = trackerInterval
|
218
|
+
interval = 20 if ! interval
|
219
|
+
interval = 2 if response && !response.successful? && @peers.length == 0
|
220
|
+
|
221
|
+
@logger.debug "Sleeping for #{interval} seconds"
|
222
|
+
@sleeper.sleep interval
|
223
|
+
|
224
|
+
rescue
|
225
|
+
@logger.warn "Unhandled exception in worker thread: #{$!}"
|
226
|
+
@logger.warn $!.backtrace.join("\n")
|
227
|
+
end
|
228
|
+
end
|
229
|
+
@logger.info "Worker thread shutting down"
|
230
|
+
@logger.info "Sending final update to tracker"
|
231
|
+
begin
|
232
|
+
request(:stopped)
|
233
|
+
rescue
|
234
|
+
addError $!
|
235
|
+
@logger.debug "Request failed due to exception: #{$!}"
|
236
|
+
@logger.debug $!.backtrace.join("\n")
|
237
|
+
end
|
238
|
+
@started = false
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
# Stop the worker thread
|
243
|
+
def stop
|
244
|
+
@stopped = true
|
245
|
+
@sleeper.wake
|
246
|
+
if @worker
|
247
|
+
@logger.info "Stop called. Waiting for worker"
|
248
|
+
@logger.info "Worker wait timed out after 2 seconds. Shutting down anyway" if ! @worker.join(2)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# Notify the tracker that we have finished downloading all pieces.
|
253
|
+
def completed
|
254
|
+
@event = :completed
|
255
|
+
@sleeper.wake
|
256
|
+
end
|
257
|
+
|
258
|
+
# Add an error to the error list
|
259
|
+
def addError(e)
|
260
|
+
@errors.pop if @errors.length == @maxErrors
|
261
|
+
@errors.push e
|
262
|
+
end
|
263
|
+
|
264
|
+
private
|
265
|
+
def eventValid?(event)
|
266
|
+
event == :started || event == :stopped || event == :completed
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
|
271
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module QuartzTorrent
|
2
|
+
# A tracker client that uses the UDP protocol as defined by http://xbtt.sourceforge.net/udp_tracker_protocol.html
|
3
|
+
class UdpTrackerClient < TrackerClient
|
4
|
+
# Set UDP receive length to a value that allows up to 100 peers to be returned in an announce.
|
5
|
+
ReceiveLength = 620
|
6
|
+
def initialize(announceUrl, infoHash, dataLength)
|
7
|
+
super(announceUrl, infoHash, dataLength)
|
8
|
+
@logger = LogManager.getLogger("udp_tracker_client")
|
9
|
+
if @announceUrl =~ /udp:\/\/([^:]+):(\d+)/
|
10
|
+
@host = $1
|
11
|
+
@trackerPort = $2
|
12
|
+
else
|
13
|
+
throw "UDP Tracker announce URL is invalid: #{announceUrl}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def request(event = nil)
|
18
|
+
if event == :started
|
19
|
+
event = UdpTrackerMessage::EventStarted
|
20
|
+
elsif event == :stopped
|
21
|
+
event = UdpTrackerMessage::EventStopped
|
22
|
+
elsif event == :completed
|
23
|
+
event = UdpTrackerMessage::EventCompleted
|
24
|
+
else
|
25
|
+
event = UdpTrackerMessage::EventNone
|
26
|
+
end
|
27
|
+
|
28
|
+
socket = UDPSocket.new
|
29
|
+
socket.connect @host, @trackerPort
|
30
|
+
|
31
|
+
@logger.debug "Sending UDP tracker request to #{@host}:#{@trackerPort}"
|
32
|
+
|
33
|
+
# Send connect request
|
34
|
+
req = UdpTrackerConnectRequest.new
|
35
|
+
socket.send req.serialize, 0
|
36
|
+
resp = UdpTrackerConnectResponse.unserialize(socket.recvfrom(ReceiveLength)[0])
|
37
|
+
raise "Invalid connect response: response transaction id is different from the request transaction id" if resp.transactionId != req.transactionId
|
38
|
+
connectionId = resp.connectionId
|
39
|
+
|
40
|
+
dynamicParams = @dynamicRequestParamsBuilder.call
|
41
|
+
|
42
|
+
# Send announce request
|
43
|
+
req = UdpTrackerAnnounceRequest.new(connectionId)
|
44
|
+
req.peerId = @peerId
|
45
|
+
req.infoHash = @infoHash
|
46
|
+
req.downloaded = dynamicParams.downloaded
|
47
|
+
req.left = dynamicParams.left
|
48
|
+
req.uploaded = dynamicParams.uploaded
|
49
|
+
req.event = event
|
50
|
+
#req.port = socket.addr[1]
|
51
|
+
req.port = @port
|
52
|
+
socket.send req.serialize, 0
|
53
|
+
resp = UdpTrackerAnnounceResponse.unserialize(socket.recvfrom(ReceiveLength)[0])
|
54
|
+
socket.close
|
55
|
+
|
56
|
+
peers = []
|
57
|
+
resp.ips.length.times do |i|
|
58
|
+
ip = resp.ips[i].unpack("CCCC").join('.')
|
59
|
+
port = resp.ports[i].unpack("n").first
|
60
|
+
peers.push TrackerPeer.new ip, port
|
61
|
+
end
|
62
|
+
peers
|
63
|
+
|
64
|
+
result = TrackerResponse.new(true, nil, peers)
|
65
|
+
result.interval = resp.interval if resp.interval
|
66
|
+
result
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|