├── .gitignore
├── .gitmodules
├── README.md
├── lib
├── ruby_torrent.rb
└── ruby_torrent
│ ├── bitfield.rb
│ ├── block.rb
│ ├── block_request_process.rb
│ ├── block_request_scheduler.rb
│ ├── byte_array.rb
│ ├── client.rb
│ ├── file_handler.rb
│ ├── incoming_message_process.rb
│ ├── message.rb
│ ├── meta_info.rb
│ ├── peer.rb
│ ├── piece.rb
│ ├── tracker.rb
│ └── utils.rb
└── test_torrents
├── cars.torrent
├── flagfromserver.torrent
├── piy.torrent
└── robba.torrent
/.gitignore:
--------------------------------------------------------------------------------
1 | junk/*
2 | downloads/*
3 | mininova/*
4 | temp/*
5 | old/*
6 | mininova/*
7 | mary_notes
8 | .DS_Store
9 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "ruby-bencode"]
2 | path = lib/ruby_torrent/ruby-bencode
3 | url = https://github.com/dasch/ruby-bencode
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | RubyTorrent
2 | ===========
3 |
4 | A simple BitTorrent client in Ruby.
5 | This client can download single- or multi-file torrents from multiple peers concurrently.
6 |
7 | INSTALL
8 | ----
9 | Clone RubyTorrent locally.
10 | CD into the RubyTorrent directory and run the following commands:
11 |
12 | ```
13 | git submodule init
14 | git submodule update
15 | ```
16 |
17 | RUN
18 | ----
19 | To download a file using RubyTorrent, run the following from the root directory:
20 |
21 | ```
22 | ruby lib/ruby_torrent.rb
23 | ```
24 | Example:
25 | ```
26 | ruby lib/ruby_torrent.rb test_torrents/flagfromserver.torrent downloads/
27 | ```
28 |
29 |
30 | REQUIREMENTS
31 | ----
32 | Ruby 2.0
33 | The torrent file must have a reference to a tracker. DTH torrent files are not yet supported.
34 |
--------------------------------------------------------------------------------
/lib/ruby_torrent.rb:
--------------------------------------------------------------------------------
1 | require 'net/http'
2 | require 'digest/sha1'
3 | require 'thread'
4 | require 'ipaddr'
5 | require 'socket'
6 | require 'timeout'
7 | require 'pp'
8 |
9 | require_relative 'ruby_torrent/ruby-bencode/lib/bencode.rb'
10 | require_relative 'ruby_torrent/client'
11 | require_relative 'ruby_torrent/piece'
12 | require_relative 'ruby_torrent/block'
13 | require_relative 'ruby_torrent/file_handler'
14 | require_relative 'ruby_torrent/meta_info'
15 | require_relative 'ruby_torrent/block_request_scheduler'
16 | require_relative 'ruby_torrent/block_request_process'
17 | require_relative 'ruby_torrent/byte_array'
18 | require_relative 'ruby_torrent/incoming_message_process'
19 | require_relative 'ruby_torrent/tracker'
20 | require_relative 'ruby_torrent/peer'
21 | require_relative 'ruby_torrent/bitfield'
22 | require_relative 'ruby_torrent/message'
23 | require_relative 'ruby_torrent/utils'
24 |
25 | include Utils
26 |
27 | torrent = ARGV[0]
28 | download_path = ARGV[1]
29 |
30 | client = Client.new(torrent, download_path)
31 | client.run!
32 |
33 | Utils::join_threads
34 |
--------------------------------------------------------------------------------
/lib/ruby_torrent/bitfield.rb:
--------------------------------------------------------------------------------
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
--------------------------------------------------------------------------------
/lib/ruby_torrent/block.rb:
--------------------------------------------------------------------------------
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 |
31 |
--------------------------------------------------------------------------------
/lib/ruby_torrent/block_request_process.rb:
--------------------------------------------------------------------------------
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
18 |
--------------------------------------------------------------------------------
/lib/ruby_torrent/block_request_scheduler.rb:
--------------------------------------------------------------------------------
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
131 |
--------------------------------------------------------------------------------
/lib/ruby_torrent/byte_array.rb:
--------------------------------------------------------------------------------
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
96 |
--------------------------------------------------------------------------------
/lib/ruby_torrent/client.rb:
--------------------------------------------------------------------------------
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
105 |
--------------------------------------------------------------------------------
/lib/ruby_torrent/file_handler.rb:
--------------------------------------------------------------------------------
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
106 |
--------------------------------------------------------------------------------
/lib/ruby_torrent/incoming_message_process.rb:
--------------------------------------------------------------------------------
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
51 |
--------------------------------------------------------------------------------
/lib/ruby_torrent/message.rb:
--------------------------------------------------------------------------------
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
--------------------------------------------------------------------------------
/lib/ruby_torrent/meta_info.rb:
--------------------------------------------------------------------------------
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
101 |
--------------------------------------------------------------------------------
/lib/ruby_torrent/peer.rb:
--------------------------------------------------------------------------------
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
68 |
--------------------------------------------------------------------------------
/lib/ruby_torrent/piece.rb:
--------------------------------------------------------------------------------
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
--------------------------------------------------------------------------------
/lib/ruby_torrent/tracker.rb:
--------------------------------------------------------------------------------
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
16 |
--------------------------------------------------------------------------------
/lib/ruby_torrent/utils.rb:
--------------------------------------------------------------------------------
1 | module Utils
2 |
3 | def join_threads
4 | Thread.list.each { |thread| thread.join unless current_thread?(thread) }
5 | end
6 |
7 | def current_thread?(thread)
8 | thread == Thread.current
9 | end
10 |
11 | end
12 |
13 |
--------------------------------------------------------------------------------
/test_torrents/cars.torrent:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willchapin/RubyTorrent/d105a0bd4fc57f3eda31b93771293a5038d4e8e0/test_torrents/cars.torrent
--------------------------------------------------------------------------------
/test_torrents/flagfromserver.torrent:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willchapin/RubyTorrent/d105a0bd4fc57f3eda31b93771293a5038d4e8e0/test_torrents/flagfromserver.torrent
--------------------------------------------------------------------------------
/test_torrents/piy.torrent:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willchapin/RubyTorrent/d105a0bd4fc57f3eda31b93771293a5038d4e8e0/test_torrents/piy.torrent
--------------------------------------------------------------------------------
/test_torrents/robba.torrent:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willchapin/RubyTorrent/d105a0bd4fc57f3eda31b93771293a5038d4e8e0/test_torrents/robba.torrent
--------------------------------------------------------------------------------