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.
@@ -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=
@@ -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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in bittorrent.gemspec
4
+ gemspec
@@ -0,0 +1,11 @@
1
+ # Bittorrent
2
+
3
+ Bittorrent client written in Ruby.
4
+
5
+ ## Usage
6
+
7
+ ```
8
+ bittorrent [TORRENT_FILE] [DOWNLOAD_FOLDER]
9
+ ```
10
+
11
+ [follow me on twitter](http://twitter.com/therealcoelho)
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,14 @@
1
+ class Bitfield
2
+
3
+ attr_accessor :bits
4
+
5
+ def initialize(*bit_array)
6
+ @bits = bit_array.join.split('').map! { |char| char.to_i }
7
+ p @bits
8
+ end
9
+
10
+ def have_piece(index)
11
+ @bits[index] = 1
12
+ end
13
+
14
+ end
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ require "bittorrent/version"
2
+
3
+ module Bittorrent
4
+ # Your code goes here...
5
+ end
@@ -0,0 +1,3 @@
1
+ module Bittorrent
2
+ VERSION = "0.0.2"
3
+ end
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: []