├── .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 --------------------------------------------------------------------------------