torrenter 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.
- checksums.yaml +7 -0
- data/bin/torrenter +9 -0
- data/lib/torrenter.rb +16 -0
- data/lib/torrenter/message/message_types.rb +12 -0
- data/lib/torrenter/message/messager.rb +224 -0
- data/lib/torrenter/peer.rb +62 -0
- data/lib/torrenter/reactor.rb +105 -0
- data/lib/torrenter/torrent_reader.rb +137 -0
- data/lib/torrenter/udp.rb +87 -0
- data/lib/torrenter/version.rb +3 -0
- metadata +54 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 473fad5c40e464ed097518df8cff579077b34072
|
4
|
+
data.tar.gz: 40ff8e80571d14cd92a32f2e7b1bad4658914a36
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e2e4ae190b34baed37fd95981f33fc4b839fbdf6fa70381a71c4fcd25d75e23ae693ab9bb715a6c5da62dffddf9e7e33d4391ae3a7281734e09ea2696f4d7a32
|
7
|
+
data.tar.gz: 15d2962fb37e1b76c86f8f9cf98a6e01f421cc4c6d97ed56ece7d36077e7bea4bc970cec02ae0a39e640e2647971ee472342c5db91dc4ad7aa1e73b186a3349c
|
data/bin/torrenter
ADDED
data/lib/torrenter.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'torrenter/message/messager'
|
2
|
+
require 'torrenter/message/message_types'
|
3
|
+
require 'torrenter/peer'
|
4
|
+
require 'torrenter/reactor'
|
5
|
+
require 'torrenter/udp'
|
6
|
+
require 'torrenter/torrent_reader'
|
7
|
+
module Torrenter
|
8
|
+
class Torrent
|
9
|
+
def start(file)
|
10
|
+
IO.write($data_dump, '', 0) if !File.exists?($data_dump)
|
11
|
+
stream = BEncode.load_file(file)
|
12
|
+
peers = Torrenter::TorrentReader.new(stream)
|
13
|
+
peers.determine_protocol
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Torrenter
|
2
|
+
PEER_ID = '-MATT16548651231825-'
|
3
|
+
BLOCK = 2**14
|
4
|
+
KEEP_ALIVE = "\x00\x00\x00\x00"
|
5
|
+
INTERESTED = "\x01"
|
6
|
+
HANDSHAKE = "T"
|
7
|
+
HAVE = "\x04"
|
8
|
+
BITFIELD = "\x05"
|
9
|
+
PIECE = "\a"
|
10
|
+
CHOKE = "\x00"
|
11
|
+
PROTOCOL = "\x13BitTorrent protocol\x00\x00\x00\x00\x00\x00\x00\x00"
|
12
|
+
end
|
@@ -0,0 +1,224 @@
|
|
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
|
+
|
8
|
+
# the following methods are responsible solely for data retrieval and data transmission
|
9
|
+
|
10
|
+
def send_data(msg, opts={})
|
11
|
+
begin
|
12
|
+
Timeout::timeout(2) { @socket.sendmsg_nonblock(msg) }
|
13
|
+
rescue Timeout::Error
|
14
|
+
''
|
15
|
+
rescue Errno::EADDRNOTAVAIL
|
16
|
+
''
|
17
|
+
rescue Errno::ECONNREFUSED
|
18
|
+
''
|
19
|
+
rescue Errno::EPIPE
|
20
|
+
''
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def recv_data(bytes=BLOCK, opts={})
|
25
|
+
begin
|
26
|
+
if opts[:peek]
|
27
|
+
Timeout::timeout(2) { @socket.recv_nonblock(4, Socket::MSG_PEEK) }
|
28
|
+
else
|
29
|
+
Timeout::timeout(2) { buffer << @socket.recv_nonblock(bytes) }
|
30
|
+
end
|
31
|
+
rescue Timeout::Error
|
32
|
+
''
|
33
|
+
rescue Errno::EADDRNOTAVAIL
|
34
|
+
''
|
35
|
+
rescue Errno::ECONNREFUSED
|
36
|
+
''
|
37
|
+
rescue Errno::ECONNRESET
|
38
|
+
''
|
39
|
+
rescue IO::EAGAINWaitReadable
|
40
|
+
''
|
41
|
+
rescue Errno::ETIMEDOUT
|
42
|
+
@status = false
|
43
|
+
@buffer = ''
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# the next few methods responsibility is for parsing the buffer.
|
48
|
+
# They are responsible for the critical step of analyzing the buffer,
|
49
|
+
# checking for message consistency from the peer (and if it fails a test
|
50
|
+
# another message is sent out to attempt to fix the missing data)
|
51
|
+
|
52
|
+
|
53
|
+
# parse message will be the gatekeeper. If the buffer is ever low in the READ part
|
54
|
+
# of the torrent program, then it knows more data may be required.
|
55
|
+
# It will also be responsble for EVERY new message that gets received, whether
|
56
|
+
# its in the download sequence of messaging or in the actual handshake, it will
|
57
|
+
# control it that way.
|
58
|
+
|
59
|
+
# for now, I'm assuming that at least the bitfield will be sent in full
|
60
|
+
|
61
|
+
def parse_bitfield
|
62
|
+
len = msg_len
|
63
|
+
buffer.slice!(0)
|
64
|
+
@piece_index = buffer.slice!(0...len).unpack("B#{sha_list.size}")
|
65
|
+
.first.split('')
|
66
|
+
.map { |bit| bit == '1' ? :available : :unavailable }
|
67
|
+
end
|
68
|
+
|
69
|
+
def send_interested
|
70
|
+
send_data("\x00\x00\x00\x01\x02")
|
71
|
+
end
|
72
|
+
|
73
|
+
def parse_have
|
74
|
+
if buffer[5..8].bytesize == 4
|
75
|
+
index = buffer[5..8].unpack("N*").first
|
76
|
+
@piece_index[index] = :available
|
77
|
+
buffer.slice!(0..8)
|
78
|
+
else
|
79
|
+
recv_data
|
80
|
+
end
|
81
|
+
send_interested if @buffer.empty?
|
82
|
+
end
|
83
|
+
|
84
|
+
# because ruby.
|
85
|
+
|
86
|
+
def parse_handshake
|
87
|
+
@buffer.slice!(0..67) if hash_match?
|
88
|
+
end
|
89
|
+
|
90
|
+
def hash_match?
|
91
|
+
@buffer.unpack("A*").first[28..47] == info_hash
|
92
|
+
end
|
93
|
+
|
94
|
+
# the negative 1 modifier is for factoring in the id
|
95
|
+
|
96
|
+
def msg_len
|
97
|
+
@buffer.slice!(0..3).unpack("N*").first - 1
|
98
|
+
end
|
99
|
+
|
100
|
+
def evaluate_index(master)
|
101
|
+
@requesting = 0
|
102
|
+
if @piece_index.any? { |chunk| chunk == :available }
|
103
|
+
@index = @piece_index.index(:available)
|
104
|
+
if master[@index] == :free
|
105
|
+
@piece_index[@index] = :downloading
|
106
|
+
master[@index] = :downloading
|
107
|
+
else
|
108
|
+
@piece_index[@index] = :downloaded
|
109
|
+
evaluate_index(master)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def modify_index(master)
|
115
|
+
master.each_with_index do |v,i|
|
116
|
+
if v == :downloaded
|
117
|
+
@piece_index[i] = :downloaded
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def state(master, blocks)
|
123
|
+
@buffer_state = @buffer[4]
|
124
|
+
if buffer.bytesize <= 3
|
125
|
+
recv_data
|
126
|
+
elsif buffer.bytesize == 4 && buffer[0..3] == KEEP_ALIVE
|
127
|
+
buffer.slice!(0..3)
|
128
|
+
else
|
129
|
+
# p @buffer[0..3].unp@buack("C*")
|
130
|
+
case @buffer[4]
|
131
|
+
when INTERESTED
|
132
|
+
buffer.slice!(0..4)
|
133
|
+
modify_index(master)
|
134
|
+
evaluate_index(master)
|
135
|
+
request_message
|
136
|
+
when HAVE
|
137
|
+
parse_have
|
138
|
+
when BITFIELD
|
139
|
+
parse_bitfield
|
140
|
+
send_interested if buffer.empty?
|
141
|
+
when PIECE
|
142
|
+
@length = buffer[0..3].unpack("N*").first + 4
|
143
|
+
if buffer.bytesize >= @length
|
144
|
+
|
145
|
+
buffer.slice!(0..12)
|
146
|
+
# the metadata is slireced off.
|
147
|
+
@offset += (@length - 13)
|
148
|
+
# buffer is reduced
|
149
|
+
pack_buffer
|
150
|
+
# that means the bytes for that block have been collected entirely.
|
151
|
+
# the buffer becomes empty
|
152
|
+
if pieces_remaining(master) > 1
|
153
|
+
if piece_size == @piece_len
|
154
|
+
pack_file(master)
|
155
|
+
evaluate_index(master)
|
156
|
+
request_message
|
157
|
+
else
|
158
|
+
request_message
|
159
|
+
end
|
160
|
+
elsif (File.size($data_dump) + piece_size) == total_file_size
|
161
|
+
pack_file(master)
|
162
|
+
else
|
163
|
+
if bytes_remaining >= BLOCK
|
164
|
+
request_message
|
165
|
+
else
|
166
|
+
request_message(bytes_remaining)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
else
|
170
|
+
recv_data(@length - buffer.bytesize)
|
171
|
+
end
|
172
|
+
# piece
|
173
|
+
when CHOKE
|
174
|
+
buffer.slice!(0..3)
|
175
|
+
send_interested
|
176
|
+
when HANDSHAKE
|
177
|
+
parse_handshake
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def pieces_remaining(master)
|
183
|
+
master.count(:downloading) + master.count(:free)
|
184
|
+
end
|
185
|
+
|
186
|
+
def bytes_remaining
|
187
|
+
(total_file_size - File.size($data_dump)) - piece_size
|
188
|
+
end
|
189
|
+
|
190
|
+
def piece_size
|
191
|
+
@block_map.join.bytesize
|
192
|
+
end
|
193
|
+
|
194
|
+
def pack_buffer
|
195
|
+
@block_map << @buffer.slice!(0...(@length - 13))
|
196
|
+
end
|
197
|
+
|
198
|
+
def pack_file(master)
|
199
|
+
data = @block_map.join
|
200
|
+
if piece_verified?(data)
|
201
|
+
IO.write($data_dump, data, @index * @offset)
|
202
|
+
master[@index] = :downloaded
|
203
|
+
@piece_index[@index] = :downloaded
|
204
|
+
else
|
205
|
+
master[@index] = :free
|
206
|
+
@piece_index[@index] = :downloaded
|
207
|
+
end
|
208
|
+
@block_map = []
|
209
|
+
@offset = 0
|
210
|
+
end
|
211
|
+
|
212
|
+
def piece_verified?(data)
|
213
|
+
Digest::SHA1.digest(data) == sha_list[@index]
|
214
|
+
end
|
215
|
+
|
216
|
+
def request_message(bytes=BLOCK)
|
217
|
+
@requesting += 1
|
218
|
+
send_data(pack(13) + "\x06" + pack(@index) + pack(@offset) + pack(bytes))
|
219
|
+
end
|
220
|
+
|
221
|
+
def pack(i)
|
222
|
+
[i].pack("I>")
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# contains the peer information
|
2
|
+
module Torrenter
|
3
|
+
class Peer
|
4
|
+
include Torrenter
|
5
|
+
# for non outside vars use the @ and remove them from
|
6
|
+
|
7
|
+
attr_reader :socket, :peer, :sha_list, :piece_len, :info_hash, :status
|
8
|
+
attr_accessor :piece_index, :offset, :buffer, :block_map
|
9
|
+
|
10
|
+
|
11
|
+
def initialize(peer, file_list, peer_info={})
|
12
|
+
@peer = peer
|
13
|
+
@info_hash = peer_info[:info_hash]
|
14
|
+
@piece_len = peer_info[:piece_length]
|
15
|
+
@sha_list = peer_info[:sha_list]
|
16
|
+
@piece_index = peer_info[:piece_index]
|
17
|
+
@buffer = ''
|
18
|
+
@block_map = []
|
19
|
+
@offset = 0
|
20
|
+
@file_list = file_list
|
21
|
+
@status = false
|
22
|
+
end
|
23
|
+
|
24
|
+
def total_file_size
|
25
|
+
if @file_list.is_a?(Array)
|
26
|
+
@file_list.map { |f| f['length'] }.inject { |x, y| x + y }
|
27
|
+
else
|
28
|
+
@file_list['length']
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def connect
|
33
|
+
puts "\nConnecting to IP: #{peer[:ip]} PORT: #{peer[:port]}"
|
34
|
+
begin
|
35
|
+
Timeout::timeout(2) { @socket = TCPSocket.new(peer[:ip], peer[:port]) }
|
36
|
+
rescue Timeout::Error
|
37
|
+
puts "Timed out."
|
38
|
+
rescue Errno::EADDRNOTAVAIL
|
39
|
+
puts "Address not available."
|
40
|
+
rescue Errno::ECONNREFUSED
|
41
|
+
puts "Connection refused."
|
42
|
+
rescue Errno::ECONNRESET
|
43
|
+
puts "bastards."
|
44
|
+
end
|
45
|
+
|
46
|
+
if @socket
|
47
|
+
@socket.write(handshake)
|
48
|
+
@status = true
|
49
|
+
else
|
50
|
+
@status = false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def handshake
|
55
|
+
"#{PROTOCOL}#{@info_hash}#{PEER_ID}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def msg_num
|
59
|
+
@buffer.slice!(0..3).unpack("N*").first
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Torrenter
|
2
|
+
class Reactor
|
3
|
+
include Torrenter
|
4
|
+
def initialize(peers, sha_list, piece_length, file_list)
|
5
|
+
@peers = peers
|
6
|
+
@master_index = Array.new(sha_list.size) { :free }
|
7
|
+
@blocks = piece_length / BLOCK
|
8
|
+
@file_list = file_list
|
9
|
+
@piece_length = piece_length
|
10
|
+
@sha_list = sha_list
|
11
|
+
@data_size = if file_list.is_a?(Array)
|
12
|
+
file_list.map { |f| f['length'] }.inject { |x, y| x + y }
|
13
|
+
else
|
14
|
+
file_list['length']
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def modify_index
|
19
|
+
IO.write($data_dump, '', 0) unless File.exists?($data_dump)
|
20
|
+
file_size = File.size($data_dump)
|
21
|
+
0.upto(@sha_list.size - 1) do |n|
|
22
|
+
data = IO.read($data_dump, @piece_length, n * @piece_length) || ''
|
23
|
+
@master_index[n] = :downloaded if Digest::SHA1.digest(data) == @sha_list[n]
|
24
|
+
end
|
25
|
+
puts "#{@master_index.count(:downloaded)} pieces are downloaded already."
|
26
|
+
end
|
27
|
+
|
28
|
+
# sends the handshake messages.
|
29
|
+
|
30
|
+
def message_reactor(opts={})
|
31
|
+
modify_index
|
32
|
+
if !@master_index.all? { |index| index == :downloaded }
|
33
|
+
@peers.each { |peer| peer.connect }
|
34
|
+
loop do
|
35
|
+
break if @master_index.all? { |piece| piece == :downloaded }
|
36
|
+
@peers.each do |peer|
|
37
|
+
piece_count = @master_index.count(:downloaded)
|
38
|
+
if peer.status
|
39
|
+
peer.state(@master_index, @blocks) # unless peer.piece_index.all? { |piece| piece == :downloaded }
|
40
|
+
if @master_index.count(:downloaded) > piece_count
|
41
|
+
system("clear")
|
42
|
+
puts download_bar + "Downloading from #{active_peers} active peers"
|
43
|
+
end
|
44
|
+
elsif Time.now.to_i % 60 == 0
|
45
|
+
peer.connect
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
stop_downloading
|
50
|
+
seperate_data_dump_into_files
|
51
|
+
else
|
52
|
+
upload_data
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def active_peers
|
57
|
+
@peers.select { |peer| peer.status }.size
|
58
|
+
end
|
59
|
+
|
60
|
+
def download_bar
|
61
|
+
("\u2588" * pieces(:downloaded)) + ("\u2593" * pieces(:downloading)) + (" " * pieces(:free)) + " %#{pieces(:downloaded)} downloaded "
|
62
|
+
end
|
63
|
+
|
64
|
+
def pieces(type)
|
65
|
+
(@master_index.count(type).fdiv(@master_index.size) * 100).round
|
66
|
+
end
|
67
|
+
|
68
|
+
def stop_downloading
|
69
|
+
@peers.each { |peer| peer.piece_index.map { |piece| piece = :downloaded} }
|
70
|
+
end
|
71
|
+
|
72
|
+
def seperate_data_dump_into_files
|
73
|
+
if multiple_files?
|
74
|
+
offset = 0
|
75
|
+
folder = $data_dump[/.+(?=\.torrent-data)/] || FileUtils.mkdir($data_dump[/.+(?=\.torrent-data)/]).join
|
76
|
+
@file_list.each do |file|
|
77
|
+
|
78
|
+
length = file['length']
|
79
|
+
filename = file['path'].pop
|
80
|
+
|
81
|
+
if multiple_sub_folders?(file)
|
82
|
+
subfolders = file['path'].join("/")
|
83
|
+
folder = folder + "/" + subfolders
|
84
|
+
FileUtils.mkdir_p("#{folder}", force: true)
|
85
|
+
end
|
86
|
+
|
87
|
+
File.open("#{folder}/#{filename}", 'a+') { |data| data << IO.read($data_dump, length, offset) }
|
88
|
+
|
89
|
+
offset += length
|
90
|
+
end
|
91
|
+
else
|
92
|
+
File.open("#{folder}/#{@file_list['name'].join}", 'w') { |data| data << File.read($data_dump) }
|
93
|
+
end
|
94
|
+
File.delete($data_dump)
|
95
|
+
end
|
96
|
+
|
97
|
+
def multiple_sub_folders?(file)
|
98
|
+
file['path'].size > 1
|
99
|
+
end
|
100
|
+
|
101
|
+
def multiple_files?
|
102
|
+
@file_list.is_a?(Array)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'digest/sha1'
|
3
|
+
require 'bencode'
|
4
|
+
require 'pry'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
|
8
|
+
module Torrenter
|
9
|
+
class TorrentReader
|
10
|
+
attr_reader :stream
|
11
|
+
def initialize(stream)
|
12
|
+
@stream = stream
|
13
|
+
end
|
14
|
+
|
15
|
+
def determine_protocol
|
16
|
+
trackers = if @stream['announce-list']
|
17
|
+
@stream['announce-list'].flatten << @stream['announce']
|
18
|
+
else
|
19
|
+
[@stream['announce']]
|
20
|
+
end
|
21
|
+
|
22
|
+
@http_trackers = trackers.select { |tracker| tracker =~ /http\:\/\// }
|
23
|
+
@udp_trackers = trackers.select { |tracker| tracker =~ /udp\:\/\// }
|
24
|
+
# first try the http trackers...
|
25
|
+
|
26
|
+
connect_to_http_tracker if @http_trackers.size > 0
|
27
|
+
connect_to_udp_tracker if @peers.nil?
|
28
|
+
peer_list
|
29
|
+
establish_reactor
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
def connect_to_http_tracker
|
34
|
+
@uri = URI(@http_trackers.shift)
|
35
|
+
@uri.query = URI.encode_www_form(peer_hash)
|
36
|
+
@peers = raw_peers
|
37
|
+
end
|
38
|
+
|
39
|
+
def connect_to_udp_tracker
|
40
|
+
@udp_trackers.map! do |udp|
|
41
|
+
port = udp[/\d+/]
|
42
|
+
udp.gsub!(/udp\:\/\/|\:\d+|\/announce/, '')
|
43
|
+
begin
|
44
|
+
ip = Socket.getaddrinfo(udp, 80)[0][3]
|
45
|
+
udp_socket = UDPConnection.new(ip, port.to_i, sha)
|
46
|
+
udp_socket.connect_to_udp_host
|
47
|
+
rescue SocketError
|
48
|
+
puts 'Unuseable UDP site'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
while @peers.nil?
|
53
|
+
udp = @udp_trackers.shift
|
54
|
+
begin
|
55
|
+
@peers = udp.message_relay
|
56
|
+
rescue
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
# url gets reformatted to include query parameters
|
64
|
+
|
65
|
+
def raw_peers
|
66
|
+
begin
|
67
|
+
BEncode.load(Net::HTTP.get(@uri))['peers']
|
68
|
+
rescue
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
def piece_length
|
75
|
+
stream['info']['piece length']
|
76
|
+
end
|
77
|
+
|
78
|
+
def sha
|
79
|
+
Digest::SHA1.digest(stream['info'].bencode)
|
80
|
+
end
|
81
|
+
|
82
|
+
# data stored as a hash in the order made necessary
|
83
|
+
|
84
|
+
def peer_hash
|
85
|
+
{
|
86
|
+
:info_hash => sha,
|
87
|
+
:peer_id => PEER_ID,
|
88
|
+
:left => piece_length,
|
89
|
+
:pieces => stream['info']['files'] || stream['info']['name']
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
# Using the peers key of the torrent file, the hex-encoded data gets reinterpreted as ips addresses.
|
94
|
+
|
95
|
+
def peer_list
|
96
|
+
ip_list = []
|
97
|
+
until @peers.empty?
|
98
|
+
ip_list << { ip: @peers.slice!(0..3).bytes.join('.'), port: @peers.slice!(0..1).unpack("S>").first }
|
99
|
+
end
|
100
|
+
|
101
|
+
@peers = ip_list.map { |peer| Peer.new(peer, file_list, peer_info) }
|
102
|
+
end
|
103
|
+
|
104
|
+
def sha_list
|
105
|
+
n, e = 0, 20
|
106
|
+
list = []
|
107
|
+
until stream['info']['pieces'].bytesize < e
|
108
|
+
list << stream['info']['pieces'].byteslice(n...e)
|
109
|
+
n += 20
|
110
|
+
e += 20
|
111
|
+
end
|
112
|
+
list
|
113
|
+
end
|
114
|
+
|
115
|
+
def peer_info
|
116
|
+
{
|
117
|
+
:info_hash => sha,
|
118
|
+
:piece_length => piece_length,
|
119
|
+
:sha_list => sha_list,
|
120
|
+
:piece_index => Array.new(sha_list.size) { false }
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
def file_list
|
125
|
+
stream['info']['files'] || stream['info']
|
126
|
+
end
|
127
|
+
|
128
|
+
def establish_reactor
|
129
|
+
react = Reactor.new(@peers, sha_list, piece_length, file_list)
|
130
|
+
begin
|
131
|
+
react.message_reactor
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Torrenter
|
2
|
+
class UDPConnection
|
3
|
+
attr_reader :sender, :response
|
4
|
+
def initialize(ip, port, info_hash)
|
5
|
+
@ip = ip
|
6
|
+
@port = port
|
7
|
+
@sender = UDPSocket.new
|
8
|
+
@t = rand(10000)
|
9
|
+
@connection_id = [0x41727101980].pack("Q>")
|
10
|
+
@transaction_id = [@t].pack("I>")
|
11
|
+
@info_hash = info_hash
|
12
|
+
end
|
13
|
+
|
14
|
+
def connect_to_udp_host
|
15
|
+
begin
|
16
|
+
@sender.connect(@ip, @port)
|
17
|
+
return self
|
18
|
+
rescue
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def message_relay
|
24
|
+
@sender.send(connect_msg, 0)
|
25
|
+
read_response
|
26
|
+
if @response
|
27
|
+
@connection_id = @response[-8..-1]
|
28
|
+
@transaction_id = [rand(10000)].pack("I>")
|
29
|
+
@sender.send(announce_msg, 0)
|
30
|
+
end
|
31
|
+
|
32
|
+
read_response
|
33
|
+
|
34
|
+
if @response
|
35
|
+
parse_announce if @response[0..3] == action(1)
|
36
|
+
return @response
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_announce
|
41
|
+
if @response[4..7] == @transaction_id
|
42
|
+
res = @response.slice!(0..11)
|
43
|
+
leechers = @response.slice!(0..3).unpack("I>").first
|
44
|
+
seeders = @response.slice!(0..3).unpack("I>").first
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def send_message
|
49
|
+
begin
|
50
|
+
@sender.send(@msg, 0)
|
51
|
+
rescue
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def read_response
|
56
|
+
begin
|
57
|
+
@response = @sender.recv(1028)
|
58
|
+
rescue Exception => e
|
59
|
+
e
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def connect_match?
|
64
|
+
data[0] == (action(0) + @transaction_id + @connection_id)
|
65
|
+
end
|
66
|
+
|
67
|
+
def announce_input
|
68
|
+
@connection_id + action(1) + @transaction_id + @info_hash + PEER_ID
|
69
|
+
end
|
70
|
+
|
71
|
+
def connect_msg
|
72
|
+
@connection_id + action(0) + @transaction_id
|
73
|
+
end
|
74
|
+
|
75
|
+
def port
|
76
|
+
@sender.addr[1]
|
77
|
+
end
|
78
|
+
|
79
|
+
def action(n)
|
80
|
+
[n].pack("I>")
|
81
|
+
end
|
82
|
+
|
83
|
+
def announce_msg
|
84
|
+
@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) + [port].pack(">S")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
metadata
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: torrenter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- wismer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-04-21 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: BitTorrent Client written in Ruby
|
14
|
+
email:
|
15
|
+
- matthewhl@gmail.com
|
16
|
+
executables:
|
17
|
+
- torrenter
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- bin/torrenter
|
22
|
+
- lib/torrenter.rb
|
23
|
+
- lib/torrenter/message/message_types.rb
|
24
|
+
- lib/torrenter/message/messager.rb
|
25
|
+
- lib/torrenter/peer.rb
|
26
|
+
- lib/torrenter/reactor.rb
|
27
|
+
- lib/torrenter/torrent_reader.rb
|
28
|
+
- lib/torrenter/udp.rb
|
29
|
+
- lib/torrenter/version.rb
|
30
|
+
homepage: http://wismer.github.io
|
31
|
+
licenses:
|
32
|
+
- MIT
|
33
|
+
metadata: {}
|
34
|
+
post_install_message:
|
35
|
+
rdoc_options: []
|
36
|
+
require_paths:
|
37
|
+
- lib
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
requirements: []
|
49
|
+
rubyforge_project:
|
50
|
+
rubygems_version: 2.2.2
|
51
|
+
signing_key:
|
52
|
+
specification_version: 4
|
53
|
+
summary: Load by typing the torrent file name after torrenter
|
54
|
+
test_files: []
|