rubytorrent-0.3/0000755000175000017500000000000011756644133013343 5ustar boutilboutilrubytorrent-0.3/dump-metainfo.rb0000644000175000017500000000320011756644133016430 0ustar boutilboutil## dump-metainfo.rb -- command-line .torrent dumper ## Copyright 2004 William Morgan. ## ## This file is part of RubyTorrent. RubyTorrent is free software; ## you can redistribute it and/or modify it under the terms of version ## 2 of the GNU General Public License as published by the Free ## Software Foundation. ## ## RubyTorrent is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License (in the file COPYING) for more details. require 'rubytorrent' def dump_metainfoinfo(mii) if mii.single? <" : mi.announce_list.map { |x| x.join(', ') }.join('; '))} creation date: #{mi.creation_date || ""} created by: #{mi.created_by || ""} comment: #{mi.comment || ""} EOS end if ARGV.length == 1 fn = ARGV[0] begin puts dump_metainfo(RubyTorrent::MetaInfo.from_location(fn)) rescue RubyTorrent::MetaInfoFormatError, RubyTorrent::BEncodingError => e puts "Can't parse #{fn}: maybe not a .torrent file?" end else puts "Usage: dump-metainfo " end rubytorrent-0.3/dump-peers.rb0000644000175000017500000000270011756644133015750 0ustar boutilboutil## dump-peers.rb -- command-line peer lister ## Copyright 2004 William Morgan. ## ## This file is part of RubyTorrent. RubyTorrent is free software; ## you can redistribute it and/or modify it under the terms of version ## 2 of the GNU General Public License as published by the Free ## Software Foundation. ## ## RubyTorrent is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License (in the file COPYING) for more details. require "rubytorrent" def die(x); $stderr << "#{x}\n" && exit(-1); end def dump_peer(p) "#{(p.peer_id.nil? ? '' : p.peer_id.inspect)} on #{p.ip}:#{p.port}" end fn = ARGV.shift or raise "first argument must be .torrent file" mi = nil begin mi = RubyTorrent::MetaInfo.from_location(fn) rescue RubyTorrent::MetaInfoFormatError, RubyTorrent::BEncodingError => e die "error parsing metainfo file #{fn}---maybe not a .torrent?" end # complete abuse mi.trackers.each do |track| puts "#{track}:" tc = RubyTorrent::TrackerConnection.new(track, mi.info.sha1, mi.info.total_length, 9999, "rubytorrent.dumppeer") # complete abuse, i know begin tc.force_refresh puts "" if tc.peers.length == 0 tc.peers.each do |p| puts dump_peer(p) end rescue RubyTorrent::TrackerError => e puts "error connecting to tracker: #{e.message}" end end rubytorrent-0.3/metadata.yml0000644000175000017500000000303011756644133015642 0ustar boutilboutil--- !ruby/object:Gem::Specification name: rubytorrent version: !ruby/object:Gem::Version version: "0.3" platform: ruby authors: - William Morgan autorequire: bindir: bin cert_chain: [] date: 2008-01-12 00:00:00 -08:00 default_executable: dependencies: [] description: A Bittorrent library. email: wmorgan-rubytorrent@masanjin.net executables: [] extensions: [] extra_rdoc_files: - COPYING - README - doc/api.txt - doc/design.txt - ReleaseNotes.txt files: - ./rtpeer.rb - ./rtpeer-ncurses.rb - ./lib/rubytorrent.rb - ./lib/rubytorrent/message.rb - ./lib/rubytorrent/peer.rb - ./lib/rubytorrent/metainfo.rb - ./lib/rubytorrent/controller.rb - ./lib/rubytorrent/util.rb - ./lib/rubytorrent/package.rb - ./lib/rubytorrent/tracker.rb - ./lib/rubytorrent/typedstruct.rb - ./lib/rubytorrent/bencoding.rb - ./lib/rubytorrent/server.rb - ./dump-peers.rb - ./dump-metainfo.rb - ./make-metainfo.rb - COPYING - README - doc/api.txt - doc/design.txt - ReleaseNotes.txt has_rdoc: true homepage: http://rubytorrent.rubyforge.org post_install_message: rdoc_options: - --main - README require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: "0" version: required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: "0" version: requirements: [] rubyforge_project: rubytorrent rubygems_version: 1.0.1 signing_key: specification_version: 2 summary: A Bittorrent librar.y test_files: [] rubytorrent-0.3/doc/0000755000175000017500000000000011756644133014110 5ustar boutilboutilrubytorrent-0.3/doc/api.txt0000644000175000017500000002237011756644133015426 0ustar boutilboutilRubyTorrent Documentation Introduction ------------ RubyTorrent is a pure-Ruby BitTorrent library. You can use RubyTorrent in your Ruby applications to download and serve files via the BitTorrent protocol. More information about BitTorrent can be found at http://bittorrent.com/. There's a lot going behind the scenes, but using this library is pretty simple: on the surface, RubyTorrent simply lets you download a file or set of files, given an initial .torrent filename or URL. I recommend you take a look at rtpeer.rb for an example Ruby BitTorrent peer that uses all this stuff. Synopsis -------- require "rubytorrent" # simple bt = RubyTorrent::BitTorrent.new(filename) bt.on_event(self, :complete) { puts "done!" } # more complex mi = RubyTorrent::MetaInfo.from_location(url) package = RubyTorrent::Package.new(mi, dest) bt = RubyTorrent::BitTorrent.new(mi, package) thread = Thread.new do until bt.complete? puts "#{bt.percent_completed}% done" sleep 5 end end bt.on_event(self, :complete) { puts "done!" } thread.join Overview -------- There are three top-level classes you should be familiar with in the RubyTorrent module: BitTorrent, MetaInfo and Package. BitTorrent is the main interface; MetaInfo and Package classes allow you more control over the details, but they're completely options and the BitTorrent class will do reasonable things if you don't use them. RubyTorrent has a very event-driven interface; all methods are non-blocking and the BitTorrent class generates notifications of all interesting events, which you can subscribe to. See the documentation on BitTorrent#on_event() below for how to subscribe to events. RubyTorrent::MetaInfo --------------------- This class represents the contents of the .torrent file or URL. CLASS METHODS from_location(location, http_proxy=ENV["http_proxy"]) Creates a MetaInfo object from a filename or URL. Arguments: location: a filename or a URL of a .torrent file. http_proxy: is the HTTP proxy to be used in the case that "location" is a URL (nil for none). Returns: A MetaInfo object. Throws: RubyTorrent::MetaInfoFormatError, RubyTorrent::BEncodingError, RubyTorrent::TypedStructError if the contents of the file/url are not a BitTorrent metainfo file. IOError, SystemCallError if reading the contents of the file/url failed for system-level issues. from_stream(stream) Creates a MetaInfo object from a readable IO stream. Arguments: stream: a readable IO stream, e.g. an opened File. Returns: A MetaInfo object. Throws: same as RubyTorrent::Metainfo#from_location INSTANCE METHODS single? Returns true if this .torrent contains a single file, false otherwise. multiple? The opposite of single? RubyTorrent::Package -------------------- This class represents the target file or files on disk. CLASS METHODS new(info, out=nil, validity_assumption=nil, path_sep="/") # optional block Creates a Package. Arguments: info: a MetaInfo object. out: if info.single?, this should be a File object corresponding to the target file on disk. If info.multiple?, this should be a Dir object corresponding to the target directory on disk. If nil, the original filename (for single-file .torrents) or the current directory (for multi-file .torrents) will be used. validity_assumption: if nil, make no assumptions about the validity of any files on disk. If true, assume all files on disk are complete and valid. If false, assume all files on disk are incomplete and invalid. This can be used to speed up start time by skipping all examination of current disk contents: if you're just starting a download, you can use false; if you're serving a complete file or set of files, you can use true. path_sep: how to join path-name components to make paths. "/" should work on both Windows and Unix worlds; I'm not sure about other OSs. Block: If given, yields a "Piece" object when checking the files on disk. This object has complete?() and valid?() methods. This is really only useful for updating the user on the status of the Package creation, which can take a long time for large files (I/O time and SHA1 calculations). Throws: IOError, if the file access fails. RubyTorrent::BitTorrent ----------------------- The main BitTorrent peer protocol interface. CLASS METHODS new(metainfo, package=nil, :host, :port, :dlratelim, :ulratelim, :http_proxy) Creates a BitTorrent peer. Arguments (all symbol arguments are optional hash pseudo-keyword arguments): metainfo: a String, IO or MetaInfo object corresponding to a .torrent file. In the case of a String or IO object, a MetaInfo object will be implictly created with default arguments. package: a Package, or nil. In the case on nil, a new Package will be implicitly created with default arguments. :host: the host to report to the tracker, if the source IP address of the HTTP request is not correct (for weird IP masquerading issues, I suppose). :port: the port to report to the tracker, if the port the BitTorrent peer is listening on is not correct (likewise). :dlratelim: the download rate limit in bytes/sec. This limit right now is applied on a per-peer basis to the average download rate. In the future this might change to something stricter/more useful. :ulratelim: likewise, for the upload rate limit. :http_proxy: the http_proxy used for connecting to the tracker, or nil or unspecified for ENV["http_proxy"]. Throws: All of the exceptions thrown by MetaInfo.new and Package.new. INSTANCE METHODS running? Returns whether this client is running or not. ip Returns the IP address the client is bound to, as a String (possibly "0.0.0.0") port Returns the port the client is bound to. complete? Returns whether the file on disk is complete or not. bytes_completed Returns the number of bytes completed. total_bytes Returns the total number of bytes in the target file/fileset. percent_completed Returns the percent of bytes completed. pieces_completed Returns the number of BitTorrent "pieces" completed. num_pieces Returns the total number of BitTorrent pieces. tracker Returns the URL of the tracker being used, or nil if no tracker can be reached. num_possible_peers Returns the number of peers we've read from the tracker, or nil if no tracker can be reached. This is typically capped at 50. peer_info Returns an array of hashes, one per current peer, with the following symbols as keys: :name: the peer name (probably "ip address/port") :seed: true if the peer is a seed, false if it's a leecher :dlamt, :ulamt: the number of bytes downloaded from /uploaded to this peer :dlrate, :ulrate: the bytes/sec downloaded from/uploaded to this peer :pending_send, :pending_recv: the number of blocks pending for send/receive :interested, :peer_interested: who's interested in the other's pieces :choking, :peer_choking: who's choking whom :snubbing: whether we're snubbing this peer :we_desire, :they_desire: number of pieces one has that the other wants A lot of this stuff has to do with the internals of the BitTorrent wire protocol, so it's mainly useful for debugging. shutdown Shuts down this particular client. shutdown_all Shuts down all clients. on_event(who, *events) # mandatory block Registers a notification for one or more events. When one of the events occurs, the block will be called. The first argument to the block will be the source of the event (in this case a RubyTorrent::BitTorrent object); the other arguments are dependent on the block itself. Arguments: who: should be "self" events: one or more event symbols (see EVENTS below) unregister_events(who, *events) Unregisters event notifications. All blocks added with on_event(who, ...) will be removed if they have an event in "events". If "events" is nil, all blocks registered with on_event(who, ...) will be removed. Arguments: who: the same argument as was passed to on_event() events: one or more event symbols. EVENTS :trying_peer |source, peer| We're trying to connect to the peer "peer" (a String: "ip addr/port"). :forgetting_peer |source, peer| We're couldn't connect to the peer. :added_peer |source, peer| We successfully connected to the peer. :removed_peer |source, peer| We dropped our connection to the peer. :received_block |source, block, peer| We received a block "block" from peer. :sent_block |source, block, peer| We sent a block "block" to peer. :have_piece |source, piece| We've successfully downloaded a complete piece "piece". :discarded_piece |source, piece| We had to discard piece "piece" because of checksum errors :complete |source| We've downloaded the entire file! Hooray! :tracker_connected |source, url| We connected to tracker "url". :tracker_lost We couldn't connect to tracker "url" after previously having connected to it. COPYRIGHT --------- Copyright 2005 William Morgan Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2; with no Invariant Sections, no Front-Cover Texts, and no Back-Covers. A copy of the license may be found at http://www.gnu.org/licenses/fdl.html. rubytorrent-0.3/doc/design.txt0000644000175000017500000000525211756644133016126 0ustar boutilboutilRubyTorrent Design ------------------ This is pretty sketchy at the moment but it might help if you want to do some hacking. +---------+ +----------------+ disk <::::::::>| Package | /-| PeerConnection |<=== network ===> peer +---------+ | +----------------+ | | +---------+ +------------+ / +----------------+ | Tracker |--| Controller |----| PeerConnection |<=== network ===> peer +---------+ +------------+ \ +----------------+ / | . +--------+ / | . | Server |- | . +--------+ \ \ +---------+ | | Package |<:::::::> disk | +---------+ | | +---------+ \+------------+ +----------------+ | Tracker |--| Controller |----| PeerConnection |<=== network ===> peer +---------+ +------------+ \ +----------------+ | | +----------------+ |--| PeerConnection |<=== network ===> peer | +----------------+ . . . Each .torrent download is associated with a Package. A Package is composed of several Pieces, each corresponding to a BitTorrent piece. A Package provides simple aggregate operations over all the Pieces. Each Piece handles writing to and reading from disk (across potentially multiple file pointers), as well as dividing its data into one or more Blocks. Each Block is an in-memory section of a Piece and corresponds to the BitTorrent piece, transferrable across the network. One Server coordinates all BitTorrent downloads. It maintains several Controllers, one per .torrent download. The server handles all handshaking. It accepts incoming connections, shunting them to the appropriate Controller, and creates outgoing ones at the Controllers' behest. Each connection to a peer is maintained by a PeerConnection, which keeps track of the peer's state and the connection state. PeerConnections get empty Blocks from their Controller and send requests for them across the wire, and, upon receiving requests from the peer, get full Blocks from the Package and transmit them back. The Controller also keeps a Tracker object, which it uses to communicate with the tracker. PeerConnections are completely reactive, and are tightly integrated with their Controller. They rely on the Controller's heartbeat thread to trigger any time-dependent events, and also for propagating any messages to other peers. rubytorrent-0.3/rtpeer-ncurses.rb0000644000175000017500000002221211756644133016650 0ustar boutilboutil## rtpeer-ncurses.rb -- RubyTorrent ncurses BitTorrent peer. ## Copyright 2005 William Morgan. ## ## This file is part of RubyTorrent. RubyTorrent is free software; ## you can redistribute it and/or modify it under the terms of version ## 2 of the GNU General Public License as published by the Free ## Software Foundation. ## ## RubyTorrent is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License (in the file COPYING) for more details. require "rubygems" require "rubytorrent" require "ncurses" require "optparse" def die(x); $stderr << "#{x}\n" && exit(-1); end dlratelim = nil ulratelim = nil opts = OptionParser.new do |opts| opts.banner = %{Usage: rtpeer-ncurses [options] [] rtpeer-ncurses is a very simple ncurses-based BitTorrent peer. You can use it to download .torrents or to seed them. is a .torrent filename or URL. is a file or directory on disk. If not specified, the default value from will be used. [options] are: } opts.on("-l", "--log FILENAME", "Log events to FILENAME (for debugging)") do |fn| RubyTorrent::log_output_to(fn) end opts.on("-d", "--downlimit LIMIT", Integer, "Limit download rate to LIMIT kb/s") do |x| dlratelim = x * 1024 end opts.on("-u", "--uplimit LIMIT", Integer, "Limit upload rate to LIMIT kb/s") do |x| ulratelim = x * 1024 end opts.on_tail("-h", "--help", "Show this message") do puts opts exit end end opts.parse!(ARGV) proxy = ENV["http_proxy"] torrent = ARGV.shift or (puts opts; exit) dest = ARGV.shift class Numeric def to_sz if self < 1024 "#{self.round}b" elsif self < 1024 ** 2 "#{(self / 1024 ).round}k" elsif self < 1024 ** 3 sprintf("%.1fm", self.to_f / (1024 ** 2)) else sprintf("%.2fg", self.to_f / (1024 ** 3)) end end MIN = 60 HOUR = 60 * MIN DAY = 24 * HOUR def to_time if self < MIN sprintf("0:%02d", self) elsif self < HOUR sprintf("%d:%02d", self / MIN, self % MIN) elsif self < DAY sprintf("%d:%02d:%02d", self / HOUR, (self % HOUR) / MIN, (self % HOUR) % MIN) else sprintf("%dd %d:%02d:%02d", self / DAY, (self % DAY) / HOUR, ((self % DAY) % HOUR) / MIN, ((self % DAY) % HOUR) % MIN) end end end class NilClass def to_time; "--:--"; end def to_sz; "-"; end end class Display STALL_SECS = 15 attr_accessor :fn, :dest, :status,:dlamt, :ulamt, :dlrate, :ulrate, :conn_peers, :fail_peers, :untried_peers, :tracker, :errcount, :completed, :total, :rate, :use_rate def initialize(window) @window = window @need_update = true @fn = "" @dest = "" @status = "" @completed = 0 @total = 0 @dlamt = 0 @dlrate = 0 @ulamt = 0 @ulrate = 0 @rate = 0 @conn_peers = 0 @fail_peers = 0 @untried_peers = 0 @tracker = "not connected" @errcount = 0 @dlblocks = 0 @ulblocks = 0 @got_blocks = 0 @sent_blocks = 0 @last_got_block = nil @last_sent_block = nil @start_time = nil @use_rate = false end def got_block @got_blocks += 1 @last_got_block = Time.now end def sent_block @sent_blocks += 1 @last_sent_block = Time.now end def sigwinch_handler(sig = nil) @need_update = true end def start_timer @start_time = Time.now end def draw if @need_update update_size @window.erase end complete_width = [@cols - 23, 0].max complete_ticks = ((@completed.to_f / @total) * complete_width) elapsed = (@start_time ? Time.now - @start_time : nil) rate = (use_rate ? @rate : @dlrate) remaining = rate && (rate > 0 ? (@total - @completed).to_f / rate : nil) dlstall = @last_got_block && ((Time.now - @last_got_block) > STALL_SECS) ulstall = @last_sent_block && ((Time.now - @last_sent_block) > STALL_SECS) line = 1 [ "Contents: #@fn", " Dest: #@dest", "", " Status: #@status", "Progress: [" + ("#" * complete_ticks), " Time: elapsed #{elapsed.to_time}, remaining #{remaining.to_time}", "Download: #{@dlamt.to_sz} at #{dlstall ? '(stalled)' : @dlrate.to_sz + '/s'}", " Upload: #{@ulamt.to_sz} at #{ulstall ? '(stalled)' : @ulrate.to_sz + '/s'}", " Peers: connected to #@conn_peers (#@fail_peers failed, #@untried_peers untried)", " Tracker: #@tracker", " Errors: #@errcount", ].each do |s| break if line > @rows @window.mvaddnstr(line, 2, s + (" " * @cols), @cols - 4) line += 1 end ## progress bar tail @window.mvaddstr(5, @cols - 11, sprintf("] %.2f%% ", (@completed.to_f / @total) * 100.0)) @window.mvaddnstr(7, 31, "|" + ("#" * (@dlrate / 1024)) + (" " * @cols), @cols - 31 - 2) @window.mvaddnstr(8, 31, "|" + ('#' * (@ulrate / 1024)) + (" " * @cols), @cols - 31 - 2) @window.box(0,0) # @got_blocks -= 1 unless @got_blocks == 0 # @sent_blocks -= 1 unless @sent_blocks == 0 end private def update_size rows = [] cols = [] ## jesus CHRIST this is a shitty interface. @window.getmaxyx(rows, cols) @rows = rows[0] @cols = cols[0] @need_update = false end end begin mi = RubyTorrent::MetaInfo.from_location(torrent, proxy) rescue RubyTorrent::MetaInfoFormatError, RubyTorrent::BEncodingError => e die %{Error: can\'t parse metainfo file "#{torrent}"---maybe not a .torrent?} rescue RubyTorrent::TypedStructError => e $stderr << < e $stderr.puts %{Error: can't read file "#{torrent}": #{e.message}} exit end unless dest.nil? if FileTest.directory?(dest) && mi.info.single? dest = File.join(dest, mi.info.name) elsif FileTest.file?(dest) && mi.info.multiple? die %{Error: .torrent contains multiple files, but "#{dest}" is a single file (must be a directory)} end end def handle_any_input(display) case(Ncurses.getch()) when ?q, ?Q Ncurses.curs_set(1) Ncurses.endwin() exit when Ncurses::KEY_RESIZE display.sigwinch_handler end end Ncurses.initscr begin Ncurses.nl() Ncurses.noecho() Ncurses.curs_set(0) Ncurses.stdscr.nodelay(true) Ncurses.timeout(0) display = Display.new Ncurses.stdscr display.status = "checking file on disk..." display.dest = File.expand_path(dest || mi.info.name) + (mi.single? ? "" : "/") if mi.single? display.fn = "#{mi.info.name} (#{mi.info.length.to_sz} in one file)" else display.fn = "#{mi.info.name}/ (#{mi.info.total_length.to_sz} in #{mi.info.files.length} files)" end display.total = mi.info.num_pieces * mi.info.piece_length display.completed = 0 display.draw; Ncurses.refresh display.use_rate = true display.start_timer num_pieces = 0 start = Time.now every = 10 package = RubyTorrent::Package.new(mi, dest) do |piece| num_pieces += 1 if (num_pieces % every) == 0 display.completed = (num_pieces * mi.info.piece_length) display.rate = display.completed.to_f / (Time.now - start) handle_any_input display display.draw; Ncurses.refresh end end display.status = "starting peer..." display.use_rate = false display.draw; Ncurses.refresh bt = RubyTorrent::BitTorrent.new(mi, package, :http_proxy => proxy, :dlratelim => dlratelim, :ulratelim => ulratelim) connecting = true bt.on_event(self, :received_block) do |s, b, peer| display.got_block connecting = false end bt.on_event(self, :sent_block) do |s, b, peer| display.sent_block connecting = false end bt.on_event(self, :discarded_piece) { |s, p| display.errcount += 1 } bt.on_event(self, :tracker_connected) do |s, url| display.tracker = url display.untried_peers = bt.num_possible_peers end bt.on_event(self, :tracker_lost) { |s, url| display.tracker = "can't connect to #{url}" } bt.on_event(self, :forgetting_peer) { |s, p| display.fail_peers += 1 } bt.on_event(self, :removed_peer, :added_peer) do |s, p| if (display.conn_peers = bt.num_active_peers) == 0 connecting = true end end bt.on_event(self, :added_peer) { |s, p| display.conn_peers += 1 } bt.on_event(self, :trying_peer) { |s, p| display.untried_peers -= 1 unless display.untried_peers == 0 } display.total = bt.total_bytes display.start_timer while true handle_any_input(display) display.status = if bt.complete? "seeding (download complete)" elsif connecting "connecting to peers" else "downloading" end display.draw; Ncurses.refresh display.dlamt = bt.dlamt display.dlrate = bt.dlrate display.ulamt = bt.ulamt display.ulrate = bt.ulrate display.completed = bt.bytes_completed display.draw; Ncurses.refresh sleep(0.5) end ensure Ncurses.curs_set(1) Ncurses.endwin() end rubytorrent-0.3/COPYING0000644000175000017500000004313111756644133014400 0ustar boutilboutil GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. rubytorrent-0.3/README0000644000175000017500000000125611756644133014227 0ustar boutilboutilTry it out ---------- RubyTorrent is primarily a library. See doc/api.txt for an example of how to use it in your Ruby applications. There are also a few executable scripts for you to play around with. rtpeer.rb: downloads a BitTorrent package. rtpeer-ncurses.rb: a nicer, ncurses version of the same. (The standard Ruby curses library appears not to play nicely with Threads, so we can't use it.) dump-metainfo.rb: takes a .torrent metainfo file and spits out everything about it. make-metainfo.rb: creates a .torrent file. dump-peers.rb: takes a .torrent metainfo file, connects to the tracker, and displays all the peers. (hack.) -- William rubytorrent-0.3/ReleaseNotes.txt0000644000175000017500000000212011756644133016470 0ustar boutilboutilRelease notes for 0.3: Many more bug fixes. Speed is now basically comparable to Bram's client---at least in my limited experiments. The following are known issues with this release: - Ruby threads don't play well with curses. Non-blocking getch hangs. See [ruby-talk:130620]. So we use ncurses. - Ruby threads don't play well with TCP sockets on Windows. There is a 20-second *global* freeze every time an outgoing connection is made to a non-responsive host. See [ruby-talk:129578], [ruby-core:04364]. As you can imagine, this can be quite a performance hit in a program that can make potentially hundreds of such connections. In fact, it renders RubyTorrent almost useless on Windows. A patch exists (indeed, has existed for many months), and if I bug Matz maybe it'll get in to 1.8.3. :) - Ruby threads don't play well with writing data over TCP sockets. At least, that's what I glean from [ruby-talk:130480], and it might explain the occasional freezing behavior I see (3 to 30 seconds, sporadic) under heavy loads in Linux. Other than that :) everything works. I think. rubytorrent-0.3/rtpeer.rb0000644000175000017500000001155611756644133015201 0ustar boutilboutil## rtpeer.rb -- RubyTorrent line-mode BitTorrent peer. ## Copyright 2004 William Morgan. ## ## This file is part of RubyTorrent. RubyTorrent is free software; ## you can redistribute it and/or modify it under the terms of version ## 2 of the GNU General Public License as published by the Free ## Software Foundation. ## ## RubyTorrent is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License (in the file COPYING) for more details. require "rubytorrent" Thread.abort_on_exception = true # make debugging easier def die(x); $stderr << "#{x}\n" && exit(-1); end def syntax %{ Syntax: rtpeer <.torrent filename or URL> [] rtpeer is a very simple line-based BitTorrent peer. You can use it to download .torrents or to seed them, but it's mainly good for debugging. } end class Numeric def k; (self.to_f / 1024.0); end def f(format="0.0") sprintf("%#{format.to_s}f", self) end end proxy = ENV["http_proxy"] torrent = ARGV.shift or die syntax dest = ARGV.shift puts "reading torrent..." begin mi = RubyTorrent::MetaInfo.from_location(torrent, proxy) rescue RubyTorrent::MetaInfoFormatError, RubyTorrent::BEncodingError => e die %{Error: can't parse metainfo file "#{torrent}"---maybe not a .torrent?} rescue RubyTorrent::TypedStructError => e $stderr << < e die %{Error: can't read file "#{torrent}": #{e.message}} end unless dest.nil? if FileTest.directory?(dest) && mi.info.single? dest = File.join(dest, mi.info.name) elsif FileTest.file?(dest) && mi.info.multiple? die %{Error: .torrent contains multiple files, but "#{dest}" is a single file (must be a directory)} end end print "checking file status: " ; $stdout.flush package = RubyTorrent::Package.new(mi, dest) do |piece| print(piece.complete? && piece.valid? ? "#" : ".") $stdout.flush end puts " done" puts "starting peer..." bt = RubyTorrent::BitTorrent.new(mi, package, :http_proxy => proxy) #, :dlratelim => 20*1024, :ulratelim => 10*1024) unless $DEBUG # these are duplicated by debugging information bt.on_event(self, :trying_peer) { |s, p| puts "trying peer #{p}" } bt.on_event(self, :forgetting_peer) { |s, p| puts "couldn't connect to peer #{p}" } bt.on_event(self, :removed_peer) { |s, p| puts "disconnected from peer #{p}" } end bt.on_event(self, :added_peer) { |s, p| puts "connected to peer #{p}" } bt.on_event(self, :received_block) { |s, b, peer| puts "<- got block #{b} from peer #{peer}, now #{package.pieces[b.pindex].percent_done.f}% done and #{package.pieces[b.pindex].percent_claimed.f}% claimed" } bt.on_event(self, :sent_block) { |s, b, peer| puts "-> sent block #{b} to peer #{peer}" } bt.on_event(self, :requested_block) { |s, b, peer| puts "-- requested block #{b} from #{peer}" } bt.on_event(self, :have_piece) { |s, p| puts "***** got complete and valid piece #{p}" } bt.on_event(self, :discarded_piece) { |s, p| puts "XXXXX checksum error on piece #{p}, discarded" } bt.on_event(self, :tracker_connected) { |s, url| puts "[tracker] connected to tracker #{url}" } bt.on_event(self, :tracker_lost) { |s, url| puts "[tracker] couldn't connect to tracker #{url}" } bt.on_event(self, :complete) do puts < e socket.close rescue nil end end def start @shutdown = false @thread = Thread.new do begin while !@shutdown; receive; end rescue IOError, StandardError rt_warning "**** socket receive error, retrying" sleep 5 retry end end self end def shutdown return if @shutdown @shutdown = true @server.close rescue nil @thread.join(0.2) @controllers.each { |hash, cont| cont.shutdown } self end def to_s "<#{self.class}: port #{port}, peer_id #{@id.inspect}>" end private def receive # blocking ssocket = @server.accept Thread.new do socket = ssocket begin rt_debug "<= incoming connection from #{socket.peeraddr[2]}:#{socket.peeraddr[1]}" hash, peer_id = shake_hands(socket, nil) cont = @controllers[hash] peer = PeerConnection.new("#{socket.peeraddr[2]}:#{socket.peeraddr[1]}", cont, socket, cont.package) cont.add_peer peer rescue SystemCallError, ProtocolError => e rt_debug "killing incoming connection: #{e}" socket.close rescue nil end end end ## if info_hash is nil here, the socket is treated as an incoming ## connection---it will wait for the peer's info_hash and respond ## with the same if it corresponds to a current download, otherwise ## it will raise a ProtocolError. ## ## if info_hash is not nil, the socket is treated as an outgoing ## connection, and it will send the info_hash immediately. def shake_hands(sock, info_hash) # rt_debug "initiating #{(info_hash.nil? ? 'incoming' : 'outgoing')} handshake..." sock.send("\023BitTorrent protocol\0\0\0\0\0\0\0\0", 0); sock.send("#{info_hash}#{@id}", 0) unless info_hash.nil? len = sock.recv(1)[0] # rt_debug "length #{len.inspect}" raise ProtocolError, "invalid handshake length byte #{len.inspect}" unless len == 19 name = sock.recv(19) # rt_debug "name #{name.inspect}" raise ProtocolError, "invalid handshake protocol string #{name.inspect}" unless name == "BitTorrent protocol" reserved = sock.recv(8) # rt_debug "reserved: #{reserved.inspect}" # ignore for now their_hash = sock.recv(20) # rt_debug "their info hash: #{their_hash.inspect}" if info_hash.nil? raise ProtocolError, "client requests package we don't have: hash=#{their_hash.inspect}" unless @controllers.has_key? their_hash info_hash = their_hash sock.send("#{info_hash}#{@id}", 0) else raise ProtocolError, "mismatched info hashes: us=#{info_hash.inspect}, them=#{their_hash.inspect}" unless info_hash == their_hash end peerid = sock.recv(20) # rt_debug "peer id: #{peerid.inspect}" raise ProtocolError, "connected to self" if peerid == @id # rt_debug "== handshake complete ==" [info_hash, peerid] end end end rubytorrent-0.3/lib/rubytorrent/package.rb0000644000175000017500000004247011756644133020437 0ustar boutilboutil## package.rb -- RubyTorrent <=> filesystem interface. ## Copyright 2004 William Morgan. ## ## This file is part of RubyTorrent. RubyTorrent is free software; ## you can redistribute it and/or modify it under the terms of version ## 2 of the GNU General Public License as published by the Free ## Software Foundation. ## ## RubyTorrent is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License (in the file COPYING) for more details. require 'thread' require 'digest/sha1' ## A Package is the connection between the network and the ## filesystem. There is one Package per torrent. Each Package is ## composed of one or more Pieces, as determined by the MetaInfoInfo ## object, and each Piece is composed of one or more Blocks, which are ## transmitted over the PeerConnection with :piece comments. module RubyTorrent ## Range plus a lot of utility methods class AwesomeRange < Range def initialize(start, endd=nil, exclude_end=false) case start when Integer raise ArgumentError, "both start and endd must be specified" if endd.nil? super(start, endd, exclude_end) when Range super(start.first, start.last, start.exclude_end?) else raise ArgumentError, "start should be an Integer or a Range, is a #{start.class}" end end ## range super-set: does this range encompass 'o'? def rss?(o) (first <= o.first) && ((last > o.last) || (o.exclude_end? && (last == o.last))) end ## range intersection def rint(o) ## three cases. either: ## a) our left endpoint is within o if ((first >= o.first) && ((first < o.last) || (!o.exclude_end? && (first == o.last)))) if last < o.last AwesomeRange.new(first, last, exclude_end?) elsif last > o.last AwesomeRange.new(first, o.last, o.exclude_end?) else # == AwesomeRange.new(first, last, exclude_end? || o.exclude_end?) end ## b) our right endpoint is within o elsif (((last > o.first) || (!exclude_end? && (last == o.first))) && ((last < o.last) || (!o.exclude_end? && (last == o.last)))) AwesomeRange.new([first, o.first].max, last, exclude_end?) ## c) we encompass o elsif rss?(o) o else nil end end ## range continuity def rcont?(o) (first == o.last) || (last == o.first) || (rint(o) != nil) end ## range union: only valid for continuous ranges def runion(o) if last > o.last AwesomeRange.new([first, o.first].min, last, exclude_end?) elsif o.last > last AwesomeRange.new([first, o.first].min, o.last, o.exclude_end?) else # equal AwesomeRange.new([first, o.first].min, last, (exclude_end? && o.exclude_end?)) end end ## range difference. returns an array of 0, 1 or 2 ranges. def rdiff(o) return [] if o == self ret = [] int = rint o return [] if int == self return [self] if int == nil raise RangeError, "can't subtract a range that doesn't have an exclusive end" unless int.exclude_end? if int.first > first ret << AwesomeRange.new(first, int.first, true) end ret + [AwesomeRange.new(int.last, last, exclude_end?)] end end ## a Covering is a set of non-overlapping ranges within a given start ## point and endpoint. class Covering attr_accessor :domain, :ranges ## 'domain' should be an AwesomeRange determining the start and end ## point. 'ranges' should be an array of non-overlapping ## AwesomeRanges sorted by start point. def initialize(domain, ranges=[]) @domain = domain @ranges = ranges end def complete!; @ranges = [@domain]; self; end def complete?; @ranges == [@domain]; end def empty!; @ranges = []; self; end def empty?; @ranges == []; end ## given a covering of size N and a new range 'r', returns a ## covering of size 0 <= s <= N + 1 that doesn't cover the range ## given by 'r'. def poke(r) raise ArgumentError, "#{r} outside of domain #@domain" unless @domain.rss? r Covering.new(@domain, @ranges.inject([]) do |set, x| if x.rint(r) != nil set + x.rdiff(r) else set + [x] end end) end ## given a covering of size N and a new range 'r', returns a ## covering of size 0 < s <= N + 1 that also covers the range 'r'. def fill(r) raise ArgumentError, "#{r} outside of domain #@domain" unless @domain.rss? r Covering.new(@domain, @ranges.inject([]) do |set, x| ## r contains the result of the continuing merge. if r is nil, ## then we've already added it, so we just copy x. if r.nil? then set + [x] else ## otoh, if r is there, we try and merge in the current ## element. if r.rcont? x ## if we can merge, keep the union in r and don't add ## anything r = r.runion x set ## if we can't merge it, we'll see if it's time to add it. we ## know that r and x don't overlap because r.mergable?(x) was ## false, so we can simply compare the start points to see ## whether it should come before x. elsif r.first < x.first s = set + [r, x] # add both r = nil s else set + [x] ## no merging or adding, so we just copy x. end end ## if 'r' still hasn't been added, it should be the last element, ## we add it here. end.push(r).compact) end ## given an array of non-overlapping ranges sorted by start point, ## and a range 'domain', returns the first range from 'domain' not ## covered by any range in the array. def first_gap(domain=@domain) start = domain.first endd = nil excl = nil @ranges.each do |r| next if r.last < start if r.first > start # found a gap if r.first < domain.last return AwesomeRange.new(start, r.first, false) else # r.first >= domain.last, so use domain's exclusion return AwesomeRange.new(start, domain.last, domain.exclude_end?) end else # r.first <= start start = r.last unless r.last < start break if start > domain.last end end if (start >= domain.last) ## entire domain was covered nil else ## tail end of the domain uncovered AwesomeRange.new(start, domain.last, domain.exclude_end?) end end def ==(o); o.domain == self.domain && o.ranges == self.ranges; end end ## Blocks are very simple chunks of data which exist solely in ## memory. they are the basic currency of the bittorrent protocol. a ## Block can be divided into "chunks" (no intelligence there; it's ## solely for the purposes of buffered reading/writing) and one or ## more Blocks comprises a Piece. class Block attr_accessor :pindex, :begin, :length, :data, :requested def initialize(pindex, beginn, length) @pindex = pindex @begin = beginn @length = length @data = nil @requested = false @time = nil end def requested?; @requested; end def have_length; @data.length; end def complete?; @data && (@data.length == @length); end def mark_time; @time = Time.now; end def time_elapsed; Time.now - @time; end def to_s "" end ## chunk can only be added to blocks in order def add_chunk(chunk) @data = "" if @data.nil? raise "adding chunk would result in too much data (#{@data.length} + #{chunk.length} > #@length)" if (@data.length + chunk.length) > @length @data += chunk self end def each_chunk(blocksize) raise "each_chunk called on incomplete block" unless complete? start = 0 while(start < @length) yield data[start, [blocksize, @length - start].min] start += blocksize end end def ==(o) o.is_a?(Block) && (o.pindex == self.pindex) && (o.begin == self.begin) && (o.length == self.length) end end ## a Piece is the basic unit of the .torrent metainfo file (though not ## of the bittorrent protocol). Pieces store their data directly on ## disk, so many operations here will be slow. each Piece stores data ## in one or more file pointers. ## ## unlike Blocks and Packages, which are either complete or ## incomplete, a Piece can be complete but not valid, if the SHA1 ## check fails. thus, a call to piece.complete? is not sufficient to ## determine whether the data is ok to use or not. ## ## Pieces handle all the trickiness involved with Blocks: taking in ## Blocks from arbitrary locations, writing them out to the correct ## set of file pointers, keeping track of which sections of the data ## have been filled, claimed but not filled, etc. class Piece include EventSource attr_reader :index, :start, :length event :complete def initialize(index, sha1, start, length, files, validity_assumption=nil) @index = index @sha1 = sha1 @start = start @length = length @files = files # array of [file pointer, mutex, file length] @valid = nil ## calculate where we start and end in terms of the file pointers. @start_index = 0 sum = 0 while(sum + @files[@start_index][2] <= @start) sum += @files[@start_index][2] @start_index += 1 end ## now sum + @files[@start_index][2] > start, and sum <= start @start_offset = @start - sum ## sections of the data we have @have = Covering.new(AwesomeRange.new(0 ... @length)).complete! @valid = validity_assumption @have.empty! unless valid? ## sections of the data someone has laid claim to but hasn't yet ## provided. a super-set of @have. @claimed = Covering.new(AwesomeRange.new(0 ... @length)) ## protects @claimed, @have @state_m = Mutex.new end def to_s "" end def complete?; @have.complete?; end def started?; !@claimed.empty? || !@have.empty?; end def discard # discard all data @state_m.synchronize do @have.empty! @claimed.empty! end @valid = false end def valid? return @valid unless @valid.nil? return (@valid = false) unless complete? data = read_bytes(0, @length) if (data.length != @length) @valid = false else @valid = (Digest::SHA1.digest(data) == @sha1) end end def unclaimed_bytes r = 0 each_gap(@claimed) { |start, len| r += len } r end def empty_bytes r = 0 each_gap(@have) { |start, len| r += len } r end def percent_claimed; 100.0 * (@length.to_f - unclaimed_bytes) / @length; end def percent_done; 100.0 * (@length.to_f - empty_bytes) / @length; end def each_unclaimed_block(max_length) raise "no unclaimed blocks in a complete piece" if complete? each_gap(@claimed, max_length) do |start, len| yield Block.new(@index, start, len) end end def each_empty_block(max_length) raise "no empty blocks in a complete piece" if complete? each_gap(@have, max_length) do |start, len| yield Block.new(@index, start, len) end end def claim_block(b) @state_m.synchronize do @claimed = @claimed.fill AwesomeRange.new(b.begin ... (b.begin + b.length)) end end def unclaim_block(b) @state_m.synchronize do @claimed = @claimed.poke AwesomeRange.new(b.begin ... (b.begin + b.length)) end end ## for a complete Piece, returns a complete Block of specified size ## and location. def get_complete_block(beginn, length) raise "can't make block from incomplete piece" unless complete? raise "invalid parameters #{beginn}, #{length}" unless (length > 0) && (beginn + length) <= @length b = Block.new(@index, beginn, length) b.add_chunk read_bytes(beginn, length) # returns b end ## we don't do any checking that this block has been claimed or not. def add_block(b) @valid = nil write = false new_have = @state_m.synchronize { @have.fill AwesomeRange.new(b.begin ... (b.begin + b.length)) } if new_have != @have @have = new_have write = true end write_bytes(b.begin, b.data) if write send_event(:complete) if complete? end private ## yields successive gaps from 'array' between 0 and @length def each_gap(covering, max_length=nil) return if covering.complete? range_first = 0 while true range = covering.first_gap(range_first ... @length) break if range.nil? || (range.first == range.last) start = range.first while start < range.last len = range.last - start len = max_length if max_length && (max_length < len) yield start, len start += len end range_first = range.last end end def write_bytes(start, data); do_bytes(start, 0, data); end def read_bytes(start, length); do_bytes(start, length, nil); end ## do the dirty work of splitting the read/writes across multiple ## file pointers to possibly incomplete, possibly overcomplete files def do_bytes(start, length, data) raise ArgumentError, "invalid start" if (start < 0) || (start > @length) # raise "invalid length" if (length < 0) || (start + length > @length) start += @start_offset index = @start_index sum = 0 while(sum + @files[index][2] <= start) sum += @files[index][2] index += 1 end offset = start - sum done = 0 abort = false if data.nil? want = length ret = "" else want = data.length ret = 0 end while (done < want) && !abort break if index > @files.length fp, mutex, size = @files[index] mutex.synchronize do fp.seek offset here = [want - done, size - offset].min if data.nil? # puts "> reading #{here} bytes from #{index} at #{offset}" s = fp.read here # puts "> got #{(s.nil? ? s.inspect : s.length)} bytes" if s.nil? abort = true else ret += s abort = true if s.length < here # puts "fp.tell is #{fp.tell}, size is #{size}, eof #{fp.eof?}" if (fp.tell == size) && !fp.eof? rt_warning "file #{index}: not at eof after #{size} bytes, truncating" fp.truncate(size - 1) end end else # puts "> writing #{here} bytes to #{index} at #{offset}" x = fp.write data[done, here] ret += here # @files[index][0].flush end done += here end index += 1 offset = 0 end ret end end ## finally, the Package. one Package per Controller so we don't do any ## thread safety stuff in here. class Package include EventSource attr_reader :pieces, :size event :complete def initialize(metainfo, out=nil, validity_assumption=nil) info = metainfo.info created = false out ||= info.name case out when File raise ArgumentError, "'out' cannot be a File for a multi-file .torrent" if info.multiple? fstream = out when Dir raise ArgumentError, "'out' cannot be a Dir for a single-file .torrent" if info.single? fstream = out when String if info.single? rt_debug "output file is #{out}" begin fstream = File.open(out, "rb+") rescue Errno::ENOENT created = true fstream = File.open(out, "wb+") end else rt_debug "output directory is #{out}" unless File.exists? out Dir.mkdir(out) created = true end fstream = Dir.open(out) end else raise ArgumentError, "'out' should be a File, Dir or String object, is #{out.class}" end @ro = false @size = info.total_length if info.single? @files = [[fstream, Mutex.new, info.length]] else @files = info.files.map do |finfo| path = File.join(finfo.path[0, finfo.path.length - 1].inject(fstream.path) do |path, el| dir = File.join(path, el) unless File.exist? dir rt_debug "making directory #{dir}" Dir.mkdir dir end dir end, finfo.path[finfo.path.length - 1]) rt_debug "opening #{path}..." [open_file(path), Mutex.new, finfo.length] end end i = 0 @pieces = info.pieces.unpack("a20" * (info.pieces.length / 20)).map do |hash| start = (info.piece_length * i) len = [info.piece_length, @size - start].min p = Piece.new(i, hash, start, len, @files, (created ? false : validity_assumption)) p.on_event(self, :complete) { send_event(:complete) if complete? } yield p if block_given? (i += 1) && p end reopen_ro if complete? end def ro?; @ro; end def reopen_ro raise "called on incomplete package" unless complete? return if @ro rt_debug "reopening all files with mode r" @files = @files.map do |fp, mutex, size| [fp.reopen(fp.path, "rb"), mutex, size] end @ro = true end def complete?; @pieces.detect { |p| !p.complete? || !p.valid? } == nil; end def bytes_completed @pieces.inject(0) { |s, p| s + (p.complete? ? p.length : 0) } end def pieces_completed @pieces.inject(0) { |s, p| s + (p.complete? ? 1 : 0) } end def percent_completed 100.0 * pieces_completed.to_f / @pieces.length.to_f end def num_pieces; @pieces.length; end def to_s "<#{self.class} size #@size>" end private def open_file(path) begin File.open(path, "rb+") rescue Errno::ENOENT File.open(path, "wb+") end end end end rubytorrent-0.3/lib/rubytorrent/controller.rb0000644000175000017500000005367411756644133021237 0ustar boutilboutil## controller.rb -- cross-peer logic. ## Copyright 2004 William Morgan. ## ## This file is part of RubyTorrent. RubyTorrent is free software; ## you can redistribute it and/or modify it under the terms of version ## 2 of the GNU General Public License as published by the Free ## Software Foundation. ## ## RubyTorrent is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License (in the file COPYING) for more details. require 'socket' require 'thread' module RubyTorrent ## keeps pieces in order class PieceOrder POP_RECALC_THRESH = 20 # popularity of all pieces is recalculated # (expensive sort) when this number of pieces # have arrived at any of the peers, OR: POP_RECALC_LIMIT = 30 # ... when this many seconds have passed when # at least one piece has changed in # popularity, or if we're in fuseki mode. def initialize(package) @package = package @order = nil @num_changed = 0 @pop = Array.new(@package.pieces.length, 0) @jitter = Array.new(@package.pieces.length) { rand } @m = Mutex.new @last_recalc = nil end ## increment the popularity of a piece def inc(i) @m.synchronize do @pop[i.to_i] += 1 @num_changed += 1 end end ## increment the popularity of multiple pieces def inc_all(bitfield, inc=1) @m.synchronize do bitfield.each_index do |i| if bitfield[i] @pop[i] += inc @num_changed += 1 end end end end def dec_all(bitfield) inc_all(bitfield, -1) end def each(in_fuseki, num_peers) if (@num_changed > POP_RECALC_THRESH) || @last_recalc.nil? || (((@num_changed > 0) || in_fuseki) && ((Time.now - @last_recalc) > POP_RECALC_LIMIT)) rt_debug "* reordering pieces: (#@num_changed changed, last recalc #{(@last_recalc.nil? ? '(never)' : (Time.now - @last_recalc).round)}s ago)..." recalc_order(in_fuseki, num_peers) end @order.each { |i| yield i } end private def recalc_order(in_fuseki, num_peers) @m.synchronize do @num_changed = 0 @order = (0 ... @pop.length).sort_by do |i| p = @package.pieces[i] @jitter[i] + if p.started? && !p.complete? # always try to complete a started piece pri = -1 + p.unclaimed_bytes.to_f / p.length rt_debug " piece #{i} is started but not completed => priority #{pri} (#{p.percent_claimed.round}% claimed, #{p.percent_done.round}% done)" pri elsif p.complete? # don't need these # puts " piece #{i} is completed => #{@pop.length}" @pop.length # a big number elsif in_fuseki # distance from (# peers) / 2 # puts " piece #{i} has fuseki score #{(@pop[i] - (num_peers / 2)).abs}" (@pop[i] - (num_peers / 2)).abs else # puts " piece #{i} has popularity #{@pop[i]}" @pop[i] end end end @last_recalc = Time.now rt_debug "* new piece priority: " + @order[0...15].map { |x| x.to_s }.join(', ') + " ..." end end ## The Controller manages all PeerConnections for a single Package. It ## instructs them to request blocks, and tells them whether to choke ## their connections or not. It also reports progress to the tracker. ## ## Incoming, post-handshake peer connections are added by the Server ## via calling add_connection; deciding to accept these is the ## Controller's responsibility, as is connecting to any new peers. class Controller include EventSource extend AttrReaderQ, MinIntervalMethods ## general behavior parameters HEARTBEAT = 5 # seconds between iterations of the heartbeat MAX_PEERS = 15 # hard limit on the number of peers ENDGAME_PIECE_THRESH = 5 # (wild guess) number of pieces remaining # before we trigger end-game mode FUSEKI_PIECE_THRESH = 2 # number of pieces we must have before # getting out of fuseki mode. in fuseki # ("opening", if you're not a weiqi/go fan) # mode, rather than ranking pieces by # rarity, we rank them by how distant their # popularity is from (# peers) / 2, and we're # also stingly in handing out requests. SPAWN_NEW_PEER_THRESH = 0.75 # portion of the download rate above # which we'll stop making new peer # connections RATE_WINDOW = 20 # window size (in seconds) of the rate calculation. # presumably this should be the same as the window # used in the RateMeter class. ## tracker parameters. when we can't access a tracker, we retry at ## DEAD_TRACKER_INITIAL_DELAY seconds and double that after every ## failure, capping at DEAD_TRACKER_MAX_DELAY. DEAD_TRACKER_INITIAL_INTERVAL = 5 DEAD_TRACKER_MAX_INTERVAL = 3600 ## single peer parameters KEEPALIVE_INTERVAL = 120 # seconds of silence before sending a keepalive SILENT_DEATH_INTERVAL = 240 # seconds of silence before we drop a peer BOREDOM_DEATH_INTERVAL = 120 # seconds of existence with no downloaded data # at which we drop a peer in favor of # an incoming peer (unless the package # is complete) BLOCK_SIZE = 2**15 # send this size blocks. need to find out more # about this parameter: how does it affect # transfer rates? ## antisnubbing ANTISNUB_RATE_THRESH = 1024 # if the total bytes/second across all # peers falls below this threshold, we # trigger anti-snubbing mode ANTISNUB_INTERVAL = 60 # seconds of no blocks from a peer before we # add an optimistic unchoke slot when in # anti-snubbing mode. ## choking and optimistic unchoking parameters NUM_FRIENDS = 4 # number of peers unchoked due to high download rates CALC_FRIENDS_INTERVAL = 10 # seconds between recalculating choked # status for each peer CALC_OPTUNCHOKES_INTERVAL = 30 # seconds between reassigning # optimistic unchoked status NUM_OPTUNCHOKES = 1 # number of optimistic unchoke slots # (not including any temporary ones # generated in anti-snubbing mode. NEW_OPTUNCHOKE_PROB = 0.5 # peers are ranked by the age of # their connection, and optimistic # unchoking slots are given with # probability p*(1-p)^r, where r # is the rank and p is this number. attr_accessor :package, :info_hash, :tracker, :ulratelim, :dlratelim, :http_proxy attr_reader_q :running event :trying_peer, :forgetting_peer, :added_peer, :removed_peer, :received_block, :sent_block, :have_piece, :discarded_piece, :tracker_connected, :tracker_lost, :requested_block def initialize(server, package, info_hash, trackers, dlratelim=nil, ulratelim=nil, http_proxy=ENV["http_proxy"]) @server = server @info_hash = info_hash @package = package @trackers = trackers @http_proxy = http_proxy @dlratelim = dlratelim @ulratelim = ulratelim @peers = [].extend(ArrayShuffle) @peers_m = Mutex.new @thread = nil @tracker = nil @last_tracker_attempt = nil @tracker_delay = DEAD_TRACKER_INITIAL_INTERVAL ## friends @num_friends = 0 @num_optunchokes = 0 @num_snubbed = 0 ## keep track of the popularity of the pieces so as to assign ## blocks optimally to peers. @piece_order = PieceOrder.new @package @running = false end def dlrate; @peers.inject(0) { |s, p| s + p.dlrate }; end def ulrate; @peers.inject(0) { |s, p| s + p.ulrate }; end def dlamt; @peers.inject(0) { |s, p| s + p.dlamt }; end def ulamt; @peers.inject(0) { |s, p| s + p.ulamt }; end def num_peers; @peers.length; end def start raise "already" if @running find_tracker @in_endgame = false @in_antisnub = false @in_fuseki = false @running = true @thread = Thread.new do while @running step sleep HEARTBEAT end end @peers.each { |p| p.start unless p.running? } self end def shutdown @running = false @tracker.stopped unless @tracker.nil? rescue TrackerError @thread.join(0.2) @peers.each { |c| c.shutdown } self end def to_s "<#{self.class}: package #{@package}>" end ## this could be called at any point by the Server, if it receives ## incoming peer connections. def add_peer(p) accept = true if @peers.length >= MAX_PEERS && !@package.complete? oldp = @peers.find { |x| !x.running? || ((x.dlamt == 0) && ((Time.now - x.start_time) > BOREDOM_DEATH_INTERVAL)) } if oldp rt_debug "killing peer for being boring: #{oldp}" oldp.shutdown else rt_debug "too many peers, ignoring #{p}" p.shutdown accept = false end end if accept p.on_event(self, :received_block) { |peer, block| received_block(block, peer) } p.on_event(self, :peer_has_piece) { |peer, piece| peer_has_piece(piece, peer) } p.on_event(self, :peer_has_pieces) { |peer, bitfield| peer_has_pieces(bitfield, peer) } p.on_event(self, :sent_block) { |peer, block| send_event(:sent_block, block, peer.name) } p.on_event(self, :requested_block) { |peer, block| send_event(:requested_block, block, peer.name) } @peers_m.synchronize do @peers.push p ## it's important not to call p.start (which triggers the ## bitfield message) until it's been added to @peer, such that ## any :have messages that might happen from other peers in ## the mean time are propagated to it. ## ## of course that means we need to call p.start within the ## mutex context so that the reaper section of the heartbeat ## doesn't kill it between push and start. ## ## ah, the joys of threaded programming. p.start if @running end send_event(:added_peer, p.name) end end def received_block(block, peer) if @in_endgame @peers_m.synchronize { @peers.each { |p| p.cancel block if p.running? && (p != peer)} } end send_event(:received_block, block, peer.name) piece = @package.pieces[block.pindex] # find corresponding piece if piece.complete? if piece.valid? @peers_m.synchronize { @peers.each { |peer| peer.have_piece piece } } send_event(:have_piece, piece) else rt_warning "#{self}: received data for #{piece} does not match SHA1 hash, discarding" send_event(:discarded_piece, piece) piece.discard end end end def peer_has_piece(piece, peer) @piece_order.inc piece.index end def peer_has_pieces(bitfield, peer) @piece_order.inc_all bitfield end ## yield all desired blocks, in order of desire. called by peers to ## refill their queues. def claim_blocks @piece_order.each(@in_fuseki, @peers.length) do |i| p = @package.pieces[i] next if p.complete? # rt_debug "+ considering piece #{p}" if @in_endgame p.each_empty_block(BLOCK_SIZE) { |b| yield b } else p.each_unclaimed_block(BLOCK_SIZE) do |b| if yield b p.claim_block b return if @in_fuseki # fuseki shortcut end end end end end def forget_blocks(blocks) # rt_debug "#{self}: forgetting blocks #{blocks.join(', ')}" blocks.each { |b| @package.pieces[b.pindex].unclaim_block b } end def peer_info @peers.map do |p| next nil unless p.running? {:name => p.name, :seed => p.peer_complete?, :dlamt => p.dlamt, :ulamt => p.ulamt, :dlrate => p.dlrate, :ulrate => p.ulrate, :pending_send => p.pending_send, :pending_recv => p.pending_recv, :interested => p.interested?, :peer_interested => p.peer_interested?, :choking => p.choking?, :peer_choking => p.peer_choking?, :snubbing => p.snubbing?, :we_desire => @package.pieces.inject(0) do |s, piece| s + (!piece.complete? && p.piece_available?(piece.index) ? 1 : 0) end, :they_desire => @package.pieces.inject(0) do |s, piece| s + (piece.complete? && !p.piece_available?(piece.index) ? 1 : 0) end, :start_time => p.start_time } end.compact end private def find_tracker return if @tracker || (@last_tracker_attempt && (Time.now - @last_tracker_attempt) < @tracker_delay) @last_tracker_attempt = Time.now Thread.new do @trackers.each do |tracker| break if @tracker rt_debug "trying tracker #{tracker}" tc = TrackerConnection.new(tracker, @info_hash, @package.size, @server.port, @server.id, nil, 50, @http_proxy) begin @tracker = tc.started tc.already_completed if @package.complete? @tracker_delay = DEAD_TRACKER_INITIAL_INTERVAL send_event(:tracker_connected, tc.url) rescue TrackerError => e rt_debug "couldn't connect: #{e.message}" end end end @tracker_delay = [@tracker_delay * 2, DEAD_TRACKER_MAX_INTERVAL].min if @tracker.nil? rt_warning "couldn't connect to tracker, next try in #@tracker_delay seconds" if @tracker.nil? end def add_a_peer return false if @tracker.nil? || (@peers.length >= MAX_PEERS) || @package.complete? || (@num_friends >= NUM_FRIENDS) || (@dlratelim && (dlrate > (@dlratelim * SPAWN_NEW_PEER_THRESH))) @tracker.peers.shuffle.each do |peer| # rt_debug "]] comparing: #{peer.ip} vs #{@server.ip} and #{peer.port} vs #{@server.port} (tried? #{peer.tried?})" next if peer.tried? || ((peer.ip == @server.ip) && (peer.port == @server.port)) rescue next peername = "#{peer.ip}:#{peer.port}" send_event(:trying_peer, peername) Thread.new do # this may ultimately result in a call to add_peer sleep rand(10) rt_debug "=> making outgoing connection to #{peername}" begin peer.tried = true socket = TCPSocket.new(peer.ip, peer.port) @server.add_connection(peername, self, socket) rescue SocketError, SystemCallError, Timeout::Error => e rt_debug "couldn't connect to #{peername}: #{e}" send_event(:forgetting_peer, peername) end end break end true end def refresh_tracker return if @tracker.nil? @tracker.downloaded = dlamt @tracker.uploaded = ulamt @tracker.left = @package.size - @package.bytes_completed begin @tracker.refresh rescue TrackerError send_event(:tracker_lost, @tracker.url) @tracker = nil find_tracker # find a new one end end def calc_friends @num_friends = 0 if @package.complete? @peers.sort_by { |p| -p.ulrate }.each do |p| next if p.snubbing? || !p.running? p.choke = (@num_friends >= NUM_FRIENDS) @num_friends += 1 if p.peer_interested? end else @peers.sort_by { |p| -p.dlrate }.each do |p| next if p.snubbing? || !p.running? p.choke = (@num_friends >= NUM_FRIENDS) @num_friends += 1 if p.peer_interested? end end end min_interval :calc_friends, CALC_FRIENDS_INTERVAL def calc_optunchokes rt_debug "* calculating optimistic unchokes..." @num_optunchokes = 0 if @in_antisnub ## count up the number of our fair weather friends: peers who ## are interested and whom we're not choking, but who haven't ## sent us a block for ANTISNUB_INTERVAL seconds. for each of ## these, we add an extra optimistic unchoking slot to our usual ## NUM_OPTUNCHOKES slots. in actuality that's the number of ## friends PLUS the number of optimistic unchokes who are ## snubbing us, but that's not a big deal, as long as we cap the ## number of extra slots at NUM_FRIENDS. @num_optunchokes -= @peers.inject(0) { |s, p| s + (p.running? && p.peer_interested? && !p.choking? && (Time.now - (p.last_recv_block_time || p.start_time) > ANTISNUB_INTERVAL) ? 1 : 0) } @num_optunchokes = [-NUM_FRIENDS, @num_optunchokes].max rt_debug "* anti-snubbing mode, #{-@num_optunchokes} extra optimistic unchoke slots" end ## i love ruby @peers.find_all { |p| p.running? }.sort_by { |p| p.start_time }.reverse.each do |p| break if @num_optunchokes >= NUM_OPTUNCHOKES next if p.snubbing? # rt_debug "* considering #{p}: #{p.peer_interested?} and #{@num_optunchokes < NUM_OPTUNCHOKES} and #{rand(0.999) < NEW_OPTUNCHOKE_PROB}" if p.peer_interested? && (rand < NEW_OPTUNCHOKE_PROB) rt_debug " #{p}: awarded optimistic unchoke" p.choke = false @num_optunchokes += 1 end end end min_interval :calc_optunchokes, CALC_OPTUNCHOKES_INTERVAL ## the "heartbeat". all time-based actions are triggered here. def step ## see if we should be in antisnubbing mode if !@package.complete? && (dlrate < ANTISNUB_RATE_THRESH) rt_debug "= dl rate #{dlrate} < #{ANTISNUB_RATE_THRESH}, in antisnub mode" if !@in_antisnub @in_antisnub = true else rt_debug "= dl rate #{dlrate} >= #{ANTISNUB_RATE_THRESH}, out of antisnub mode" if @in_antisnub @in_antisnub = false end ## see if we should be in fuseki mode if !@package.complete? && (@package.pieces_completed < FUSEKI_PIECE_THRESH) rt_debug "= num pieces #{@package.pieces_completed} < #{FUSEKI_PIECE_THRESH}, in fuseki mode" if !@in_fuseki @in_fuseki = true else rt_debug "= num pieces #{@package.pieces_completed} >= #{FUSEKI_PIECE_THRESH}, out of fuseki mode" if @in_fuseki @in_fuseki = false end ## see if we should be in endgame mode if @package.complete? rt_debug "= left endgame mode" if @in_endgame @in_endgame = false elsif (@package.pieces.length - @package.pieces_completed) <= ENDGAME_PIECE_THRESH rt_debug "= have #{@package.pieces_completed} pieces, in endgame mode" @in_endgame = true end # puts " heartbeat: dlrate #{(dlrate / 1024.0).round}kb/s (lim #{(@dlratelim ? (@dlratelim / 1024.0).round : 'none')}) ulrate #{(ulrate / 1024.0).round}kb/s (lim #{(@ulratelim ? (@ulratelim / 1024.0).round : 'none')}) endgame? #@in_endgame antisnubbing? #@in_antisnub fuseki? #@in_fuseki" # @package.pieces.each do |p| # next if p.complete? || !p.started? # l1 = 0 # p.each_unclaimed_block(9999999) { |b| l1 += b.length } # l2 = 0 # p.each_empty_block(9999999) { |b| l2 += b.length } # puts " heartbeat: #{p.index}: #{l1} unclaimed bytes, #{l2} unfilled bytes" # end ## find a tracker if we aren't already connected to one find_tracker if @tracker.nil? if @package.complete? # if package is complete... ## kill all peers who are complete as well, as per bram's client @peers.each { |p| p.shutdown if p.peer_complete? } @tracker.completed unless @tracker.nil? || @tracker.sent_completed? ## reopen all files as readonly (dunno why, just seems like a ## good idea) @package.reopen_ro unless @package.ro? end ## kill any silent connections, and anyone who hasn't sent or ## received data in a long time. @peers_m.synchronize do @peers.each do |p| next unless p.running? if ((Time.now - (p.last_send_time || p.start_time)) > SILENT_DEATH_INTERVAL) rt_warning "shutting down peer #{p} for silence/boredom" p.shutdown end end end ## discard any dead connections @peers_m.synchronize do @peers.delete_if do |p| !p.running? && begin p.unregister_events self @piece_order.dec_all p.peer_pieces rt_debug "burying corpse of #{p}" send_event(:removed_peer, p) true end end end ## get more peers from the tracker, if all of the following are true: ## a) the package is incomplete (i.e. we're downloading, not uploading) ## b) we're connected to a tracker ## c) we've tried all the peers we've gotten so far ## d) the tracker hasn't already reported the maximum number of peers if !@package.complete? && @tracker && (@tracker.peers.inject(0) { |s, p| s + (p.tried? ? 0 : 1) } == 0) && (@tracker.numwant <= @tracker.peers.length) rt_debug "* getting more peers from the tracker" @tracker.numwant += 50 unless @tracker.in_force_refresh Thread.new do begin @tracker.force_refresh rescue TrackerError end end end end ## add peer if necessary 3.times { add_a_peer } # there's no place like home ## iterate choking policy calc_friends calc_optunchokes ## this is needed. sigh. break unless @running ## send keepalives @peers_m.synchronize { @peers.each { |p| p.send_keepalive if p.running? && p.last_send_time && ((Time.now - p.last_send_time) > KEEPALIVE_INTERVAL) } } ## now we apportion our bandwidth amongst all the peers. we'll go ## through them at random, dump everything we can, and move on iff ## we don't expect to hit our bandwidth cap. dllim = @dlratelim.nil? ? nil : (@dlratelim.to_f * (RATE_WINDOW.to_f + HEARTBEAT)) - (dlrate.to_f * RATE_WINDOW) ullim = @ulratelim.nil? ? nil : (@ulratelim.to_f * (RATE_WINDOW.to_f + HEARTBEAT)) - (ulrate.to_f * RATE_WINDOW) dl = ul = 0 @peers.shuffle.each do |p| break if (dllim && (dl >= dllim)) || (ullim && (ul >= ullim)) if p.running? pdl, pul = p.send_blocks_and_reqs(dllim && (dllim - dl), ullim && (ullim - ul)) dl += pdl ul += pul end end ## refresh tracker stats refresh_tracker if @tracker end end end rubytorrent-0.3/lib/rubytorrent/metainfo.rb0000644000175000017500000001346411756644133020647 0ustar boutilboutil## metainfo.rb -- parsed .torrent file ## Copyright 2004 William Morgan. ## ## This file is part of RubyTorrent. RubyTorrent is free software; ## you can redistribute it and/or modify it under the terms of version ## 2 of the GNU General Public License as published by the Free ## Software Foundation. ## ## RubyTorrent is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License (in the file COPYING) for more details. require "rubytorrent/typedstruct" require 'uri' require 'open-uri' require 'digest/sha1' ## MetaInfo file is the parsed form of the .torrent file that people ## send around. It contains a MetaInfoInfo and possibly some ## MetaInfoInfoFile objects. module RubyTorrent class MetaInfoFormatError < StandardError; end class MetaInfoInfoFile def initialize(dict=nil) @s = TypedStruct.new do |s| s.field :length => Integer, :md5sum => String, :sha1 => String, :path => String s.array :path s.required :length, :path end @dict = dict unless dict.nil? @s.parse dict check end end def method_missing(meth, *args) @s.send(meth, *args) end def check raise MetaInfoFormatError, "invalid file length" unless @s.length >= 0 end def to_bencoding check (@dict || @s).to_bencoding end end class MetaInfoInfo def initialize(dict=nil) @s = TypedStruct.new do |s| s.field :length => Integer, :md5sum => String, :name => String, :piece_length => Integer, :pieces => String, :files => MetaInfoInfoFile, :sha1 => String s.label :piece_length => "piece length" s.required :name, :piece_length, :pieces s.array :files s.coerce :files => lambda { |x| x.map { |y| MetaInfoInfoFile.new(y) } } end @dict = dict unless dict.nil? @s.parse dict check if dict["sha1"] ## this seems to always be off. don't know how it's supposed ## to be calculated, so fuck it. # puts "we have #{sha1.inspect}, they have #{dict['sha1'].inspect}" # rt_warning "info hash SHA1 mismatch" unless dict["sha1"] == sha1 # raise MetaInfoFormatError, "info hash SHA1 mismatch" unless dict["sha1"] == sha1 end end end def check raise MetaInfoFormatError, "invalid file length" unless @s.length.nil? || @s.length >= 0 raise MetaInfoFormatError, "one (and only one) of 'length' (single-file torrent) or 'files' (multi-file torrent) must be specified" if (@s.length.nil? && @s.files.nil?) || (!@s.length.nil? && !@s.files.nil?) if single? length = @s.length else length = @s.files.inject(0) { |s, x| s + x.length } end raise MetaInfoFormatError, "invalid metainfo file: length #{length} > (#{@s.pieces.length / 20} pieces * #{@s.piece_length})" unless length <= (@s.pieces.length / 20) * @s.piece_length raise MetaInfoFormatError, "invalid metainfo file: pieces length = #{@s.pieces.length} not a multiple of 20" unless (@s.pieces.length % 20) == 0 end def to_bencoding check (@dict || @s).to_bencoding end def sha1 if @s.dirty @sha1 = Digest::SHA1.digest(self.to_bencoding) @s.dirty = false end @sha1 end def single? !length.nil? end def multiple? length.nil? end def total_length if single? length else files.inject(0) { |a, f| a + f.length } end end def num_pieces pieces.length / 20 end def method_missing(meth, *args) @s.send(meth, *args) end end class MetaInfo def initialize(dict=nil) raise TypeError, "argument must be a Hash (maybe see MetaInfo.from_location)" unless dict.is_a? Hash @s = TypedStruct.new do |s| s.field :info => MetaInfoInfo, :announce => URI::HTTP, :announce_list => Array, :creation_date => Time, :comment => String, :created_by => String, :encoding => String s.label :announce_list => "announce-list", :creation_date => "creation date", :created_by => "created by" s.array :announce_list s.coerce :info => lambda { |x| MetaInfoInfo.new(x) }, :creation_date => lambda { |x| Time.at(x) }, :announce => lambda { |x| URI.parse(x) }, :announce_list => lambda { |x| x.map { |y| y.map { |z| URI.parse(z) } } } end @dict = dict unless dict.nil? @s.parse dict check end end def single?; info.single?; end def multiple?; info.multiple?; end def check if @s.announce_list @s.announce_list.each do |tier| tier.each { |track| raise MetaInfoFormatError, "expecting HTTP URL in announce-list, got #{track} instead" unless track.is_a? URI::HTTP } end end end def self.from_bstream(bs) dict = nil bs.each do |e| if dict == nil dict = e else raise MetaInfoFormatError, "too many bencoded elements for metainfo file (just need one)" end end raise MetaInfoFormatError, "bencoded element must be a dictionary, got a #{dict.class}" unless dict.kind_of? ::Hash MetaInfo.new(dict) end ## either a filename or a URL def self.from_location(fn, http_proxy=ENV["http_proxy"]) if http_proxy # lame! open(fn, "rb", :proxy => http_proxy) { |f| from_bstream(BStream.new(f)) } else open(fn, "rb") { |f| from_bstream(BStream.new(f)) } end end def self.from_stream(s) from_bstream(BStream.new(s)) end def method_missing(meth, *args) @s.send(meth, *args) end def to_bencoding check (@dict || @s).to_bencoding end def trackers if announce_list && (announce_list.length > 0) announce_list.map do |tier| tier.extend(ArrayShuffle).shuffle end.flatten else [announce] end end end end rubytorrent-0.3/lib/rubytorrent/typedstruct.rb0000644000175000017500000000721711756644133021436 0ustar boutilboutil## typedstruct.rb -- type-checking struct, for bencoded objects. ## Copyright 2004 William Morgan. ## ## This file is part of RubyTorrent. RubyTorrent is free software; ## you can redistribute it and/or modify it under the terms of version ## 2 of the GNU General Public License as published by the Free ## Software Foundation. ## ## RubyTorrent is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License (in the file COPYING) for more details. require "rubytorrent/bencoding" module RubyTorrent module ArrayToH def to_h inject({}) { |h, (k, v)| h[k] = v; h } # found this neat trick on the internet end end module HashMapHash def map_hash a = map { |k, v| yield k, v }.extend(ArrayToH).to_h end end class TypedStructError < StandardError; end ## type-checking struct meant for easy translation from and to ## bencoded dicts. class TypedStruct attr_accessor :dirty attr_reader :fields # writer below def initialize @required = {} @label = {} @coerce = {} @field = {} @array = {} @dirty = false @values = {} yield self if block_given? @field.each do |f, type| @required[f] ||= false @label[f] ||= f.to_s @array[f] ||= false end end def method_missing(meth, *args) if meth.to_s =~ /^(.*?)=$/ # p [meth, args] f = $1.intern raise ArgumentError, "no such value #{f}" unless @field.has_key? f type = @field[f] o = args[0] if @array[f] raise TypeError, "for #{f}, expecting Array, got #{o.class}" unless o.kind_of? ::Array o.each { |e| raise TypeError, "for elements of #{f}, expecting #{type}, got #{e.class}" unless e.kind_of? type } @values[f] = o @dirty = true else raise TypeError, "for #{f}, expecting #{type}, got #{o.class}" unless o.kind_of? type @values[f] = o @dirty = true end else raise ArgumentError, "no such value #{meth}" unless @field.has_key? meth # p [meth, @values[meth]] @values[meth] end end [:required, :array].each do |f| class_eval %{ def #{f}(*args) args.each do |x| raise %q{unknown field "\#{x}" in #{f} list} unless @field[x] @#{f}[x] = true end end } end [:field , :label, :coerce].each do |f| class_eval %{ def #{f}(hash) hash.each { |k, v| @#{f}[k] = v } end } end ## given a Hash from a bencoded dict, parses it according to the ## rules you've set up with field, required, label, etc. def parse(dict) @required.each do |f, reqd| flabel = @label[f] raise TypedStructError, "missing required parameter #{flabel} (dict has #{dict.keys.join(', ')})" if reqd && !(dict.member? flabel) if dict.member? flabel v = dict[flabel] if @coerce.member? f v = @coerce[f][v] end if @array[f] raise TypeError, "for #{flabel}, expecting Array, got #{v.class} instead" unless v.kind_of? ::Array end self.send("#{f}=", v) end end ## disabled the following line as applications seem to put tons of ## weird fields in their .torrent files. # dict.each { |k, v| raise TypedStructError, %{unknown field "#{k}"} unless @field.member?(k.to_sym) || @label.values.member?(k) } end def to_bencoding @required.each { |f, reqd| raise ArgumentError, "missing required parameter #{f}" if reqd && self.send(f).nil? } @field.extend(HashMapHash).map_hash { |f, type| [@label[f], self.send(f)] }.to_bencoding end end end rubytorrent-0.3/lib/rubytorrent/peer.rb0000644000175000017500000004112311756644133017771 0ustar boutilboutil## peer.rb -- bitttorrent peer ("wire") protocol. ## Copyright 2004 William Morgan. ## ## This file is part of RubyTorrent. RubyTorrent is free software; ## you can redistribute it and/or modify it under the terms of version ## 2 of the GNU General Public License as published by the Free ## Software Foundation. ## ## RubyTorrent is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License (in the file COPYING) for more details. require 'socket' require 'thread' require "rubytorrent/message" module RubyTorrent module ArrayToBitstring def to_bitstring ret = "\0" bit = 7 map do |b| if bit == -1 ret += "\0" bit = 7 end ret[ret.length - 1] |= (1 << bit) if b bit -= 1 end ret end end module ArrayDelete2 ## just like delete but returns the *array* element deleted rather ## than the argument. someone should file an rcr. def delete2(el) i = index el unless i.nil? ret = self[i] delete_at i ret else nil end end end module StringToBarray include StringMapBytes def to_barray self.map_bytes do |b| (0 .. 7).map { |i| (b & (1 << (7 - i))) != 0 } end.flatten end end ## estimate a rate. basically copied from bram's code. class RateMeter attr_reader :amt def initialize(window=20) @window = window.to_f @amt = 0 @rate = 0 @last = @since = Time.now - 1 @m = Mutex.new end def add(new_amt) now = Time.now @m.synchronize do @amt += new_amt @rate = ((@rate * (@last - @since)) + new_amt).to_f / (now - @since) @last = now @since = [@since, now - @window].max end end def rate (@rate * (@last - @since)).to_f / (Time.now - @since) end def bytes_until(new_rate) [(new_rate.to_f * (Time.now - @since)) - (@rate * (@last - @since)), 0].max end end class ProtocolError < StandardError; end ## The PeerConnection object deals with all the protocol issues. It ## keeps state information as to the connection and the peer. It is ## tightly integrated with the Controller object. ## ## Remember to be "strict in what you send, lenient in what you ## accept". class PeerConnection extend AttrReaderQ include EventSource attr_reader :peer_pieces, :name attr_reader_q :running, :choking, :interested, :peer_choking, :peer_interested, :snubbing event :peer_has_piece, :peer_has_pieces, :received_block, :sent_block, :requested_block BUFSIZE = 8192 MAX_PEER_REQUESTS = 5 # how many peer requests to keep queued MAX_REQUESTS = 5 # how many requests for blocks to keep current MIN_REQUESTS = 1 # get more blocks from controller when this limit is reached REQUEST_TIMEOUT = 60 # number of seconds after sending a request before we # decide it's been forgotten def initialize(name, controller, socket, package) @name = name @controller = controller @socket = socket @package = package @running = false ## my state @want_blocks = [].extend(ArrayDelete2) # blocks i want @want_blocks_m = Mutex.new @choking = true @interested = false @snubbing = false ## peer's state @peer_want_blocks = [].extend(ArrayDelete2) @peer_choking = true # assumption of initial condition @peer_interested = false # ditto @peer_pieces = Array.new(@package.num_pieces, false) # ditto @peer_virgin = true # does the peer have any pieces at all? ## connection stats @dlmeter = RateMeter.new @ulmeter = RateMeter.new @send_q = Queue.new # output thread takes messages from here and # puts them on the wire end def pending_recv; @want_blocks.find_all { |b| b.requested? }.length; end def pending_send; @peer_want_blocks.length; end def start @running = true @time = {:start => Time.now} Thread.new do # start input thread begin while @running; input_thread_step; end rescue SystemCallError, IOError, ProtocolError => e rt_debug "#{self} (input): #{e.message}, releasing #{@want_blocks.length} claimed blocks and dying" # rt_debug e.backtrace.join("\n") @running = false @controller.forget_blocks @want_blocks end end Thread.new do # start output thread begin while @running; output_thread_step; end rescue SystemCallError, IOError, ProtocolError => e rt_debug "#{self} (output): #{e.message}, releasing #{@want_blocks.length} claimed blocks and dying" # rt_debug e.backtrace.join("\n") @running = false @controller.forget_blocks @want_blocks end end ## queue the initial messages queue_message(:bitfield, {:bitfield => @package.pieces.map { |p| p.complete? }.extend(ArrayToBitstring).to_bitstring}) ## and that's it. if peer sends a bitfield, we'll send an ## interested and start requesting blocks at that point. if they ## don't, it means they don't have any pieces, so we can just sit ## tight. self end ## the Controller calls this from heartbeat thread to tell us ## whether to choke or not. def choke=(now_choke) queue_message(now_choke ? :choke : :unchoke) unless @choking == now_choke @choking = now_choke end ## the Controller calls this from heartbeat thread to tell us ## whether to snub or not. def snub=(now_snub) unless @snubbing = now_snub @snubbing = now_snub choke = true if @snubbing end end def peer_complete?; @peer_pieces.all?; end def last_send_time; @time[:send]; end def last_recv_time; @time[:recv]; end def last_send_block_time; @time[:send_block]; end def last_recv_block_time; @time[:recv_block]; end def start_time; @time[:start]; end def dlrate; @dlmeter.rate; end def ulrate; @ulmeter.rate; end def dlamt; @dlmeter.amt; end def ulamt; @ulmeter.amt; end def piece_available?(index); @peer_pieces[index]; end def to_s; ""; end ## called by Controller in the event that a request needs to be ## rescinded. def cancel(block) wblock = @want_blocks_m.synchronize { @want_blocks.delete2 block } unless wblock.nil? || !wblock.requested? rt_debug "#{self}: sending cancel for #{wblock}" queue_message(:cancel, {:index => wblock.pindex, :begin => wblock.begin, :length => wblock.length}) end get_want_blocks unless wblock.nil? end def shutdown rt_debug "#{self.to_s}: shutting down" @running = false @socket.close rescue nil end ## Controller calls this to tell us that a complete piece has been ## received. def have_piece(piece) queue_message(:have, {:index => piece.index}) end ## Controller calls this to tell us to send a keepalive def send_keepalive # rt_debug "* sending keepalive!" queue_message(:keepalive) end ## this is called both by input_thread_step and by the controller's ## heartbeat thread. it sends as many pending blocks as it can while ## keeping the amount below 'ullim', and sends as many requests as ## it can while keeping the amount below 'dllim'. ## ## returns the number of bytes requested and sent def send_blocks_and_reqs(dllim=nil, ullim=nil) sent_bytes = 0 reqd_bytes = 0 @want_blocks_m.synchronize do @want_blocks.each do |b| # puts "[][] #{self}: #{b} is #{b.requested? ? 'requested' : 'NOT requested'} and has time_elapsed of #{b.requested? ? b.time_elapsed.round : 'n/a'}s" if b.requested? && (b.time_elapsed > REQUEST_TIMEOUT) rt_warning "#{self}: for block #{b}, time elapsed since request is #{b.time_elapsed} > #{REQUEST_TIMEOUT}, assuming peer forgot about it" @want_blocks.delete b @controller.forget_blocks [b] end end end ## send :requests unless @peer_choking || !@interested @want_blocks_m.synchronize do @want_blocks.each do |b| break if dllim && (reqd_bytes >= dllim) next if b.requested? if @package.pieces[b.pindex].complete? # not sure that this will ever happen, but... rt_warning "#{self}: deleting scheduled block for already-complete piece #{b}" @want_blocks.delete b next end queue_message(:request, {:index => b.pindex, :begin => b.begin, :length => b.length}) reqd_bytes += b.length b.requested = true b.mark_time send_event(:requested_block, b) end end end ## send blocks # rt_debug "sending blocks. choking? #@choking, choked? #@peer_choking, ul rate #{ulrate}b/s, limit #@ulmeterlim" unless @peer_want_blocks.empty? unless @choking || !@peer_interested while !@peer_want_blocks.empty? break if ullim && (sent_bytes >= ullim) if (b = @peer_want_blocks.shift) sent_bytes += b.length @send_q.push b @time[:send_block] = Time.now send_event(:sent_block, b) end end end get_want_blocks [reqd_bytes, sent_bytes] end private ## re-calculate whether we're interested or not. triggered by ## received :have and :bitfield messages. def recalc_interested show_interest = !@peer_virgin || (@package.pieces.detect do |p| !p.complete? && @peer_pieces[p.index] end) != nil queue_message(show_interest ? :interested : :uninterested) unless show_interest == @interested if ((@interested = show_interest) == false) @want_blocks_m.synchronize do @controller.forget_blocks @want_blocks @want_blocks.clear end end end ## take a message/block from the send_q and place it on the wire. blocking. def output_thread_step obj = @send_q.deq case obj when Message # rt_debug "output: sending message #{obj}" + (obj.id == :request ? " (request queue size #{@want_blocks.length})" : "") send_bytes obj.to_wire_form @time[:send] = Time.now when Block # rt_debug "output: sending block #{obj}" send_bytes Message.new(:piece, {:length => obj.length, :index => obj.pindex, :begin => obj.begin}).to_wire_form obj.each_chunk(BUFSIZE) { |c| send_bytes c } @time[:send] = Time.now @ulmeter.add obj.length # rt_debug "sent block #{obj} ul rate now #{(ulrate / 1024.0).round}kb/s" else raise "don't know what to do with #{obj}" end end ## take bits from the wire and respond to them. blocking. def input_thread_step case (obj = read_from_wire) when Block handle_block obj when Message handle_message obj else raise "don't know what to do with #{obj.inspect}" end ## to enable immediate response, if there are no rate limits, ## we'll send the blocks and reqs right here. otherwise, the ## controller will call this at intervals. send_blocks_and_reqs if @controller.dlratelim.nil? && @controller.ulratelim.nil? end ## take bits from the wire and make a message/block out of them. blocking. def read_from_wire len = nil while (len = recv_bytes(4).from_fbbe) == 0 @time[:recv] = Time.now # rt_debug "* hey, a keepalive!" end id = recv_bytes(1)[0] if Message::WIRE_IDS[id] == :piece # add a block len -= 9 m = Message.from_wire_form(id, recv_bytes(8)) b = Block.new(m.index, m.begin, len) while len > 0 thislen = [BUFSIZE, len].min b.add_chunk recv_bytes(thislen) len -= thislen end @time[:recv] = @time[:recv_block] = Time.now b else # add a message m = Message.from_wire_form(id, recv_bytes(len - 1)) # rt_debug "input: read message #{m}" @time[:recv] = Time.now m end end def handle_block(block) wblock = @want_blocks_m.synchronize { @want_blocks.delete2 block } return rt_warning("#{self}: peer sent unrequested (possibly cancelled) block #{block}") if wblock.nil? || !wblock.requested? @dlmeter.add block.have_length # rt_debug "received block #{block}, dl rate now #{(dlrate / 1024.0).round}kb/s" piece = @package.pieces[block.pindex] # find corresponding piece piece.add_block block send_event(:received_block, block) get_want_blocks end def send_bytes(s) if s.nil? raise "can't send nil" elsif s.length > 0 @socket.send(s, 0) end end def recv_bytes(len) if len < 0 raise "can't recv negative bytes" elsif len == 0 "" elsif len > 512 * 1024 # 512k raise ProtocolError, "read size too big." else r = "" zeros = 0 while r.length < len x = @socket.recv(len - r.length) raise IOError, "zero bytes received" if x.length == 0 r += x end r end end def handle_message(m) case m.id when :choke # rt_debug "#{self}: peer choking (was #{@peer_choking})" @peer_choking = true @want_blocks_m.synchronize do @controller.forget_blocks @want_blocks @want_blocks.clear end when :unchoke # rt_debug "#{self}: peer not choking (was #{@peer_choking})" @peer_choking = false when :interested # rt_debug "peer interested (was #{@peer_interested})" @peer_interested = true when :uninterested # rt_debug "peer not interested (was #{@peer_interested})" @peer_interested = false when :have # rt_debug "peer has piece #{m.index}" rt_warning "#{self}: peer already has piece #{m.index}" if @peer_pieces[m.index] @peer_pieces[m.index] = true @peer_virgin = false send_event(:peer_has_piece, m) recalc_interested when :bitfield # rt_debug "peer reports bitfield #{m.bitfield.inspect}" barray = m.bitfield.extend(StringToBarray).to_barray expected_pieces = @package.num_pieces - (@package.num_pieces % 8) + ((@package.num_pieces % 8) == 0 ? 0 : 8) raise ProtocolError, "invalid length in bitfield message (package has #{@package.num_pieces} pieces; bitfield should be size #{expected_pieces} but is #{barray.length} pieces)" unless barray.length == expected_pieces @peer_pieces.each_index { |i| @peer_pieces[i] = barray[i] } @peer_virgin = false send_event(:peer_has_pieces, barray) recalc_interested get_want_blocks when :request return rt_warning("#{self}: peer requests invalid piece #{m.index}") unless m.index < @package.num_pieces return rt_warning("#{self}: peer requests a block but we're choking") if @choking return rt_warning("#{self}: peer requests a block but isn't interested") unless @peer_interested return rt_warning("#{self}: peer requested too many blocks, ignoring") if @peer_want_blocks.length > MAX_PEER_REQUESTS piece = @package.pieces[m.index] return rt_warning("#{self}: peer requests unavailable block from piece #{piece}") unless piece.complete? @peer_want_blocks.push piece.get_complete_block(m.begin, m.length) when :piece raise "can't handle piece here" when :cancel b = Block.new(m.index, m.begin, m.length) # rt_debug "peer cancels #{b}" if @peer_want_blocks.delete2(b) == nil rt_warning "#{self}: peer wants to cancel unrequested block #{b}" end else raise "unknown message #{type}" end end ## queues a message for delivery. (for :piece messages, this ## transmits everything but the piece itself) def queue_message(id, args=nil) @send_q.push Message.new(id, args) end ## talks to Controller and get some new blocks to request. could be ## slow. this is presumably called whenever the queue of requests is ## too small. def get_want_blocks return if (@want_blocks.length >= MIN_REQUESTS) || @peer_virgin || @peer_choking || !@interested rej_count = 0 acc_count = 0 @controller.claim_blocks do |b| break if @want_blocks.length >= MAX_REQUESTS if @peer_pieces[b.pindex] && !@want_blocks.member?(b) rt_debug "! #{self}: starting new piece #{@package.pieces[b.pindex]}" unless @package.pieces[b.pindex].started? # rt_debug "#{self}: added to queue block #{b}" # puts "#{self}: claimed block #{b}" @want_blocks.push b acc_count += 1 true else # puts "#{self}: cont offers block #{b} but peer has? #{@peer_pieces[b.pindex]} i already want? #{@want_blocks.member? b}" if rej_count < 10 rej_count += 1 false end end # puts "#{self}: ... and #{rej_count} more (peer has #{@peer_pieces.inject(0) { |s, p| s + (p ? 1 : 0) }} pieces)... " if rej_count >= 10 # puts "#{self}: accepted #{acc_count} blocks, rejected #{rej_count} blocks" end end end rubytorrent-0.3/lib/rubytorrent/util.rb0000644000175000017500000001117211756644133020014 0ustar boutilboutil## util.rb -- miscellaneous RubyTorrent utility modules. ## Copyright 2005 William Morgan. ## ## This file is part of RubyTorrent. RubyTorrent is free software; ## you can redistribute it and/or modify it under the terms of version ## 2 of the GNU General Public License as published by the Free ## Software Foundation. ## ## RubyTorrent is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License (in the file COPYING) for more details. def rt_debug(*args) if $DEBUG || RubyTorrent.log stream = RubyTorrent.log || $stdout stream << args.join << "\n" stream.flush end end def rt_warning(*args) if $DEBUG || RubyTorrent.log stream = RubyTorrent.log || $stderr stream << "warning: " << args.join << "\n" stream.flush end end module RubyTorrent @log = nil def log_output_to(fn) @log = File.open(fn, "w") end attr_reader :log module_function :log_output_to, :log ## parse final hash of pseudo-keyword arguments def get_args(rest, *names) hash = rest.find { |x| x.is_a? Hash } if hash rest.delete hash hash.each { |k, v| raise ArgumentError, %{unknown argument "#{k}"} unless names.include?(k) } end [hash || {}, rest] end module_function :get_args ## "events": very similar to Observable, but cleaner, IMO. events are ## listened to and sent in instance space, but registered in class ## space. example: ## ## class C ## include EventSource ## event :goat, :boat ## ## def send_events ## send_event :goat ## send_event(:boat, 3) ## end ## end ## ## c = C.new ## c.on_event(:goat) { puts "got goat!" } ## c.on_event(:boat) { |x| puts "got boat: #{x}" } ## ## Defining them in class space is not really necessary, except as an ## error-checking mechanism. module EventSource def on_event(who, *events, &b) @event_handlers ||= Hash.new { [] } events.each do |e| raise ArgumentError, "unknown event #{e} for #{self.class}" unless (self.class.class_eval "@@event_has")[e] @event_handlers[e] <<= [who, b] end nil end def send_event(e, *args) raise ArgumentError, "unknown event #{e} for #{self.class}" unless (self.class.class_eval "@@event_has")[e] @event_handlers ||= Hash.new { [] } @event_handlers[e].each { |who, proc| proc[self, *args] } nil end def unregister_events(who, *events) @event_handlers.each do |event, handlers| handlers.each do |ewho, proc| if (ewho == who) && (events.empty? || events.member?(event)) @event_handlers[event].delete [who, proc] end end end nil end def relay_event(who, *events) @event_handlers ||= Hash.new { [] } events.each do |e| raise "unknown event #{e} for #{self.class}" unless (self.class.class_eval "@@event_has")[e] raise "unknown event #{e} for #{who.class}" unless (who.class.class_eval "@@event_has")[e] @event_handlers[e] <<= [who, lambda { |s, *a| who.send_event e, *a }] end nil end def self.append_features(mod) super(mod) mod.class_eval %q{ @@event_has ||= Hash.new(false) def self.event(*args) args.each { |a| @@event_has[a] = true } end } end end ## ensure that a method doesn't execute more frequently than some ## number of seconds. e.g.: ## ## def meth ## ... ## end ## min_iterval :meth, 10 ## ## ensures that "meth" won't be executed more than once every 10 ## seconds. module MinIntervalMethods def min_interval(meth, int) class_eval %{ @@min_interval ||= {} @@min_interval[:#{meth}] = [nil, #{int.to_i}] alias :min_interval_#{meth} :#{meth} def #{meth}(*a, &b) last, int = @@min_interval[:#{meth}] unless last && ((Time.now - last) < int) min_interval_#{meth}(*a, &b) @@min_interval[:#{meth}][0] = Time.now end end } end end ## boolean attributes now get question marks in their accessors ## don't forget to 'extend' rather than 'include' this one module AttrReaderQ def attr_reader_q(*args) args.each { |v| class_eval "def #{v}?; @#{v}; end" } end def attr_writer_q(*args) args.each { |v| attr_writer v } end def attr_accessor_q(*args) attr_reader_q args attr_writer_q args end end module ArrayShuffle def shuffle! each_index do |i| j = i + rand(self.size - i); self[i], self[j] = self[j], self[i] end end def shuffle self.clone.shuffle! # dup doesn't preserve shuffle! method end end module StringMapBytes def map_bytes ret = [] each_byte { |x| ret.push(yield(x)) } ret end end end rubytorrent-0.3/lib/rubytorrent/tracker.rb0000644000175000017500000001316011756644133020471 0ustar boutilboutil## tracker.rb -- bittorrent tracker protocol. ## Copyright 2004 William Morgan. ## ## This file is part of RubyTorrent. RubyTorrent is free software; ## you can redistribute it and/or modify it under the terms of version ## 2 of the GNU General Public License as published by the Free ## Software Foundation. ## ## RubyTorrent is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License (in the file COPYING) for more details. require 'open-uri' require 'timeout' require "rubytorrent" module RubyTorrent module HashAddition def +(o) ret = self.dup o.each { |k, v| ret[k] = v } ret end end ## am i insane or does 'uniq' not use == or === for some insane ## reason? wtf is that about? module ArrayUniq2 def uniq2 ret = [] each { |x| ret.push x unless ret.member? x } ret end end class TrackerResponsePeer attr_writer :tried def initialize(dict=nil) @s = TypedStruct.new do |s| s.field :peer_id => String, :ip => String, :port => Integer s.required :ip, :port s.label :peer_id => "peer id" end @s.parse(dict) unless dict.nil? @connected = false @tried = false end def tried?; @tried; end def method_missing(meth, *args) @s.send(meth, *args) end def ==(o); (self.ip == o.ip) && (self.port == o.port); end def to_s %{<#{self.class}: ip=#{self.ip}, port=#{self.port}>} end end class TrackerResponse def initialize(dict=nil) @s = TypedStruct.new do |s| s.field :interval => Integer, :complete => Integer, :incomplete => Integer, :peers => TrackerResponsePeer s.array :peers s.required :peers #:interval, :complete, :incomplete, :peers s.coerce :peers => lambda { |x| make_peers x } end @s.parse(dict) unless dict.nil? peers.extend ArrayShuffle end def method_missing(meth, *args) @s.send(meth, *args) end private def make_peers(x) case x when Array x.map { |e| TrackerResponsePeer.new e }.extend(ArrayUniq2).uniq2 when String x.unpack("a6" * (x.length / 6)).map do |y| TrackerResponsePeer.new({"ip" => (0..3).map { |i| y[i] }.join('.'), "port" => (y[4] << 8) + y[5] }) end.extend(ArrayUniq2).uniq2 else raise "don't know how to make peers array from #{x.class}" end end end class TrackerError < StandardError; end class TrackerConnection attr_reader :port, :left, :peer_id, :last_conn_time, :url, :in_force_refresh attr_accessor :uploaded, :downloaded, :left, :numwant def initialize(url, info_hash, length, port, peer_id, ip=nil, numwant=50, http_proxy=ENV["http_proxy"]) @url = url @hash = info_hash @length = length @port = port @uploaded = @downloaded = @left = 0 @ip = ip @numwant = numwant @peer_id = peer_id @http_proxy = http_proxy @state = :stopped @sent_completed = false @last_conn_time = nil @tracker_data = nil @compact = true @in_force_refresh = false end def already_completed; @sent_completed = true; end def sent_completed?; @sent_completed; end def started return unless @state == :stopped @state = :started @tracker_data = send_tracker "started" self end def stopped return unless @state == :started @state = :stopped @tracker_data = send_tracker "stopped" self end def completed return if @sent_completed @tracker_data = send_tracker "completed" @sent_completed = true self end def refresh return unless (Time.now - @last_conn_time) >= (interval || 0) @tracker_data = send_tracker nil end def force_refresh return if @in_force_refresh @in_force_refresh = true @tracker_data = send_tracker nil @in_force_refresh = false end [:interval, :seeders, :leechers, :peers].each do |m| class_eval %{ def #{m} if @tracker_data then @tracker_data.#{m} else nil end end } end private def send_tracker(event) resp = nil if @compact resp = get_tracker_response({ :event => event, :compact => 1 }) if resp["failure reason"] @compact = false end end resp = get_tracker_response({ :event => event }) unless resp raise TrackerError, "tracker reports error: #{resp['failure reason']}" if resp["failure reason"] TrackerResponse.new(resp) end def get_tracker_response(opts) target = @url.dup opts.extend HashAddition opts += {:info_hash => @hash, :peer_id => @peer_id, :port => @port, :uploaded => @uploaded, :downloaded => @downloaded, :left => @left, :numwant => @numwant, :ip => @ip} target.query = opts.map do |k, v| unless v.nil? ek = URI.escape(k.to_s) # sigh ev = URI.escape(v.to_s, /[^a-zA-Z0-9]/) "#{ek}=#{ev}" end end.compact.join "&" rt_debug "connecting to #{target.to_s} ..." ret = nil begin target.open(:proxy => @http_proxy) do |resp| BStream.new(resp).each do |e| if ret.nil? ret = e else raise TrackerError, "don't understand tracker response (too many objects)" end end end rescue SocketError, EOFError, OpenURI::HTTPError, RubyTorrent::TrackerError, Timeout::Error, SystemCallError, NoMethodError => e raise TrackerError, e.message end @last_conn_time = Time.now raise TrackerError, "empty tracker response" if ret.nil? raise TrackerError, "don't understand tracker response (not a dict)" unless ret.kind_of? ::Hash ret end end end rubytorrent-0.3/lib/rubytorrent/bencoding.rb0000644000175000017500000000737311756644133020777 0ustar boutilboutil## bencoding.rb -- parse and generate bencoded values. ## Copyright 2004 William Morgan. ## ## This file is part of RubyTorrent. RubyTorrent is free software; ## you can redistribute it and/or modify it under the terms of version ## 2 of the GNU General Public License as published by the Free ## Software Foundation. ## ## RubyTorrent is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License (in the file COPYING) for more details. require 'uri' require 'digest/sha1' module RubyTorrent ## we mess in the users' namespaces in this file. there's no good way ## around it. i don't think it's too egregious though. class BEncodingError < StandardError; end class BStream include Enumerable @@classes = [] def initialize(s) @s = s end def self.register_bencoded_class(c) @@classes.push c end def each happy = true begin happy = false c = @s.getc @@classes.each do |klass| if klass.bencoded? c o = klass.parse_bencoding(c, @s) happy = true yield o break end end unless c.nil? unless happy @s.ungetc c unless c.nil? end end while happy self end end end class String def to_bencoding self.length.to_s + ":" + self.to_s end def self.bencoded?(c) (?0 .. ?9).include? c end def self.parse_bencoding(c, s) lens = c.chr while ((x = s.getc) != ?:) unless (?0 .. ?9).include? x s.ungetc x raise RubyTorrent::BEncodingError, "invalid bencoded string length #{lens} + #{x}" end lens += x.chr end raise RubyTorrent::BEncodingError, %{invalid length #{lens} in bencoded string} unless lens.length <= 20 len = lens.to_i raise RubyTorrent::BEncodingError, %{invalid length #{lens} in bencoded string} unless len >= 0 (len > 0 ? s.read(len) : "") end RubyTorrent::BStream.register_bencoded_class self end class Integer def to_bencoding "i" + self.to_s + "e" end def self.bencoded?(c) c == ?i end def self.parse_bencoding(c, s) ints = "" while ((x = s.getc.chr) != 'e') raise RubyTorrent::BEncodingError, "invalid bencoded integer #{x.inspect}" unless x =~ /\d|-/ ints += x end raise RubyTorrent::BEncodingError, "invalid integer #{ints} (too long)" unless ints.length <= 20 int = ints.to_i raise RubyTorrent::BEncodingError, %{can't parse bencoded integer "#{ints}"} if (int == 0) && (ints !~ /^0$/) #' int end RubyTorrent::BStream.register_bencoded_class self end class Time def to_bencoding self.to_i.to_bencoding end end module URI def to_bencoding self.to_s.to_bencoding end end class Array def to_bencoding "l" + self.map { |e| e.to_bencoding }.join + "e" end def self.bencoded?(c) c == ?l end def self.parse_bencoding(c, s) ret = RubyTorrent::BStream.new(s).map { |x| x } raise RubyTorrent::BEncodingError, "missing list terminator" unless s.getc == ?e ret end RubyTorrent::BStream.register_bencoded_class self end class Hash def to_bencoding "d" + keys.sort.map do |k| v = self[k] if v.nil? nil else [k.to_bencoding, v.to_bencoding].join end end.compact.join + "e" end def self.bencoded?(c) c == ?d end def self.parse_bencoding(c, s) ret = {} key = nil RubyTorrent::BStream.new(s).each do |x| if key == nil key = x else ret[key] = x key = nil end end raise RubyTorrent::BEncodingError, "no dictionary terminator" unless s.getc == ?e ret end RubyTorrent::BStream.register_bencoded_class self end rubytorrent-0.3/lib/rubytorrent/message.rb0000644000175000017500000000761311756644133020470 0ustar boutilboutil## message.rb -- peer wire protocol message parsing/composition ## Copyright 2004 William Morgan. ## ## This file is part of RubyTorrent. RubyTorrent is free software; ## you can redistribute it and/or modify it under the terms of version ## 2 of the GNU General Public License as published by the Free ## Software Foundation. ## ## RubyTorrent is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License (in the file COPYING) for more details. ## we violate the users' namespaces here. but it's not in too ## egregious of a way, and it's a royal pita to remove, so i'm keeping ## it in for the time being. class String def from_fbbe # four-byte big-endian integer raise "fbbe must be four-byte string (got #{self.inspect})" unless length == 4 (self[0] << 24) + (self[1] << 16) + (self[2] << 8) + self[3] end end class Integer def to_fbbe # four-byte big-endian integer raise "fbbe must be < 2^32" unless self <= 2**32 raise "fbbe must be >= 0" unless self >= 0 s = " " s[0] = (self >> 24) % 256 s[1] = (self >> 16) % 256 s[2] = (self >> 8) % 256 s[3] = (self ) % 256 s end end module RubyTorrent module StringExpandBits include StringMapBytes def expand_bits # just for debugging purposes self.map_bytes do |b| (0 .. 7).map { |i| ((b & (1 << (7 - i))) == 0 ? "0" : "1") } end.flatten.join end end class Message WIRE_IDS = [:choke, :unchoke, :interested, :uninterested, :have, :bitfield, :request, :piece, :cancel] attr_accessor :id def initialize(id, args=nil) @id = id @args = args end def method_missing(meth) if @args.has_key? meth @args[meth] else raise %{no such argument "#{meth}" to message #{self.to_s}} end end def to_wire_form case @id when :keepalive 0.to_fbbe when :choke, :unchoke, :interested, :uninterested 1.to_fbbe + WIRE_IDS.index(@id).chr when :have 5.to_fbbe + 4.chr + @args[:index].to_fbbe when :bitfield (@args[:bitfield].length + 1).to_fbbe + 5.chr + @args[:bitfield] when :request, :cancel 13.to_fbbe + WIRE_IDS.index(@id).chr + @args[:index].to_fbbe + @args[:begin].to_fbbe + @args[:length].to_fbbe when :piece (@args[:length] + 9).to_fbbe + 7.chr + @args[:index].to_fbbe + @args[:begin].to_fbbe else raise "unknown message type #{id}" end end def self.from_wire_form(idnum, argstr) type = WIRE_IDS[idnum] case type when :choke, :unchoke, :interested, :uninterested raise ProtocolError, "invalid length #{argstr.length} for #{type} message" unless argstr.nil? or (argstr.length == 0) Message.new(type) when :have raise ProtocolError, "invalid length #{str.length} for #{type} message" unless argstr.length == 4 Message.new(type, {:index => argstr[0,4].from_fbbe}) when :bitfield Message.new(type, {:bitfield => argstr}) when :request, :cancel raise ProtocolError, "invalid length #{argstr.length} for #{type} message" unless argstr.length == 12 Message.new(type, {:index => argstr[0,4].from_fbbe, :begin => argstr[4,4].from_fbbe, :length => argstr[8,4].from_fbbe}) when :piece raise ProtocolError, "invalid length #{argstr.length} for #{type} message" unless argstr.length == 8 Message.new(type, {:index => argstr[0,4].from_fbbe, :begin => argstr[4,4].from_fbbe}) else raise "unknown message #{type.inspect}" end end def to_s case @id when :bitfield %{bitfield <#{@args[:bitfield].extend(StringExpandBits).expand_bits}>} else %{#@id#{@args.nil? ? "" : "(" + @args.map { |k, v| "#{k}=#{v.to_s.inspect}" }.join(", ") + ")"}} end end end end rubytorrent-0.3/lib/rubytorrent.rb0000644000175000017500000000623711756644133017045 0ustar boutilboutil## rubytorrent.rb -- top-level RubyTorrent file. ## Copyright 2004 William Morgan. ## ## This file is part of RubyTorrent. RubyTorrent is free software; ## you can redistribute it and/or modify it under the terms of version ## 2 of the GNU General Public License as published by the Free ## Software Foundation. ## ## RubyTorrent is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License (in the file COPYING) for more details. require 'rubytorrent/util' require 'rubytorrent/bencoding' require 'rubytorrent/metainfo' require 'rubytorrent/tracker' require 'rubytorrent/package' require 'rubytorrent/server' require "socket" Socket.do_not_reverse_lookup = true module RubyTorrent VERSION = 0.3 ## the top-level class for RubyTorrent. class BitTorrent include EventSource event :trying_peer, :forgetting_peer, :added_peer, :removed_peer, :received_block, :sent_block, :have_piece, :discarded_piece, :complete, :tracker_connected, :tracker_lost, :requested_block @@server = nil ## hash arguments: host, port, dlratelim, ulratelim def initialize(metainfo, *rest) args, rest = RubyTorrent::get_args(rest, :host, :port, :dlratelim, :ulratelim, :http_proxy) out = rest.shift raise ArgumentError, "wrong number of arguments (expected 0/1, got #{rest.length})" unless rest.empty? case metainfo when MetaInfo @metainfo = metainfo when String @metainfo = MetaInfo.from_location(metainfo) when IO @metainfo = MetaInfo.from_stream(metainfo) else raise ArgumentError, "'metainfo' should be a String, IO or RubyTorrent::MetaInfo object" end case out when Package @package = out else @package = Package.new(@metainfo, out) end unless @@server @@server = RubyTorrent::Server.new(args[:host], args[:port], args[:http_proxy]) @@server.start end @cont = @@server.add_torrent(@metainfo, @package, args[:dlratelim], args[:ulratelim]) @cont.relay_event self, :trying_peer, :forgetting_peer, :added_peer, :removed_peer, :received_block, :sent_block, :have_piece, :discarded_piece, :tracker_connected, :tracker_lost, :requested_block @package.relay_event self, :complete end def ip; @@server.ip; end def port; @@server.port; end def peer_info; @cont.peer_info; end def shutdown; @cont.shutdown; end def shutdown_all; @@server.shutdown; end def complete?; @package.complete?; end def bytes_completed; @package.bytes_completed; end def percent_completed; @package.percent_completed; end def pieces_completed; @package.pieces_completed; end def dlrate; @cont.dlrate; end def ulrate; @cont.ulrate; end def dlamt; @cont.dlamt; end def ulamt; @cont.ulamt; end def num_pieces; @package.num_pieces; end def tracker; (@cont.tracker ? @cont.tracker.url : nil); end def num_possible_peers; (@cont.tracker ? @cont.tracker.peers.length : 0); end def num_active_peers; @cont.num_peers; end def total_bytes; @package.size; end end end rubytorrent-0.3/make-metainfo.rb0000644000175000017500000001314311756644133016407 0ustar boutilboutil## make-metainfo.rb -- interactive .torrent creater ## Copyright 2005 William Morgan. ## ## This file is part of RubyTorrent. RubyTorrent is free software; ## you can redistribute it and/or modify it under the terms of version ## 2 of the GNU General Public License as published by the Free ## Software Foundation. ## ## RubyTorrent is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License (in the file COPYING) for more details. require 'digest/sha1' require "rubytorrent" def die(x); $stderr << "#{x}\n" && exit(-1); end def syntax %{ Syntax: make-metainfo.rb []+ make-metainfo is an interactive program for creating .torrent files from a set of files or directories. any directories specified will be scanned recursively. } end def find_files(f) if FileTest.directory? f Dir.new(f).entries.map { |x| find_files(File.join(f, x)) unless x =~ /^\.[\.\/]*$/}.compact else f end end class Numeric def to_size_s if self < 1024 "#{self.round}b" elsif self < 1024**2 "#{(self / 1024.0).round}kb" elsif self < 1024**3 "#{(self / (1024.0**2)).round}mb" else "#{(self / (1024.0**3)).round}gb" end end end def read_pieces(files, length) buf = "" files.each do |f| File.open(f) do |fh| begin read = fh.read(length - buf.length) if (buf.length + read.length) == length yield(buf + read) buf = "" else buf += read end end until fh.eof? end end yield buf end die syntax if ARGV.length == 0 puts "Scanning..." files = ARGV.map { |f| find_files f }.flatten single = files.length == 1 puts "Building #{(single ? 'single' : 'multi')}-file .torrent for #{files.length} file#{(single ? '' : 's')}." mi = RubyTorrent::MetaInfo.new mii = RubyTorrent::MetaInfoInfo.new maybe_name = if single ARGV[0] else (File.directory?(ARGV[0]) ? File.basename(ARGV[0]) : File.basename(File.dirname(ARGV[0]))) end puts print %{Default output file/directory name (enter for "#{maybe_name}"): } name = $stdin.gets.chomp mii.name = (name == "" ? maybe_name : name) puts %{We'll use "#{mii.name}".} puts puts "Measuring..." length = nil if single length = mii.length = files.inject(0) { |s, f| s + File.size(f) } else mii.files = [] length = files.inject(0) do |s, f| miif = RubyTorrent::MetaInfoInfoFile.new miif.length = File.size f miif.path = f.split File::SEPARATOR miif.path = miif.path[1, miif.path.length - 1] if miif.path[0] == mii.name mii.files << miif s + miif.length end end puts < #{num_pieces} pieces and .torrent size of approx. #{tsize.to_size_s}." break if tsize < 10240 end maybe_plen = [size, 256].min begin print "Piece size in kb (enter for #{maybe_plen}k): " plen = $stdin.gets.chomp end while plen !~ /^\d*$/ plen = (plen == "" ? maybe_plen : plen.to_i) mii.piece_length = plen * 1024 num_pieces = (length.to_f / mii.piece_length.to_f).ceil puts "Using piece size of #{plen}kb => .torrent size of approx. #{(num_pieces * 20.0).to_size_s}." print "Calculating #{num_pieces} piece SHA1s... " ; $stdout.flush mii.pieces = "" i = 0 read_pieces(files, mii.piece_length) do |piece| mii.pieces += Digest::SHA1.digest(piece) i += 1 if (i % 100) == 0 print "#{(i.to_f / num_pieces * 100.0).round}%... "; $stdout.flush end end puts "done" mi.info = mii puts <