torrenter 0.0.5 → 0.0.6
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.
- checksums.yaml +4 -4
- data/lib/torrenter.rb +38 -3
- data/lib/torrenter/file_creator.rb +0 -0
- data/lib/torrenter/http_tracker.rb +16 -12
- data/lib/torrenter/message/message_types.rb +1 -1
- data/lib/torrenter/message/messager.rb +108 -171
- data/lib/torrenter/peer.rb +28 -26
- data/lib/torrenter/peer_state.rb +44 -4
- data/lib/torrenter/reactor.rb +158 -92
- data/lib/torrenter/setup.rb +10 -0
- data/lib/torrenter/test_msg.rb +166 -0
- data/lib/torrenter/test_reactor.rb +219 -0
- data/lib/torrenter/torrent_reader.rb +87 -51
- data/lib/torrenter/tracker.rb +13 -0
- data/lib/torrenter/udp_tracker.rb +12 -10
- data/lib/torrenter/version.rb +1 -1
- metadata +7 -2
@@ -0,0 +1,219 @@
|
|
1
|
+
module Torrenter
|
2
|
+
class Reactor < Torrenter::TorrentReader
|
3
|
+
|
4
|
+
attr_accessor :master_index
|
5
|
+
def initialize(peers, stream)
|
6
|
+
super(stream)
|
7
|
+
@peers = peers
|
8
|
+
@master_index = Array.new(sha_hashes.bytesize / 20) { :free }
|
9
|
+
@time_counter = 0
|
10
|
+
@time = Time.now.to_i
|
11
|
+
@byte_counter = 0
|
12
|
+
end
|
13
|
+
|
14
|
+
def check_for_existing_data
|
15
|
+
# if the torrent data file is not present, one will be created
|
16
|
+
IO.write($data_dump, '', 0) unless File.file?($data_dump)
|
17
|
+
|
18
|
+
0.upto(@master_index.size - 1) do |n|
|
19
|
+
# this needs to get changed in order to account for
|
20
|
+
# the last piece not being equal to the piece_length
|
21
|
+
data = IO.read($data_dump, piece_length, n * piece_length) || ''
|
22
|
+
# binding.pry
|
23
|
+
if verified?(n, data)
|
24
|
+
@master_index[n] = :downloaded
|
25
|
+
@byte_counter += data.bytesize
|
26
|
+
end
|
27
|
+
end
|
28
|
+
puts "#{@master_index.count(:downloaded)} pieces downloaded"
|
29
|
+
end
|
30
|
+
|
31
|
+
def verified?(loc, piece)
|
32
|
+
Digest::SHA1.digest(piece) == sha_hashes[loc * 20..(loc * 20) + 19]
|
33
|
+
end
|
34
|
+
|
35
|
+
def finished?
|
36
|
+
@master_index.all? { |index| index == :downloaded }
|
37
|
+
end
|
38
|
+
|
39
|
+
def connect_peers
|
40
|
+
@peers.each { |peer| peer.connect if active_peers.size < 8 }
|
41
|
+
end
|
42
|
+
|
43
|
+
# sends the handshake messages.
|
44
|
+
|
45
|
+
def message_reactor
|
46
|
+
@peers.each { |peer| peer.connect if active_peers.length < 8 }
|
47
|
+
if !finished?
|
48
|
+
peer.state(@master_index) do |loc, buf|
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def piece_indices
|
55
|
+
active_peers.select { |peer| peer.piece_index }
|
56
|
+
.map { |peer| peer.piece_index }
|
57
|
+
end
|
58
|
+
|
59
|
+
def tally
|
60
|
+
@master_index.map.with_index do |p,i|
|
61
|
+
p == :free ? piece_indices.map { |x| x[i] } : []
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def piece_select(peer, limit=1)
|
66
|
+
binding.pry if @master_index.count(:free) == 1
|
67
|
+
puts "#{peer.ip} picking a piece"
|
68
|
+
piece_index = peer.piece_index
|
69
|
+
@tally = tally
|
70
|
+
index = nil
|
71
|
+
|
72
|
+
loop do
|
73
|
+
if limit == active_peers.size + 1
|
74
|
+
index = piece_index.index(:free)
|
75
|
+
break
|
76
|
+
elsif index.is_a?(Integer)
|
77
|
+
break
|
78
|
+
end
|
79
|
+
i = 0
|
80
|
+
loop do
|
81
|
+
break if i > piece_index.size - 1|| !index.nil?
|
82
|
+
if @tally[i].count(:free) == limit && piece_index[i] == :free
|
83
|
+
index = i
|
84
|
+
else
|
85
|
+
i += 1
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
limit += 1 if index.nil?
|
90
|
+
end
|
91
|
+
|
92
|
+
if index
|
93
|
+
@master_index[index] = :downloading
|
94
|
+
peer.request_piece(index)
|
95
|
+
p index
|
96
|
+
return index
|
97
|
+
else
|
98
|
+
binding.pry
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def show_status
|
103
|
+
if (Time.now.to_i - @time) == 1
|
104
|
+
system('clear')
|
105
|
+
puts "#{download_bar} \n Downloading #{$data_dump[/(.+)(?=torrent-data)/]} at #{real} KB/sec with #{pieces_left} pieces left to download"
|
106
|
+
puts "and #{data_remaining} MB remaining ------ #{active_peers.size} connected."
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def remaining
|
111
|
+
total_file_size - @byte_counter
|
112
|
+
end
|
113
|
+
|
114
|
+
def select_index(index)
|
115
|
+
poo = @master_index.map.with_index do |peer,index|
|
116
|
+
active_peers.map { |x| x.piece_index[index] }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def peer_indices
|
121
|
+
active_peers.map { |peer| peer.piece_index }
|
122
|
+
end
|
123
|
+
|
124
|
+
def data_remaining
|
125
|
+
(total_file_size - @byte_counter).fdiv(1024).fdiv(1024).round(2)
|
126
|
+
end
|
127
|
+
|
128
|
+
def pieces_left
|
129
|
+
@master_index.count(:free)
|
130
|
+
end
|
131
|
+
|
132
|
+
def real
|
133
|
+
@time_counter.fdiv(1024).round(1)
|
134
|
+
end
|
135
|
+
|
136
|
+
def reattempt_disconnected_peer
|
137
|
+
disconnected_peers.sample.connect
|
138
|
+
end
|
139
|
+
|
140
|
+
def disconnected_peers
|
141
|
+
@peers.select { |peer| !peer.connected? }
|
142
|
+
end
|
143
|
+
|
144
|
+
def pack_piece(peer)
|
145
|
+
IO.write($data_dump, peer.piece_data, piece_length * peer.index)
|
146
|
+
end
|
147
|
+
|
148
|
+
def indices
|
149
|
+
@master_index.map do |piece|
|
150
|
+
if piece == :free
|
151
|
+
0
|
152
|
+
elsif piece == :downloaded
|
153
|
+
1
|
154
|
+
else
|
155
|
+
get_status @master_index.index(piece)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def get_status(i)
|
161
|
+
peer = @peers.find { |peer| peer.piece_index[i] == :downloading }
|
162
|
+
(peer.buffer.bytesize + peer.piece_size).fdiv(@piece_length)
|
163
|
+
end
|
164
|
+
|
165
|
+
def active_peers
|
166
|
+
@peers.select { |peer| peer.peer_state }
|
167
|
+
end
|
168
|
+
|
169
|
+
def index_percentages
|
170
|
+
active_peers.map do |peer|
|
171
|
+
size = peer.buffer.bytesize + peer.piece_size
|
172
|
+
[peer.index, (size.fdiv @piece_length) * 100]
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def download_bar
|
177
|
+
("\u2588" * pieces(:downloaded)) + ("\u2593" * pieces(:downloading)) + (" " * pieces(:free)) + " %#{pieces(:downloaded)} downloaded "
|
178
|
+
end
|
179
|
+
|
180
|
+
def pieces(type)
|
181
|
+
(@master_index.count(type).fdiv(@master_index.size) * 100).round
|
182
|
+
end
|
183
|
+
|
184
|
+
def seperate_data_dump_into_files
|
185
|
+
folder = $data_dump[/.+(?=\.torrent-data)/]
|
186
|
+
|
187
|
+
if multiple_files?
|
188
|
+
offset = 0
|
189
|
+
|
190
|
+
file_list.each do |file|
|
191
|
+
|
192
|
+
length = file['length']
|
193
|
+
filename = file['path'].pop
|
194
|
+
|
195
|
+
if multiple_sub_folders?(file)
|
196
|
+
subfolders = file['path'].join("/")
|
197
|
+
folder = folder + "/" + subfolders
|
198
|
+
FileUtils.mkdir_p("#{folder}", force: true)
|
199
|
+
end
|
200
|
+
|
201
|
+
File.open("#{folder}/#{filename}", 'a+') { |data| data << IO.read($data_dump, length, offset) }
|
202
|
+
offset += length
|
203
|
+
end
|
204
|
+
else
|
205
|
+
FileUtils.mkdir("#{folder}")
|
206
|
+
File.open("#{folder}/#{file_list['name']}", 'w') { |data| data << File.read($data_dump) }
|
207
|
+
end
|
208
|
+
File.delete($data_dump)
|
209
|
+
end
|
210
|
+
|
211
|
+
def multiple_sub_folders?(file)
|
212
|
+
file['path'].size > 1
|
213
|
+
end
|
214
|
+
|
215
|
+
def multiple_files?
|
216
|
+
file_list.is_a?(Array)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
@@ -1,74 +1,110 @@
|
|
1
1
|
module Torrenter
|
2
|
+
# torrent reader should only read the torrent file and return either
|
3
|
+
# a UDP tracker object or an HTTP tracker object
|
2
4
|
class TorrentReader
|
3
5
|
attr_reader :stream
|
4
6
|
def initialize(stream)
|
5
|
-
@stream
|
6
|
-
|
7
|
-
@sha = Digest::SHA1.digest(stream['info'].bencode)
|
8
|
-
@piece_length = stream['info']['piece length']
|
9
|
-
@sha_list = stream['info']['pieces']
|
10
|
-
@sha_size = stream['info']['pieces'].size / 20
|
11
|
-
end
|
12
|
-
|
13
|
-
def determine_protocol
|
14
|
-
trackers = if @stream['announce-list']
|
15
|
-
@stream['announce-list'].flatten << @stream['announce']
|
16
|
-
else
|
17
|
-
[@stream['announce']]
|
18
|
-
end
|
19
|
-
|
20
|
-
trackers.each do |track|
|
21
|
-
tracker = build_tracker(track)
|
22
|
-
|
23
|
-
tracker.connect
|
24
|
-
|
25
|
-
if tracker.connected?
|
26
|
-
@peers = tracker.address_list
|
27
|
-
end
|
7
|
+
@stream = stream
|
8
|
+
end
|
28
9
|
|
29
|
-
|
30
|
-
|
10
|
+
def info_hash
|
11
|
+
Digest::SHA1.digest(@stream['info'].bencode)
|
12
|
+
end
|
13
|
+
|
14
|
+
def sha_hashes
|
15
|
+
@stream['info']['pieces']
|
16
|
+
end
|
17
|
+
|
18
|
+
def total_file_size
|
19
|
+
file_list.is_a?(Array) ? multiple_file_size : file_list['length']
|
20
|
+
end
|
21
|
+
|
22
|
+
def multiple_file_size
|
23
|
+
file_list.map { |file| file['length'] }.inject { |x,y| x + y }
|
24
|
+
end
|
25
|
+
|
26
|
+
def piece_length
|
27
|
+
@stream['info']['piece length']
|
28
|
+
end
|
29
|
+
|
30
|
+
def file_list
|
31
|
+
@stream['info']['files'] || @stream['info']
|
32
|
+
end
|
33
|
+
|
34
|
+
def announce_url
|
35
|
+
@stream['announce']
|
36
|
+
end
|
37
|
+
|
38
|
+
def announce_list
|
39
|
+
@stream['announce-list']
|
40
|
+
end
|
41
|
+
|
42
|
+
def url_list
|
43
|
+
announce_list ? announce_list.flatten << announce_url : [announce_url]
|
44
|
+
end
|
31
45
|
|
32
|
-
|
33
|
-
|
46
|
+
def access_trackers
|
47
|
+
url_list.map do |track|
|
48
|
+
if track.include?("http://")
|
49
|
+
HTTPTracker.new(track, @stream)
|
50
|
+
else
|
51
|
+
UDPTracker.new(track, @stream)
|
52
|
+
end
|
34
53
|
end
|
35
54
|
end
|
36
55
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
}
|
56
|
+
def peer_list(bytestring)
|
57
|
+
bytestring.chars.each_slice(6).map do |peer_data|
|
58
|
+
ip = peer_data[0..3].join('').bytes.join('.')
|
59
|
+
port = peer_data[4..5].join('').unpack("S>").first
|
60
|
+
Peer.new(ip, port, info_hash, piece_length)
|
61
|
+
end
|
44
62
|
end
|
45
63
|
|
46
|
-
def
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
64
|
+
def unpack_data
|
65
|
+
puts "ALL FINISHED! Transferring data into file(s)."
|
66
|
+
@main_folder = $data_dump[/.+(?=\.torrent-data)/]
|
67
|
+
create_folders
|
68
|
+
|
69
|
+
if multiple_files?
|
70
|
+
offset = 0
|
71
|
+
file_list.each do |file|
|
72
|
+
|
73
|
+
length = file['length']
|
74
|
+
filename = file['path'].join("/")
|
75
|
+
|
76
|
+
File.open("#{@main_folder}/#{filename}", 'a+') do |data|
|
77
|
+
data << IO.read($data_dump, length, offset)
|
78
|
+
end
|
79
|
+
|
80
|
+
offset += length
|
81
|
+
end
|
82
|
+
else
|
83
|
+
FileUtils.mkdir(@main_folder)
|
84
|
+
File.open("#{@main_folder}/#{file_list['name']}", 'w') { |data| data << File.read($data_dump) }
|
51
85
|
end
|
86
|
+
File.delete($data_dump)
|
52
87
|
end
|
53
88
|
|
54
|
-
def
|
55
|
-
|
89
|
+
def create_folders
|
90
|
+
if multiple_files?
|
91
|
+
folders = sub_folders.map { |fold| fold['path'][0..-2].join("/") }.uniq
|
92
|
+
folders.each { |folder| FileUtils.mkdir_p(@main_folder + "/#{folder}") }
|
93
|
+
else
|
94
|
+
FileUtils.mkdir(@main_folder)
|
95
|
+
end
|
56
96
|
end
|
57
97
|
|
58
|
-
def
|
59
|
-
|
98
|
+
def sub_folders
|
99
|
+
stream['info']['files'].select { |fold| fold['path'].length > 1 }
|
60
100
|
end
|
61
101
|
|
62
|
-
def
|
63
|
-
|
64
|
-
Array.new(@sha_size) { start += 20; term += 20; @sha_list[start...term]}
|
102
|
+
def multiple_sub_folders?(file)
|
103
|
+
file['path'].length > 0
|
65
104
|
end
|
66
105
|
|
67
|
-
def
|
68
|
-
|
69
|
-
react.message_reactor
|
106
|
+
def multiple_files?
|
107
|
+
file_list.is_a?(Array)
|
70
108
|
end
|
71
109
|
end
|
72
110
|
end
|
73
|
-
|
74
|
-
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Torrenter
|
2
|
+
class Tracker < TorrentReader
|
3
|
+
def initialize(tracker_url)
|
4
|
+
@tracker_url = tracker_url
|
5
|
+
end
|
6
|
+
|
7
|
+
def tracker_connect
|
8
|
+
if @tracker_url.include?("http://")
|
9
|
+
@tracker_url = URI(@tracker_url)
|
10
|
+
@tracker_url.query = URI.encode_www_form(peer_hash)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -1,19 +1,20 @@
|
|
1
1
|
module Torrenter
|
2
2
|
class UDPTracker < TorrentReader
|
3
|
+
|
3
4
|
attr_reader :socket, :response
|
4
5
|
|
5
6
|
def initialize(tracker, stream)
|
7
|
+
super(stream)
|
6
8
|
@url = tracker[/(?<=udp\:\/\/).+(?=\:\d+)/]
|
7
9
|
@port = tracker[/\d+$/].to_i
|
8
10
|
@socket = UDPSocket.new
|
9
11
|
@connection_id = [0x41727101980].pack("Q>")
|
10
|
-
super(stream)
|
11
12
|
end
|
12
13
|
|
13
14
|
def connect
|
15
|
+
@transaction_id = [rand(1000)].pack("I>")
|
16
|
+
@socket.connect(ip_addr, @port)
|
14
17
|
begin
|
15
|
-
@transaction_id = [rand(1000)].pack("I>")
|
16
|
-
@socket.connect(ip_addr, @port)
|
17
18
|
send_msg(connect_msg)
|
18
19
|
read_response
|
19
20
|
rescue
|
@@ -37,15 +38,16 @@ module Torrenter
|
|
37
38
|
Socket.getaddrinfo(@url, @port)[0][3]
|
38
39
|
end
|
39
40
|
|
40
|
-
def
|
41
|
+
def bound_peers
|
41
42
|
@connection_id = @response[-8..-1]
|
42
43
|
@transaction_id = [rand(10000)].pack("I>")
|
43
44
|
send_msg(announce_msg)
|
44
45
|
|
45
46
|
read_response
|
46
|
-
|
47
|
+
|
47
48
|
parse_announce if @response[0..3] == action(1)
|
48
|
-
|
49
|
+
|
50
|
+
peer_list(@response)
|
49
51
|
end
|
50
52
|
|
51
53
|
def parse_announce
|
@@ -76,7 +78,7 @@ module Torrenter
|
|
76
78
|
end
|
77
79
|
|
78
80
|
def announce_input
|
79
|
-
@connection_id + action(1) + @transaction_id +
|
81
|
+
@connection_id + action(1) + @transaction_id + info_hash + PEER_ID
|
80
82
|
end
|
81
83
|
|
82
84
|
def connect_msg
|
@@ -86,9 +88,9 @@ module Torrenter
|
|
86
88
|
def action(n)
|
87
89
|
[n].pack("I>")
|
88
90
|
end
|
89
|
-
|
91
|
+
|
90
92
|
def announce_msg
|
91
|
-
@connection_id + action(1) + @transaction_id +
|
93
|
+
@connection_id + action(1) + @transaction_id + info_hash + PEER_ID + [0].pack("Q>") + [0].pack("Q>") + [0].pack("Q>") + action(0) + action(0) + action(0) + action(-1) + [@socket.addr[1]].pack(">S")
|
92
94
|
end
|
93
95
|
end
|
94
|
-
end
|
96
|
+
end
|