bittorrent 0.0.2
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 +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: []
|