bittorrent 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []