bittorrent 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/README.md +11 -0
- data/Rakefile +2 -0
- data/bin/bitfield.rb +14 -0
- data/bin/bittorrent +39 -0
- data/bin/block.rb +30 -0
- data/bin/block_request_process.rb +17 -0
- data/bin/block_request_scheduler.rb +130 -0
- data/bin/byte_array.rb +95 -0
- data/bin/client.rb +104 -0
- data/bin/file_handler.rb +105 -0
- data/bin/incoming_message_process.rb +50 -0
- data/bin/message.rb +62 -0
- data/bin/meta_info.rb +100 -0
- data/bin/peer.rb +67 -0
- data/bin/piece.rb +13 -0
- data/bin/tracker.rb +15 -0
- data/bittorrent.gemspec +23 -0
- data/lib/bittorrent.rb +5 -0
- data/lib/bittorrent/version.rb +3 -0
- metadata +107 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MGFjNWY0YTZjYTM4OWUyOWY3MjlkZTZkNWJhNzhjMjdmYmEwNmRiOA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZDUzMGQ3NTA0ZmQ3YmQ3MmQwYTk0ZmVjN2E3Nzc0NGYxMTQ2NWRiMA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
N2ZkYzIyMmIxZDcyZTA4MTY3ZDc4MDdmMjkxNTY0ZGYwOGFkMzNiNjc2MmM2
|
10
|
+
ZDhiOWNiODVmNjk0NzVlZTViNTYwM2RiZjk5NmFhNWMyMzcwNjg5MzllODlk
|
11
|
+
MDg0Njk3NzlhNzFiMjk0YzU4OTJkNGQ2YjFlMzVmOTUxMmQ4YWM=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ODk0MTA2N2UwYWJjOGM3MDI5YWQ1ZjkzNWRjOWYyMWU2OWM2NzZjZjY4MzIw
|
14
|
+
YjBmNmI5MTlkOTM3MjI4NDFiY2I1ZWZlNzdjNTA3MmI0M2I0M2VjNzcxNDI2
|
15
|
+
MDRmZDIxM2I2YTliODEyNTE3ODQzNmFmOTE5NTRhZTExZjA0MjI=
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/Gemfile
ADDED
data/README.md
ADDED
data/Rakefile
ADDED
data/bin/bitfield.rb
ADDED
data/bin/bittorrent
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'digest/sha1'
|
5
|
+
require 'thread'
|
6
|
+
require 'ipaddr'
|
7
|
+
require 'socket'
|
8
|
+
require 'timeout'
|
9
|
+
require 'pp'
|
10
|
+
|
11
|
+
require './ruby-bencode/lib/bencode.rb'
|
12
|
+
require './client'
|
13
|
+
require './piece'
|
14
|
+
require './block'
|
15
|
+
require './file_handler'
|
16
|
+
require './meta_info'
|
17
|
+
require './block_request_scheduler'
|
18
|
+
require './block_request_process'
|
19
|
+
require './byte_array'
|
20
|
+
require './incoming_message_process'
|
21
|
+
require './tracker'
|
22
|
+
require './peer'
|
23
|
+
require './bitfield'
|
24
|
+
require './message'
|
25
|
+
require './utils'
|
26
|
+
|
27
|
+
include Utils
|
28
|
+
|
29
|
+
if ARGV[0] == '-h'
|
30
|
+
puts 'Usage: bittorrent [TORRENT_FILE] [DOWNLOAD_FOLDER]'
|
31
|
+
end
|
32
|
+
|
33
|
+
torrent = ARGV[0]
|
34
|
+
download_path = ARGV[1]
|
35
|
+
|
36
|
+
client = Client.new(torrent, download_path)
|
37
|
+
client.run!
|
38
|
+
|
39
|
+
Utils::join_threads
|
data/bin/block.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
class Block
|
2
|
+
attr_accessor :start_byte, :end_byte, :data, :piece_index, :peer, :offset
|
3
|
+
|
4
|
+
def initialize(piece_index, offset, data, piece_length, peer)
|
5
|
+
@offset = offset
|
6
|
+
@peer = peer
|
7
|
+
@data = data
|
8
|
+
@piece_index = piece_index
|
9
|
+
@offset = offset
|
10
|
+
@start_byte = get_start_byte(piece_length)
|
11
|
+
@end_byte = get_end_byte
|
12
|
+
end
|
13
|
+
|
14
|
+
def inspect
|
15
|
+
"piece #{@piece_index}, offset #{@offset_in_piece}, data len #{@data.length}"
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def get_start_byte(piece_length)
|
21
|
+
@piece_index * piece_length + @offset
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_end_byte
|
25
|
+
@start_byte + @data.length - 1
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class BlockRequestProcess
|
2
|
+
def pipe(request)
|
3
|
+
connection = request[:connection]
|
4
|
+
connection.write(compose_request(request))
|
5
|
+
end
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def compose_request(request)
|
10
|
+
msg_length = "\0\0\0\x0d"
|
11
|
+
id = "\6"
|
12
|
+
piece_index = [request[:index]].pack("N")
|
13
|
+
byte_offset = [request[:offset]].pack("N")
|
14
|
+
request_length = [request[:size]].pack("N")
|
15
|
+
msg_length + id + piece_index + byte_offset + request_length
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
class BlockRequestScheduler
|
2
|
+
|
3
|
+
BLOCK_SIZE = 2**14
|
4
|
+
NUM_PENDING = 10
|
5
|
+
|
6
|
+
attr_accessor :request_queue
|
7
|
+
|
8
|
+
def initialize(peers, metainfo)
|
9
|
+
@peers = peers
|
10
|
+
@metainfo = metainfo
|
11
|
+
@all_block_requests = Queue.new
|
12
|
+
@request_queue = Queue.new
|
13
|
+
store_block_requests
|
14
|
+
init_requests
|
15
|
+
end
|
16
|
+
|
17
|
+
def store_block_requests
|
18
|
+
store_all_but_last_piece
|
19
|
+
store_last_piece
|
20
|
+
store_last_block
|
21
|
+
end
|
22
|
+
|
23
|
+
def init_requests
|
24
|
+
@peers.each do |peer|
|
25
|
+
NUM_PENDING.times { assign_request(peer, @all_block_requests.pop) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def store_all_but_last_piece
|
30
|
+
0.upto(num_pieces - 2).each do |piece_num|
|
31
|
+
0.upto(num_blocks_in_piece - 1).each do |block_num|
|
32
|
+
store_request(piece_num, block_offset(block_num), BLOCK_SIZE)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def store_last_piece
|
38
|
+
0.upto(num_full_blocks_in_last_piece - 1) do |block_num|
|
39
|
+
store_request(num_pieces - 1, block_offset(block_num), BLOCK_SIZE)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def store_last_block
|
44
|
+
store_request(num_pieces - 1, last_block_offset, last_block_size)
|
45
|
+
end
|
46
|
+
|
47
|
+
def store_request(index, offset, size)
|
48
|
+
@all_block_requests.push(create_block(index, offset, size))
|
49
|
+
end
|
50
|
+
|
51
|
+
def assign_request(peer, block)
|
52
|
+
peer.pending_requests << block
|
53
|
+
@request_queue.push(assign_peer(peer, block))
|
54
|
+
end
|
55
|
+
|
56
|
+
def pipe(incoming_block)
|
57
|
+
request = get_next_request(incoming_block)
|
58
|
+
enqueue_request(incoming_block, request) if request
|
59
|
+
end
|
60
|
+
|
61
|
+
def get_next_request(block)
|
62
|
+
if @all_block_requests.empty?
|
63
|
+
oldest_pending_request(block)
|
64
|
+
else
|
65
|
+
@all_block_requests.pop
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def enqueue_request(incoming_block, request)
|
70
|
+
incoming_block.peer.pending_requests << request
|
71
|
+
@request_queue.push(assign_peer(incoming_block.peer, request))
|
72
|
+
end
|
73
|
+
|
74
|
+
def oldest_pending_request(block)
|
75
|
+
slowest_peer = @peers.sort_by{|peer| peer.pending_requests.length}.first
|
76
|
+
slowest_peer.pending_requests.last
|
77
|
+
end
|
78
|
+
|
79
|
+
def assign_peer(peer, block)
|
80
|
+
{ connection: peer.connection,
|
81
|
+
index: block[:index],
|
82
|
+
offset: block[:offset],
|
83
|
+
size: block[:size] }
|
84
|
+
end
|
85
|
+
|
86
|
+
def create_block(index, offset, size)
|
87
|
+
{ index: index, offset: offset, size: size }
|
88
|
+
end
|
89
|
+
|
90
|
+
def num_pieces
|
91
|
+
(@metainfo.total_size.to_f/@metainfo.piece_length).ceil
|
92
|
+
end
|
93
|
+
|
94
|
+
def last_block_size
|
95
|
+
@metainfo.total_size.remainder(BLOCK_SIZE)
|
96
|
+
end
|
97
|
+
|
98
|
+
def num_full_blocks
|
99
|
+
@metainfo.total_size/BLOCK_SIZE
|
100
|
+
end
|
101
|
+
|
102
|
+
def last_piece_size
|
103
|
+
file_size - (@metainfo.piece_length * (@metainfo.number_of_pieces - 1))
|
104
|
+
end
|
105
|
+
|
106
|
+
def num_blocks_in_piece
|
107
|
+
(@metainfo.piece_length.to_f/BLOCK_SIZE).ceil
|
108
|
+
end
|
109
|
+
|
110
|
+
def num_full_blocks_in_last_piece
|
111
|
+
num_full_blocks.remainder(num_blocks_in_piece)
|
112
|
+
end
|
113
|
+
|
114
|
+
def total_num_blocks_in_last_piece
|
115
|
+
num_full_blocks_in_last_piece + 1
|
116
|
+
end
|
117
|
+
|
118
|
+
def last_block_offset
|
119
|
+
BLOCK_SIZE * num_full_blocks_in_last_piece
|
120
|
+
end
|
121
|
+
|
122
|
+
def last_block_offset
|
123
|
+
BLOCK_SIZE * num_full_blocks_in_last_piece
|
124
|
+
end
|
125
|
+
|
126
|
+
def block_offset(block_num)
|
127
|
+
BLOCK_SIZE * block_num
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
data/bin/byte_array.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
class ByteArray
|
2
|
+
|
3
|
+
def initialize(meta_info)
|
4
|
+
@length = meta_info.total_size
|
5
|
+
@bytes = Array.new([[0, @length - 1, false]])
|
6
|
+
end
|
7
|
+
|
8
|
+
def have_all(start, fin)
|
9
|
+
check_range(start,fin)
|
10
|
+
start_item, end_item = boundry_items(start, fin)
|
11
|
+
|
12
|
+
start_index = @bytes.index(start_item)
|
13
|
+
end_index = @bytes.index(end_item)
|
14
|
+
|
15
|
+
result = Array.new(3,nil)
|
16
|
+
first, second, third = nil
|
17
|
+
|
18
|
+
if start_item[2] and end_item[2]
|
19
|
+
result[0] = [start_item[0], end_item[1], true]
|
20
|
+
elsif start_item[2]
|
21
|
+
result[0] = [start_item[0], fin, start_item[2]]
|
22
|
+
result[1] = [fin + 1, end_item[1], end_item[2]]
|
23
|
+
elsif end_item[2]
|
24
|
+
result[0] = [start_item[0], start - 1, start_item[2]]
|
25
|
+
result[1] = [start, end_item[1], true]
|
26
|
+
else
|
27
|
+
result[0] = [start_item[0], start - 1, start_item[2]]
|
28
|
+
result[1] = [start, fin, true]
|
29
|
+
result[2] = [fin + 1, end_item[1], end_item[2]]
|
30
|
+
end
|
31
|
+
|
32
|
+
result.map! do |item|
|
33
|
+
unless item.nil?
|
34
|
+
item = nil if item[0] > item[1]
|
35
|
+
end
|
36
|
+
item
|
37
|
+
end
|
38
|
+
|
39
|
+
@bytes[start_index..end_index] = result.compact
|
40
|
+
consolidate!
|
41
|
+
@bytes
|
42
|
+
end
|
43
|
+
|
44
|
+
def consolidate!
|
45
|
+
0.upto(@bytes.length - 2).each do |n|
|
46
|
+
if @bytes[n][2] == @bytes[n+1][2]
|
47
|
+
@bytes[n+1][0] = @bytes[n][0]
|
48
|
+
@bytes[n] = nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
@bytes.compact!
|
52
|
+
end
|
53
|
+
|
54
|
+
def boundry_items(start, fin)
|
55
|
+
start_item, end_item = nil
|
56
|
+
@bytes.each_with_index do |element, index|
|
57
|
+
start_item = @bytes[index] if start.between?(element[0],element[1])
|
58
|
+
end_item = @bytes[index] if fin.between?(element[0],element[1])
|
59
|
+
end
|
60
|
+
[start_item, end_item]
|
61
|
+
end
|
62
|
+
|
63
|
+
def have_all?(start, fin)
|
64
|
+
check_range(start, fin)
|
65
|
+
@bytes.each do |i, j, bool|
|
66
|
+
if bool == false
|
67
|
+
if intersect?(start, fin, i, j)
|
68
|
+
return false
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
def intersect?(start, fin, i, j)
|
76
|
+
start.between?(i, j) ||
|
77
|
+
fin.between?(i,j) ||
|
78
|
+
i.between?(start, fin) ||
|
79
|
+
j.between?(start, fin)
|
80
|
+
end
|
81
|
+
|
82
|
+
def complete?
|
83
|
+
@bytes == [[0, @length - 1, true]]
|
84
|
+
end
|
85
|
+
|
86
|
+
def check_range(start,fin)
|
87
|
+
if start < 0 or
|
88
|
+
fin < 0 or
|
89
|
+
start > @length - 1 or
|
90
|
+
fin > @length - 1 or
|
91
|
+
start > fin
|
92
|
+
raise "Byte Array: out of range"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/bin/client.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
class Client
|
2
|
+
|
3
|
+
# set queues and set processes in different methods
|
4
|
+
|
5
|
+
def initialize(path_to_file, download_folder)
|
6
|
+
@metainfo = parse_metainfo(File.open(path_to_file), download_folder)
|
7
|
+
@tracker = Tracker.new(@metainfo.announce)
|
8
|
+
@id = rand_id # TODO: assign meaningful id
|
9
|
+
@peers = []
|
10
|
+
set_peers
|
11
|
+
@scheduler = BlockRequestScheduler.new(@peers, @metainfo)
|
12
|
+
@message_queue = Queue.new
|
13
|
+
@incoming_block_queue = Queue.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse_metainfo(torrent_file, download_folder)
|
17
|
+
metainfo = BEncode::Parser.new(torrent_file).parse!
|
18
|
+
MetaInfo.new(metainfo, download_folder)
|
19
|
+
end
|
20
|
+
|
21
|
+
def rand_id
|
22
|
+
20.times.reduce("") { |a, _| a + rand(9).to_s }
|
23
|
+
end
|
24
|
+
|
25
|
+
def set_peers
|
26
|
+
peers = @tracker.make_request(tracker_request_params)["peers"].scan(/.{6}/)
|
27
|
+
get_unpacked_peers(peers).each do |ip_string, port|
|
28
|
+
set_peer(ip_string, port)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def set_peer(ip_string, port)
|
33
|
+
begin
|
34
|
+
handshake = "\x13BitTorrent protocol\x00\x00\x00\x00\x00\x00\x00\x00#{@metainfo.info_hash}#{@id}"
|
35
|
+
Timeout::timeout(1) { @peers << Peer.new(ip_string, port, handshake, @metainfo.info_hash) }
|
36
|
+
rescue => exception
|
37
|
+
puts exception
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def tracker_request_params
|
42
|
+
{ info_hash: @metainfo.info_hash,
|
43
|
+
peer_id: @id,
|
44
|
+
port: '6881',
|
45
|
+
uploaded: '0',
|
46
|
+
downloaded: '0',
|
47
|
+
left: '10000',
|
48
|
+
compact: '1',
|
49
|
+
no_peer_id: '0',
|
50
|
+
event: 'started' }
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_unpacked_peers(peers)
|
54
|
+
peers.map {|p| p.unpack('a4n') }
|
55
|
+
end
|
56
|
+
|
57
|
+
def run!
|
58
|
+
Thread::abort_on_exception = true
|
59
|
+
@peers.each { |peer| peer.start!(@message_queue) }
|
60
|
+
make_threads([scheduler, incoming_message, file_handler])
|
61
|
+
end
|
62
|
+
|
63
|
+
def scheduler
|
64
|
+
lambda { pipe(@scheduler.request_queue, BlockRequestProcess.new) }
|
65
|
+
end
|
66
|
+
|
67
|
+
def incoming_message
|
68
|
+
lambda do
|
69
|
+
pipe(@message_queue,
|
70
|
+
IncomingMessageProcess.new(@metainfo.piece_length),
|
71
|
+
@incoming_block_queue)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def file_handler
|
76
|
+
lambda do
|
77
|
+
multi_pipe(@incoming_block_queue,
|
78
|
+
FileHandler.new(@metainfo),
|
79
|
+
@scheduler)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def make_threads(processes)
|
84
|
+
processes.each do |process|
|
85
|
+
Thread.new { process.call }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def multi_pipe(input, *processors)
|
90
|
+
while m = input.pop
|
91
|
+
processors.each { |p| p.pipe(m) }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def pipe(input, processor, output=nil)
|
96
|
+
while m = input.pop
|
97
|
+
if output
|
98
|
+
processor.pipe(m, output)
|
99
|
+
else
|
100
|
+
processor.pipe(m)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
data/bin/file_handler.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
class FileHandler
|
4
|
+
include FileUtils
|
5
|
+
|
6
|
+
def initialize(metainfo)
|
7
|
+
@metainfo = metainfo
|
8
|
+
@byte_array = ByteArray.new(@metainfo)
|
9
|
+
@temp_name = "temp/" + ('a'..'z').to_a.shuffle.take(10).join
|
10
|
+
@file = init_file
|
11
|
+
@download_path = download_path
|
12
|
+
end
|
13
|
+
|
14
|
+
def init_file
|
15
|
+
Dir.mkdir("temp") unless File.directory?("temp")
|
16
|
+
File.open(@temp_name, "w+")
|
17
|
+
File.open(@temp_name, "r+")
|
18
|
+
end
|
19
|
+
|
20
|
+
def pipe(block)
|
21
|
+
write_block(block)
|
22
|
+
record_block(block)
|
23
|
+
|
24
|
+
piece_start = @metainfo.pieces[block.piece_index].start_byte
|
25
|
+
piece_end = @metainfo.pieces[block.piece_index].end_byte
|
26
|
+
|
27
|
+
if @byte_array.have_all?(piece_start, piece_end)
|
28
|
+
verify_piece(block.piece_index)
|
29
|
+
end
|
30
|
+
|
31
|
+
finish if @byte_array.complete?
|
32
|
+
end
|
33
|
+
|
34
|
+
def write_block(block)
|
35
|
+
@file.seek(block.start_byte)
|
36
|
+
@file.write(block.data)
|
37
|
+
end
|
38
|
+
|
39
|
+
def record_block(block)
|
40
|
+
@byte_array.have_all(block.start_byte, block.end_byte)
|
41
|
+
end
|
42
|
+
|
43
|
+
def verify_piece(index)
|
44
|
+
piece = @metainfo.pieces[index]
|
45
|
+
if piece.hash == hash_from_file(piece)
|
46
|
+
puts "piece #{index} verified!"
|
47
|
+
else
|
48
|
+
puts "piece #{index} verification FAILED!"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def hash_from_file(piece)
|
53
|
+
Digest::SHA1.new.digest(read(piece.start_byte, piece.length))
|
54
|
+
end
|
55
|
+
|
56
|
+
def read(start, length)
|
57
|
+
@file.seek(start)
|
58
|
+
@file.read(length)
|
59
|
+
end
|
60
|
+
|
61
|
+
def finish
|
62
|
+
puts "finishing!"
|
63
|
+
@file.close
|
64
|
+
make_download_dir
|
65
|
+
if @metainfo.is_multi_file?
|
66
|
+
split_files
|
67
|
+
remove_temp_file
|
68
|
+
else
|
69
|
+
move_file
|
70
|
+
end
|
71
|
+
abort("File download successful!")
|
72
|
+
end
|
73
|
+
|
74
|
+
def split_files
|
75
|
+
File.open(@temp_name, "r") do |temp_file|
|
76
|
+
@metainfo.files.each do |file_info|
|
77
|
+
File.open(@download_path + file_info[:name], "w") do |out_file|
|
78
|
+
out_file.write(temp_file.read(file_info[:length]))
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def move_file
|
85
|
+
FileUtils.mv(@temp_name, @download_path + @metainfo.files[0][:name])
|
86
|
+
end
|
87
|
+
|
88
|
+
def make_download_dir
|
89
|
+
unless File.directory?(@download_path)
|
90
|
+
Dir.mkdir(@download_path)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def remove_temp_file
|
95
|
+
File.delete(@temp_name)
|
96
|
+
end
|
97
|
+
|
98
|
+
def download_path
|
99
|
+
if @metainfo.download_folder[-1] == "/"
|
100
|
+
return @metainfo.download_folder
|
101
|
+
end
|
102
|
+
@metainfo.download_folder + "/"
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class IncomingMessageProcess
|
2
|
+
def initialize(piece_length)
|
3
|
+
@piece_length = piece_length
|
4
|
+
end
|
5
|
+
|
6
|
+
def pipe(message, output)
|
7
|
+
puts "Peer #{message.peer.id} has sent you a #{message.type} message"
|
8
|
+
case message.type
|
9
|
+
when :piece
|
10
|
+
piece_index, byte_offset, block_data = split_piece_payload(message.payload)
|
11
|
+
block = Block.new(piece_index,
|
12
|
+
byte_offset,
|
13
|
+
block_data,
|
14
|
+
@piece_length,
|
15
|
+
message.peer)
|
16
|
+
remove_from_pending(block)
|
17
|
+
output.push(block)
|
18
|
+
when :choking
|
19
|
+
message.peer.state[:is_choking] = true
|
20
|
+
when :unchoke
|
21
|
+
message.peer.state[:is_choking] = false
|
22
|
+
when :not_interested
|
23
|
+
message.peer.state[:is_interested] = false
|
24
|
+
when :interested
|
25
|
+
message.peer.state[:is_interested] = true
|
26
|
+
when :have
|
27
|
+
message.peer.bitfield.have_piece(message.payload.unpack("N")[0])
|
28
|
+
puts "have #{message.peer.bitfield}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def remove_from_pending(block)
|
35
|
+
block.peer.pending_requests.delete_if do |req|
|
36
|
+
if req
|
37
|
+
req[:index] == block.piece_index and
|
38
|
+
req[:offset] == block.offset
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def split_piece_payload(payload)
|
44
|
+
piece_index = payload.slice!(0..3).unpack("N")[0]
|
45
|
+
byte_offset = payload.slice!(0..3).unpack("N")[0]
|
46
|
+
block_data = payload
|
47
|
+
[piece_index, byte_offset, block_data]
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
data/bin/message.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
class Message
|
2
|
+
|
3
|
+
MESSAGE_TYPES = { "-1" => :keep_alive,
|
4
|
+
"0" => :choke,
|
5
|
+
"1" => :unchoke,
|
6
|
+
"2" => :interested,
|
7
|
+
"3" => :not_interested,
|
8
|
+
"4" => :have,
|
9
|
+
"5" => :bitfield,
|
10
|
+
"6" => :request,
|
11
|
+
"7" => :piece,
|
12
|
+
"8" => :cancel,
|
13
|
+
"9" => :port }
|
14
|
+
|
15
|
+
attr_accessor :peer, :length, :type, :payload
|
16
|
+
|
17
|
+
def initialize(peer, length, id, payload)
|
18
|
+
@peer = peer
|
19
|
+
@length = length
|
20
|
+
@type = MESSAGE_TYPES[id.to_s]
|
21
|
+
@payload = payload
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.has_payload?(id)
|
25
|
+
# message ids associated with payload
|
26
|
+
/[456789]/.match(id)
|
27
|
+
end
|
28
|
+
|
29
|
+
def print
|
30
|
+
"index: #{ self.payload[0..3].unpack("N")}, offset: #{self.payload[4..8].unpack("N") }"
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.parse_stream(peer, message_queue)
|
34
|
+
loop do
|
35
|
+
begin
|
36
|
+
length = peer.connection.read(4).unpack("N")[0]
|
37
|
+
id = length.zero? ? "-1" : peer.connection.readbyte.to_s
|
38
|
+
payload = has_payload?(id) ? peer.connection.read(length - 1) : nil
|
39
|
+
message_queue << self.new(peer, length, id, payload)
|
40
|
+
rescue => exception
|
41
|
+
puts exception
|
42
|
+
break
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.send_interested(peer)
|
48
|
+
length = "\0\0\0\1"
|
49
|
+
id = "\2"
|
50
|
+
peer.connection.write(length + id)
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.send_have(peers, index)
|
54
|
+
length = "\0\0\0\5"
|
55
|
+
id = "\4"
|
56
|
+
piece_index = [index].pack("N")
|
57
|
+
peers.each do |peer|
|
58
|
+
peer.connection.write(length + id + piece_index)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
data/bin/meta_info.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
class MetaInfo
|
2
|
+
|
3
|
+
attr_accessor :info_hash, :announce, :number_of_pieces,
|
4
|
+
:pieces, :files, :total_size, :piece_length,
|
5
|
+
:pieces_hash, :folder, :download_folder
|
6
|
+
|
7
|
+
def initialize(meta_info, download_folder)
|
8
|
+
@info = meta_info["info"]
|
9
|
+
@download_folder = download_folder
|
10
|
+
@piece_length = @info["piece length"]
|
11
|
+
@info_hash = Digest::SHA1.new.digest(@info.bencode)
|
12
|
+
@pieces_hash = @info["pieces"]
|
13
|
+
@announce = meta_info["announce"]
|
14
|
+
@number_of_pieces = @info["pieces"].length/20
|
15
|
+
set_total_size
|
16
|
+
set_folder
|
17
|
+
set_files
|
18
|
+
set_pieces
|
19
|
+
end
|
20
|
+
|
21
|
+
def set_total_size
|
22
|
+
if is_multi_file?
|
23
|
+
set_multi_file_size
|
24
|
+
else
|
25
|
+
set_single_file_size
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def set_multi_file_size
|
30
|
+
@total_size = @info["files"].inject(0) do |start_byte, file|
|
31
|
+
start_byte + file["length"]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def set_single_file_size
|
36
|
+
@total_size = @info["length"]
|
37
|
+
end
|
38
|
+
|
39
|
+
def set_folder
|
40
|
+
@folder = is_multi_file? ? @info["name"] : nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def set_files
|
44
|
+
@files = []
|
45
|
+
if is_multi_file?
|
46
|
+
set_multi_files
|
47
|
+
else
|
48
|
+
add_file(@info["name"], @info["length"], 0, @info["length"] - 1)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def set_multi_files
|
53
|
+
@info["files"].inject(0) do |start_byte, file|
|
54
|
+
name, length, start_byte, end_byte = get_add_file_args(start_byte, file)
|
55
|
+
add_file(name, length, start_byte, end_byte)
|
56
|
+
start_byte + file["length"]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_add_file_args(start_byte, file)
|
61
|
+
name = file["path"][0]
|
62
|
+
length = file["length"]
|
63
|
+
start_byte = start_byte
|
64
|
+
end_byte = start_byte + file["length"] - 1
|
65
|
+
|
66
|
+
return name, length, start_byte, end_byte
|
67
|
+
end
|
68
|
+
|
69
|
+
def add_file(name, length, start, fin)
|
70
|
+
@files << { name: name, length: length, start_byte: start, end_byte: fin }
|
71
|
+
end
|
72
|
+
|
73
|
+
def set_pieces
|
74
|
+
@pieces = []
|
75
|
+
(0...@number_of_pieces).each do |index|
|
76
|
+
start_byte = index * @piece_length
|
77
|
+
end_byte = get_end_byte(start_byte, index)
|
78
|
+
hash = get_correct_hash(index)
|
79
|
+
@pieces << Piece.new(index, start_byte, end_byte, hash)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def get_end_byte(start_byte, index)
|
84
|
+
return @total_size - 1 if last_piece?(index)
|
85
|
+
start_byte + @piece_length - 1
|
86
|
+
end
|
87
|
+
|
88
|
+
def last_piece?(index)
|
89
|
+
index == @number_of_pieces - 1
|
90
|
+
end
|
91
|
+
|
92
|
+
def get_correct_hash(index)
|
93
|
+
@info["pieces"][20 * index...20 * (index+1)]
|
94
|
+
end
|
95
|
+
|
96
|
+
def is_multi_file?
|
97
|
+
!@info["files"].nil?
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
data/bin/peer.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
class Peer
|
2
|
+
|
3
|
+
attr_accessor :connection, :bitfield, :state, :id, :pending_requests
|
4
|
+
|
5
|
+
def initialize(ip_string, port, handshake, correct_info_hash)
|
6
|
+
@pending_requests = []
|
7
|
+
@connection = TCPSocket.new(IPAddr.new_ntoh(ip_string).to_s, port)
|
8
|
+
@state = { is_choking: true, is_choked: true, is_interested: false, is_interesting: false }
|
9
|
+
@correct_info_hash = correct_info_hash
|
10
|
+
greet(handshake)
|
11
|
+
set_bitfield
|
12
|
+
end
|
13
|
+
|
14
|
+
def greet(handshake)
|
15
|
+
@connection.write(handshake)
|
16
|
+
set_initial_response
|
17
|
+
verify_initial_response
|
18
|
+
@id = @initial_response[:peer_id]
|
19
|
+
end
|
20
|
+
|
21
|
+
def set_initial_response
|
22
|
+
pstrlen = @connection.getbyte
|
23
|
+
@initial_response = {
|
24
|
+
pstrlen: pstrlen,
|
25
|
+
pstr: @connection.read(pstrlen),
|
26
|
+
reserved: @connection.read(8),
|
27
|
+
info_hash: @connection.read(20),
|
28
|
+
peer_id: @connection.read(20)
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def verify_initial_response
|
33
|
+
disconnect unless @initial_response[:info_hash] == @correct_info_hash
|
34
|
+
end
|
35
|
+
|
36
|
+
def disconnect
|
37
|
+
@connection.close
|
38
|
+
end
|
39
|
+
|
40
|
+
def set_bitfield
|
41
|
+
length = @connection.read(4).unpack("N")[0]
|
42
|
+
message_id = @connection.read(1).bytes[0]
|
43
|
+
if message_id == 5
|
44
|
+
@bitfield = Bitfield.new(@connection.read(length - 1).unpack("B8" * (length - 1)))
|
45
|
+
else
|
46
|
+
puts "no bitfield!"
|
47
|
+
@bitfield = nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def start!(message_queue)
|
52
|
+
Thread.new { Message.parse_stream(self, message_queue) }
|
53
|
+
Thread.new { keep_alive }
|
54
|
+
Message.send_interested(self)
|
55
|
+
end
|
56
|
+
|
57
|
+
def keep_alive
|
58
|
+
loop do
|
59
|
+
begin
|
60
|
+
@connection.write("\0\0\0\0")
|
61
|
+
rescue
|
62
|
+
puts "keep alive broken"
|
63
|
+
end
|
64
|
+
sleep(60)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/bin/piece.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
class Piece
|
2
|
+
|
3
|
+
attr_accessor :index, :start_byte, :end_byte, :length, :hash
|
4
|
+
|
5
|
+
def initialize(index, start_byte, end_byte, hash)
|
6
|
+
@index = index
|
7
|
+
@start_byte = start_byte
|
8
|
+
@end_byte = end_byte
|
9
|
+
@length = @end_byte - @start_byte + 1
|
10
|
+
@hash = hash
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
data/bin/tracker.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
class Tracker
|
4
|
+
|
5
|
+
def initialize(uri_string)
|
6
|
+
@uri = URI(uri_string)
|
7
|
+
end
|
8
|
+
|
9
|
+
def make_request(params)
|
10
|
+
request = @uri
|
11
|
+
request.query = URI.encode_www_form(params)
|
12
|
+
BEncode.load(Net::HTTP.get_response(request).body)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
data/bittorrent.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'bittorrent/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "bittorrent"
|
8
|
+
spec.version = Bittorrent::VERSION
|
9
|
+
spec.authors = ["Karl Coelho"]
|
10
|
+
spec.email = ["karl.coelho1@gmail.com"]
|
11
|
+
spec.summary = %q{Bittorrent Client written in Ruby.}
|
12
|
+
spec.description = %q{Bittorrent Client written in Ruby.}
|
13
|
+
spec.homepage = "http://github.com/karlcoelho/bittorrent"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
end
|
data/lib/bittorrent.rb
ADDED
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bittorrent
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Karl Coelho
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-06-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: Bittorrent Client written in Ruby.
|
42
|
+
email:
|
43
|
+
- karl.coelho1@gmail.com
|
44
|
+
executables:
|
45
|
+
- bitfield.rb
|
46
|
+
- bittorrent
|
47
|
+
- block.rb
|
48
|
+
- block_request_process.rb
|
49
|
+
- block_request_scheduler.rb
|
50
|
+
- byte_array.rb
|
51
|
+
- client.rb
|
52
|
+
- file_handler.rb
|
53
|
+
- incoming_message_process.rb
|
54
|
+
- message.rb
|
55
|
+
- meta_info.rb
|
56
|
+
- peer.rb
|
57
|
+
- piece.rb
|
58
|
+
- tracker.rb
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- .gitignore
|
63
|
+
- Gemfile
|
64
|
+
- README.md
|
65
|
+
- Rakefile
|
66
|
+
- bin/bitfield.rb
|
67
|
+
- bin/bittorrent
|
68
|
+
- bin/block.rb
|
69
|
+
- bin/block_request_process.rb
|
70
|
+
- bin/block_request_scheduler.rb
|
71
|
+
- bin/byte_array.rb
|
72
|
+
- bin/client.rb
|
73
|
+
- bin/file_handler.rb
|
74
|
+
- bin/incoming_message_process.rb
|
75
|
+
- bin/message.rb
|
76
|
+
- bin/meta_info.rb
|
77
|
+
- bin/peer.rb
|
78
|
+
- bin/piece.rb
|
79
|
+
- bin/tracker.rb
|
80
|
+
- bittorrent.gemspec
|
81
|
+
- lib/bittorrent.rb
|
82
|
+
- lib/bittorrent/version.rb
|
83
|
+
homepage: http://github.com/karlcoelho/bittorrent
|
84
|
+
licenses:
|
85
|
+
- MIT
|
86
|
+
metadata: {}
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options: []
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ! '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ! '>='
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
requirements: []
|
102
|
+
rubyforge_project:
|
103
|
+
rubygems_version: 2.2.0
|
104
|
+
signing_key:
|
105
|
+
specification_version: 4
|
106
|
+
summary: Bittorrent Client written in Ruby.
|
107
|
+
test_files: []
|