require 'socket' unless defined?(Socket) module Tor ## # Tor Control Protocol (TC) client. # # The Tor control protocol is used by other programs (such as frontend # user interfaces) to communicate with a locally running Tor process. It # is not part of the Tor onion routing protocol. # # @example Establishing a controller connection (1) # tor = Tor::Controller.new # # @example Establishing a controller connection (2) # tor = Tor::Controller.new(:host => '127.0.0.1', :port => 9051) # # @example Authenticating the controller connection # tor.authenticate # # @example Obtaining information about the Tor process # tor.version #=> "0.2.1.25" # tor.config_file #=> # # # @see http://gitweb.torproject.org/tor.git?a=blob_plain;hb=HEAD;f=doc/spec/control-spec.txt # @see http://www.thesprawl.org/memdump/?entry=8 # @since 0.1.1 class Controller PROTOCOL_VERSION = 1 ## # @param [Hash{Symbol => Object}] options # @option options [String, #to_s] :host ("127.0.0.1") # @option options [Integer, #to_i] :port (9051) # @option options [String, #to_s] :cookie (nil) # @option options [Integer, #to_i] :version (PROTOCOL_VERSION) def self.connect(options = {}, &block) if block_given? result = block.call(tor = self.new(options)) tor.quit result else self.new(options) end end ## # @param [Hash{Symbol => Object}] options # @option options [String, #to_s] :host ("127.0.0.1") # @option options [Integer, #to_i] :port (9051) # @option options [String, #to_s] :cookie (nil) # @option options [Integer, #to_i] :version (PROTOCOL_VERSION) def initialize(options = {}, &block) @options = options.dup @host = (@options.delete(:host) || '127.0.0.1').to_s @port = (@options.delete(:port) || 9051).to_i @version = (@options.delete(:version) || PROTOCOL_VERSION).to_i connect if block_given? block.call(self) quit end end attr_reader :host, :port ## # Establishes the socket connection to the Tor process. # # @example # tor.close # tor.connect # # @return [void] def connect close @socket = TCPSocket.new(@host, @port) @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true) self end ## # Returns `true` if the controller connection is active. # # @example # tor.connected? #=> true # tor.close # tor.connected? #=> false # # @return [Boolean] def connected? !!@socket end ## # Closes the socket connection to the Tor process. # # @example # tor.close # # @return [void] def close @socket.close if @socket @socket = nil self end ## # Tells the Tor process to hang up on this controller connection. # # This command can be used before authenticating. # # @example # C: QUIT # S: 250 closing connection # ^D # # @example # tor.quit # # @return [void] def quit send_line('QUIT') reply = read_reply close reply end ## # Returns information about the authentication method required by the # Tor process. # # This command may be used before authenticating. # # @example # C: PROTOCOLINFO # S: 250-PROTOCOLINFO 1 # S: 250-AUTH METHODS=NULL # S: 250-VERSION Tor="0.2.1.25" # S: 250 OK # # @example # tor.authentication_method #=> nil # tor.authentication_method #=> :hashedpassword # tor.authentication_method #=> :cookie # # @return [Symbol] # @since 0.1.2 def authentication_method @authentication_method ||= begin method = nil send_line('PROTOCOLINFO') loop do # TODO: support for reading multiple authentication methods case reply = read_reply when /^250-AUTH METHODS=(\w*)/ method = $1.strip.downcase.to_sym method = method.eql?(:null) ? nil : method when /^250-/ then next when '250 OK' then break end end method end end ## # Returns `true` if the controller connection has been authenticated. # # @example # tor.authenticated? #=> false # tor.authenticate # tor.authenticated? #=> true # # @return [Boolean] def authenticated? @authenticated || false end ## # Authenticates the controller connection. # # @example # C: AUTHENTICATE # S: 250 OK # # @example # tor.authenticate # # @return [void] # @raise [AuthenticationError] if authentication failed def authenticate(cookie = nil) cookie ||= @options[:cookie] send(:send_line, cookie ? "AUTHENTICATE \"#{cookie}\"" : "AUTHENTICATE") case reply = read_reply when '250 OK' then @authenticated = true else raise AuthenticationError.new(reply) end self end ## # Returns the version number of the Tor process. # # @example # C: GETINFO version # S: 250-version=0.2.1.25 # S: 250 OK # # @example # tor.version #=> "0.2.1.25" # # @return [String] def version send_command(:getinfo, 'version') reply = read_reply.split('=').last read_reply # skip "250 OK" reply end ## # Returns the path to the Tor configuration file. # # @example # C: GETINFO config-file # S: 250-config-file=/opt/local/etc/tor/torrc # S: 250 OK # # @example # tor.config_file #=> # # # @return [Pathname] def config_file send_command(:getinfo, 'config-file') reply = read_reply.split('=').last read_reply # skip "250 OK" Pathname(reply) end ## # Returns the current (in-memory) Tor configuration. # Response is terminated with a "." # # @example # C: GETINFO config-text # S: 250+config-text= # S: ControlPort 9051 # S: RunAsDaemon 1 # S: . def config_text send_command(:getinfo, 'config-text') reply = "" read_reply # skip "250+config-text=" while line = read_reply break unless line != "." reply.concat(line + "\n") end read_reply # skip "250 OK" return reply end ## # Send a signal to the server # # @example # tor.signal("newnym") # # @return [String] def signal(name) send_command(:signal, name) read_reply end protected ## # Sends a command line over the socket. # # @param [Symbol, #to_s] command # @param [Array] args # @return [void] def send_command(command, *args) authenticate unless authenticated? send_line(["#{command.to_s.upcase}", *args].join(' ')) end ## # Sends a text line over the socket. # # @param [String, #to_s] line # @return [void] def send_line(line) @socket.write(line.to_s + "\r\n") @socket.flush end ## # Reads a reply line from the socket. # # @return [String] def read_reply @socket.readline.chomp end ## # Used to signal an authentication error. # # @see Tor::Controller#authenticate class AuthenticationError < StandardError; end end end