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,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