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
data/lib/torrenter/peer.rb
CHANGED
@@ -2,34 +2,32 @@ module Torrenter
|
|
2
2
|
class Peer
|
3
3
|
include Torrenter
|
4
4
|
|
5
|
-
attr_reader :
|
6
|
-
attr_accessor :piece_index, :
|
7
|
-
|
8
|
-
def initialize(
|
9
|
-
@
|
10
|
-
@
|
11
|
-
@
|
12
|
-
@
|
13
|
-
@
|
14
|
-
@
|
15
|
-
@
|
16
|
-
@
|
17
|
-
@file_list = file_list
|
18
|
-
@status = false
|
5
|
+
attr_reader :status, :peer_state, :info_hash, :piece_length, :blocks, :buffer, :ip
|
6
|
+
attr_accessor :piece_index, :remaining, :index
|
7
|
+
|
8
|
+
def initialize(ip, port, info_hash, piece_length)
|
9
|
+
@ip = ip
|
10
|
+
@port = port
|
11
|
+
@info_hash = info_hash
|
12
|
+
@piece_length = piece_length
|
13
|
+
@buffer = ''
|
14
|
+
@blocks = []
|
15
|
+
@dl_rate = 0
|
16
|
+
@piece_index = []
|
19
17
|
end
|
20
18
|
|
21
|
-
def
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
19
|
+
def current_size
|
20
|
+
piece_data.bytesize + @buffer.bytesize
|
21
|
+
end
|
22
|
+
|
23
|
+
def piece_data
|
24
|
+
@blocks.join('')
|
27
25
|
end
|
28
26
|
|
29
27
|
def connect
|
30
|
-
puts "\nConnecting to IP: #{
|
28
|
+
puts "\nConnecting to IP: #{@ip} PORT: #{@port}"
|
31
29
|
begin
|
32
|
-
Timeout::timeout(1) { @socket = TCPSocket.new(
|
30
|
+
Timeout::timeout(1) { @socket = TCPSocket.new(@ip, @port) }
|
33
31
|
rescue Timeout::Error
|
34
32
|
puts "Timed out."
|
35
33
|
rescue Errno::EADDRNOTAVAIL
|
@@ -43,9 +41,9 @@ module Torrenter
|
|
43
41
|
if @socket
|
44
42
|
puts "Connected!"
|
45
43
|
@socket.write(handshake)
|
46
|
-
@
|
44
|
+
@peer_state = true
|
47
45
|
else
|
48
|
-
@
|
46
|
+
@peer_state = false
|
49
47
|
end
|
50
48
|
end
|
51
49
|
|
@@ -53,8 +51,12 @@ module Torrenter
|
|
53
51
|
"#{PROTOCOL}#{@info_hash}#{PEER_ID}"
|
54
52
|
end
|
55
53
|
|
56
|
-
def
|
57
|
-
@
|
54
|
+
def connected?
|
55
|
+
@peer_state
|
56
|
+
end
|
57
|
+
|
58
|
+
def update_indices(master)
|
59
|
+
master.each_with_index { |p,i| @piece_index[i] = p }
|
58
60
|
end
|
59
61
|
end
|
60
62
|
end
|
data/lib/torrenter/peer_state.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
module Torrenter
|
2
|
-
|
3
2
|
# The buffer state should be responsible ONLY for the handling of non-metadata bytes.
|
4
3
|
# the messaging behavior should and ought to remain with the peer class, though
|
5
4
|
# in truth, it may be better if I didnt do that.
|
@@ -9,9 +8,50 @@ module Torrenter
|
|
9
8
|
|
10
9
|
# instead of initialiazing it with the buffer,
|
11
10
|
|
12
|
-
class BufferState
|
13
|
-
|
14
|
-
|
11
|
+
class BufferState
|
12
|
+
|
13
|
+
def intialize(socket)
|
14
|
+
@socket = socket
|
15
|
+
@buffer = ''
|
16
|
+
end
|
17
|
+
|
18
|
+
# include Torrenter
|
19
|
+
|
20
|
+
def state(buffer, master_index)
|
21
|
+
buffer_id = buffer[4]
|
22
|
+
if buffer_id.nil?
|
23
|
+
buffer.slice!(0..3)
|
24
|
+
else
|
25
|
+
@length = buffer[0..3].unpack("N>")
|
26
|
+
case buffer_id
|
27
|
+
when BITFIELD then process_bitfield
|
28
|
+
when HAVE then process_have
|
29
|
+
when INTERESTED then process_interested
|
30
|
+
when PIECE then process_piece
|
31
|
+
when CHOKE then choke_message
|
32
|
+
when HANDSHAKE then process_handshake
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def process_bitfield
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
def process_have
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
def process_piece
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
def process_handshake
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
def recv_data
|
54
|
+
|
15
55
|
end
|
16
56
|
end
|
17
57
|
end
|
data/lib/torrenter/reactor.rb
CHANGED
@@ -1,68 +1,175 @@
|
|
1
1
|
module Torrenter
|
2
|
-
class Reactor
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
@
|
8
|
-
@
|
9
|
-
@
|
10
|
-
@
|
11
|
-
@
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
24
27
|
end
|
25
|
-
|
26
|
-
|
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 }
|
27
41
|
end
|
28
42
|
|
29
43
|
# sends the handshake messages.
|
30
44
|
|
31
45
|
def message_reactor
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
@
|
46
|
+
connect_peers
|
47
|
+
|
48
|
+
puts "You are now connected to #{active_peers.size} peers."
|
49
|
+
loop do
|
50
|
+
break if finished?
|
51
|
+
|
52
|
+
if Time.now.to_i - @time == 1
|
53
|
+
@time_counter = 0
|
54
|
+
end
|
55
|
+
|
56
|
+
@time = Time.now.to_i
|
57
|
+
|
58
|
+
active_peers.each_with_index do |peer, i|
|
59
|
+
|
60
|
+
datasize = peer.current_size
|
61
|
+
|
62
|
+
state = peer.state(@master_index, remaining) do |data, i|
|
63
|
+
if verified?(i, data)
|
64
|
+
@master_index[peer.index] = :downloaded
|
65
|
+
@byte_counter += peer.piece_data.bytesize
|
66
|
+
pack_piece(peer)
|
67
|
+
peer.mark_complete
|
68
|
+
piece_select(peer)
|
51
69
|
end
|
52
70
|
end
|
71
|
+
|
72
|
+
current = peer.current_size - datasize
|
73
|
+
if state == :interested
|
74
|
+
peer.update_indices(@master_index)
|
75
|
+
piece_select(peer)
|
76
|
+
end
|
77
|
+
|
78
|
+
if current > 0
|
79
|
+
@time_counter += current
|
80
|
+
end
|
53
81
|
end
|
54
|
-
|
55
|
-
|
82
|
+
reattempt_disconnected_peer if Time.now.to_i % 30 == 0
|
83
|
+
|
84
|
+
show_status
|
56
85
|
end
|
57
|
-
seperate_data_dump_into_files
|
58
86
|
end
|
59
87
|
|
60
|
-
def
|
61
|
-
|
88
|
+
def piece_indices
|
89
|
+
active_peers.select { |peer| peer.piece_index }
|
90
|
+
.map { |peer| peer.piece_index }
|
62
91
|
end
|
63
92
|
|
64
|
-
def
|
65
|
-
|
93
|
+
def tally
|
94
|
+
@master_index.map.with_index do |p,i|
|
95
|
+
p == :free ? piece_indices.map { |x| x[i] } : []
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def piece_select(peer, limit=1)
|
100
|
+
# binding.pry if @master_index.count(:free) == 1
|
101
|
+
|
102
|
+
piece_index = peer.piece_index
|
103
|
+
@tally = tally
|
104
|
+
index = nil
|
105
|
+
|
106
|
+
loop do
|
107
|
+
if limit == active_peers.size + 1
|
108
|
+
index = piece_index.index(:free)
|
109
|
+
break
|
110
|
+
elsif index.is_a?(Integer)
|
111
|
+
break
|
112
|
+
end
|
113
|
+
i = 0
|
114
|
+
loop do
|
115
|
+
break if i > piece_index.size - 1|| !index.nil?
|
116
|
+
if @tally[i].count(:free) == limit && piece_index[i] == :free
|
117
|
+
index = i
|
118
|
+
else
|
119
|
+
i += 1
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
limit += 1 if index.nil?
|
124
|
+
end
|
125
|
+
|
126
|
+
if index
|
127
|
+
@master_index[index] = :downloading
|
128
|
+
peer.request_piece(index)
|
129
|
+
return index
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def show_status
|
134
|
+
if (Time.now.to_i - @time) == 1
|
135
|
+
system('clear')
|
136
|
+
puts "#{download_bar} \n Downloading #{$data_dump[/(.+)(?=torrent-data)/]}"\
|
137
|
+
"at #{real} KB/sec #{data_remaining} MB remaining"\
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def remaining
|
142
|
+
total_file_size - @byte_counter
|
143
|
+
end
|
144
|
+
|
145
|
+
def select_index(index)
|
146
|
+
poo = @master_index.map.with_index do |peer,index|
|
147
|
+
active_peers.map { |x| x.piece_index[index] }
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def peer_indices
|
152
|
+
active_peers.map { |peer| peer.piece_index }
|
153
|
+
end
|
154
|
+
|
155
|
+
def data_remaining
|
156
|
+
(total_file_size - @byte_counter).fdiv(1024).fdiv(1024).round(2)
|
157
|
+
end
|
158
|
+
|
159
|
+
def real
|
160
|
+
@time_counter.fdiv(1024).round(1)
|
161
|
+
end
|
162
|
+
|
163
|
+
def reattempt_disconnected_peer
|
164
|
+
disconnected_peers.sample.connect
|
165
|
+
end
|
166
|
+
|
167
|
+
def disconnected_peers
|
168
|
+
@peers.select { |peer| !peer.connected? }
|
169
|
+
end
|
170
|
+
|
171
|
+
def pack_piece(peer)
|
172
|
+
IO.write($data_dump, peer.piece_data, piece_length * peer.index)
|
66
173
|
end
|
67
174
|
|
68
175
|
def indices
|
@@ -77,17 +184,13 @@ module Torrenter
|
|
77
184
|
end
|
78
185
|
end
|
79
186
|
|
80
|
-
def peer_data
|
81
|
-
active_peers.map { |peer| "ip: #{peer.peer[:ip]} port:#{peer.peer[:port]}" }.join("\n")
|
82
|
-
end
|
83
|
-
|
84
187
|
def get_status(i)
|
85
188
|
peer = @peers.find { |peer| peer.piece_index[i] == :downloading }
|
86
189
|
(peer.buffer.bytesize + peer.piece_size).fdiv(@piece_length)
|
87
190
|
end
|
88
191
|
|
89
192
|
def active_peers
|
90
|
-
@peers.select { |peer| peer.
|
193
|
+
@peers.select { |peer| peer.peer_state }
|
91
194
|
end
|
92
195
|
|
93
196
|
def index_percentages
|
@@ -104,42 +207,5 @@ module Torrenter
|
|
104
207
|
def pieces(type)
|
105
208
|
(@master_index.count(type).fdiv(@master_index.size) * 100).round
|
106
209
|
end
|
107
|
-
|
108
|
-
def stop_downloading
|
109
|
-
@peers.each { |peer| peer.piece_index.map { |piece| piece = :downloaded} }
|
110
|
-
end
|
111
|
-
|
112
|
-
def seperate_data_dump_into_files
|
113
|
-
if multiple_files?
|
114
|
-
offset = 0
|
115
|
-
folder = $data_dump[/.+(?=\.torrent-data)/] || FileUtils.mkdir($data_dump[/.+(?=\.torrent-data)/]).join
|
116
|
-
@file_list.each do |file|
|
117
|
-
|
118
|
-
length = file['length']
|
119
|
-
filename = file['path'].pop
|
120
|
-
|
121
|
-
if multiple_sub_folders?(file)
|
122
|
-
subfolders = file['path'].join("/")
|
123
|
-
folder = folder + "/" + subfolders
|
124
|
-
FileUtils.mkdir_p("#{folder}", force: true)
|
125
|
-
end
|
126
|
-
|
127
|
-
File.open("#{folder}/#{filename}", 'a+') { |data| data << IO.read($data_dump, length, offset) }
|
128
|
-
|
129
|
-
offset += length
|
130
|
-
end
|
131
|
-
else
|
132
|
-
File.open("#{folder}/#{@file_list['name']}", 'w') { |data| data << File.read($data_dump) }
|
133
|
-
end
|
134
|
-
File.delete($data_dump)
|
135
|
-
end
|
136
|
-
|
137
|
-
def multiple_sub_folders?(file)
|
138
|
-
file['path'].size > 1
|
139
|
-
end
|
140
|
-
|
141
|
-
def multiple_files?
|
142
|
-
@file_list.is_a?(Array)
|
143
|
-
end
|
144
210
|
end
|
145
211
|
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'bencode'
|
2
|
+
require 'pry'
|
3
|
+
require 'digest/sha1'
|
4
|
+
require './lib/torrenter/torrent_reader.rb'
|
5
|
+
require './lib/torrenter/udp_tracker.rb'
|
6
|
+
require './lib/torrenter/http_tracker.rb'
|
7
|
+
|
8
|
+
stream = BEncode.load_file('thrones.torrent')
|
9
|
+
reader = TorrentReader.new(stream)
|
10
|
+
binding.pry
|
@@ -0,0 +1,166 @@
|
|
1
|
+
module Torrenter
|
2
|
+
# KEEP_ALIVE = "\x00\x00\x00\x00"
|
3
|
+
# BLOCK = 2**14
|
4
|
+
# these methods get mixed in with the Peer class as a way to help
|
5
|
+
# organize and parse the byte-encoded data. The intention is to shorten
|
6
|
+
# and shrink the complexity of the Peer class.
|
7
|
+
# the following methods are responsible solely for data retrieval and data transmission
|
8
|
+
|
9
|
+
def state(master_index)
|
10
|
+
recv_data if @buffer.size <= 3
|
11
|
+
if @buffer[4].nil?
|
12
|
+
@buffer.slice!(0..3) if @buffer[0..3] == KEEP_ALIVE
|
13
|
+
else
|
14
|
+
@length = @buffer[0..3].unpack("N*").last
|
15
|
+
@buffer_id = @buffer[4]
|
16
|
+
case @buffer_id
|
17
|
+
when BITFIELD
|
18
|
+
process_bitfield(master_index.size, clear_msg)
|
19
|
+
when HAVE
|
20
|
+
process_have(clear_msg)
|
21
|
+
when INTERESTED
|
22
|
+
@buffer = ''
|
23
|
+
|
24
|
+
|
25
|
+
return :interested
|
26
|
+
when PIECE
|
27
|
+
process_piece
|
28
|
+
when CHOKE
|
29
|
+
choke_message
|
30
|
+
when HANDSHAKE
|
31
|
+
process_handshake
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def choke_message
|
37
|
+
@peer_state = false
|
38
|
+
end
|
39
|
+
|
40
|
+
def clear_msg
|
41
|
+
@buffer.slice!(0..@length + 3)
|
42
|
+
end
|
43
|
+
|
44
|
+
def request_message(bytes=BLOCK)
|
45
|
+
@msg = "index: #{@index} offset: #{@blocks.size * BLOCK} bytes: #{bytes}"
|
46
|
+
send_data(pack(13) + "\x06" + pack(@index) + pack(@blocks.size * BLOCK) + pack(bytes))
|
47
|
+
end
|
48
|
+
|
49
|
+
def pack(msg)
|
50
|
+
[msg].pack("I>")
|
51
|
+
end
|
52
|
+
|
53
|
+
def process_bitfield(size, msg)
|
54
|
+
index = msg[5..-1].unpack("B#{size}").join.split('')
|
55
|
+
@piece_index = index.map { |bit| bit == '1' ? :free : :unavailable }
|
56
|
+
send_interested if @buffer.empty?
|
57
|
+
end
|
58
|
+
|
59
|
+
def process_have(msg)
|
60
|
+
@piece_index[msg[5..-1].unpack("C*").last] = :free
|
61
|
+
send_interested if @buffer.empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
def request_piece(index)
|
65
|
+
@index = index
|
66
|
+
@piece_index[@index] = :downloading
|
67
|
+
|
68
|
+
request_message
|
69
|
+
end
|
70
|
+
|
71
|
+
def send_interested
|
72
|
+
send_data("\x00\x00\x00\x01\x02")
|
73
|
+
end
|
74
|
+
|
75
|
+
def buffer_length?
|
76
|
+
@buffer.bytesize >= @length + 4
|
77
|
+
end
|
78
|
+
|
79
|
+
def process_piece
|
80
|
+
if buffer_length?
|
81
|
+
if buffer_complete?
|
82
|
+
pack_buffer
|
83
|
+
end
|
84
|
+
|
85
|
+
diff = @remaining - @blocks.join('').bytesize
|
86
|
+
if diff < BLOCK
|
87
|
+
request_message(diff)
|
88
|
+
else
|
89
|
+
request_message
|
90
|
+
end
|
91
|
+
end
|
92
|
+
recv_data
|
93
|
+
end
|
94
|
+
|
95
|
+
def mark_complete
|
96
|
+
@blocks = []
|
97
|
+
@piece_index[@index] = :downloaded
|
98
|
+
end
|
99
|
+
|
100
|
+
def last_piece?
|
101
|
+
@remaining
|
102
|
+
end
|
103
|
+
|
104
|
+
def piece_complete?
|
105
|
+
piece_size == @piece_length
|
106
|
+
end
|
107
|
+
|
108
|
+
def buffer_size
|
109
|
+
@buffer[13..-1].bytesize
|
110
|
+
end
|
111
|
+
|
112
|
+
def pack_buffer
|
113
|
+
@blocks << @buffer.slice!(13..-1)
|
114
|
+
@buffer.clear
|
115
|
+
end
|
116
|
+
|
117
|
+
def buffer_complete?
|
118
|
+
(@buffer.length - 13) == BLOCK || (@buffer.length - 13) == @remaining
|
119
|
+
end
|
120
|
+
|
121
|
+
# will need a last piece sort of thing inserted in here
|
122
|
+
|
123
|
+
def piece_full?
|
124
|
+
@blocks.join('').size == @piece_len
|
125
|
+
end
|
126
|
+
|
127
|
+
def piece_size
|
128
|
+
@blocks.join('').bytesize + (@buffer.bytesize - 13)
|
129
|
+
end
|
130
|
+
|
131
|
+
def process_handshake
|
132
|
+
@buffer.slice!(0..67) if hash_check?
|
133
|
+
end
|
134
|
+
|
135
|
+
def hash_check?
|
136
|
+
@buffer[28..47] == @info_hash
|
137
|
+
end
|
138
|
+
|
139
|
+
def send_data(msg)
|
140
|
+
begin
|
141
|
+
Timeout::timeout(2) { @socket.sendmsg_nonblock(msg) }
|
142
|
+
rescue Timeout::Error
|
143
|
+
''
|
144
|
+
rescue IO::EAGAINWaitReadable
|
145
|
+
''
|
146
|
+
rescue *EXCEPTIONS
|
147
|
+
''
|
148
|
+
rescue Errno::ETIMEDOUT
|
149
|
+
@peer_state = false
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def recv_data(bytes=BLOCK)
|
154
|
+
begin
|
155
|
+
Timeout::timeout(5) { @buffer << @socket.recv_nonblock(bytes) }
|
156
|
+
rescue Timeout::Error
|
157
|
+
''
|
158
|
+
rescue Errno::ETIMEDOUT
|
159
|
+
@peer_state = false
|
160
|
+
rescue *EXCEPTIONS
|
161
|
+
''
|
162
|
+
rescue IO::EAGAINWaitReadable
|
163
|
+
''
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|