2012-06-29 05:18:28 +00:00
|
|
|
# -*- coding: binary -*-
|
2005-04-10 01:17:19 +00:00
|
|
|
|
|
|
|
require 'socket'
|
2009-07-22 18:37:51 +00:00
|
|
|
require 'openssl'
|
|
|
|
|
2006-09-19 03:15:25 +00:00
|
|
|
require 'rex/script'
|
2005-07-09 21:18:49 +00:00
|
|
|
require 'rex/post/meterpreter/client_core'
|
|
|
|
require 'rex/post/meterpreter/channel'
|
|
|
|
require 'rex/post/meterpreter/channel_container'
|
|
|
|
require 'rex/post/meterpreter/dependencies'
|
|
|
|
require 'rex/post/meterpreter/object_aliases'
|
|
|
|
require 'rex/post/meterpreter/packet'
|
|
|
|
require 'rex/post/meterpreter/packet_parser'
|
|
|
|
require 'rex/post/meterpreter/packet_dispatcher'
|
2017-07-11 09:42:56 +00:00
|
|
|
require 'rex/post/meterpreter/pivot'
|
2017-07-10 09:59:41 +00:00
|
|
|
require 'rex/post/meterpreter/pivot_container'
|
2005-04-10 01:17:19 +00:00
|
|
|
|
|
|
|
module Rex
|
|
|
|
module Post
|
|
|
|
module Meterpreter
|
|
|
|
|
2005-07-18 07:46:54 +00:00
|
|
|
#
|
|
|
|
# Just to get it in there...
|
|
|
|
#
|
|
|
|
module Extensions
|
|
|
|
end
|
|
|
|
|
2005-04-10 01:17:19 +00:00
|
|
|
###
|
|
|
|
#
|
2005-11-15 05:22:13 +00:00
|
|
|
# This class represents a logical meterpreter client class. This class
|
|
|
|
# provides an interface that is compatible with the Rex post-exploitation
|
|
|
|
# interface in terms of the feature set that it attempts to expose. This
|
|
|
|
# class is meant to drive a single meterpreter client session.
|
2005-04-10 01:17:19 +00:00
|
|
|
#
|
|
|
|
###
|
|
|
|
class Client
|
|
|
|
|
2013-08-30 21:28:33 +00:00
|
|
|
include Rex::Post::Meterpreter::PacketDispatcher
|
|
|
|
include Rex::Post::Meterpreter::ChannelContainer
|
2017-07-11 09:42:56 +00:00
|
|
|
include Rex::Post::Meterpreter::PivotContainer
|
2013-08-30 21:28:33 +00:00
|
|
|
|
|
|
|
#
|
|
|
|
# Extension name to class hash.
|
|
|
|
#
|
|
|
|
@@ext_hash = {}
|
|
|
|
|
|
|
|
#
|
2014-11-27 03:23:08 +00:00
|
|
|
# Cached auto-generated SSL certificate
|
2014-11-22 21:35:00 +00:00
|
|
|
#
|
|
|
|
@@ssl_cached_cert = nil
|
2013-08-30 21:28:33 +00:00
|
|
|
|
|
|
|
#
|
|
|
|
# Mutex to synchronize class-wide operations
|
|
|
|
#
|
|
|
|
@@ssl_mutex = ::Mutex.new
|
|
|
|
|
|
|
|
#
|
|
|
|
# Lookup the error that occurred
|
|
|
|
#
|
|
|
|
def self.lookup_error(code)
|
|
|
|
code
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# Checks the extension hash to see if a class has already been associated
|
|
|
|
# with the supplied extension name.
|
|
|
|
#
|
|
|
|
def self.check_ext_hash(name)
|
|
|
|
@@ext_hash[name]
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# Stores the name to class association for the supplied extension name.
|
|
|
|
#
|
|
|
|
def self.set_ext_hash(name, klass)
|
|
|
|
@@ext_hash[name] = klass
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# Initializes the client context with the supplied socket through
|
|
|
|
# which communication with the server will be performed.
|
|
|
|
#
|
2017-06-21 11:01:59 +00:00
|
|
|
def initialize(sock, opts={})
|
2013-08-30 21:28:33 +00:00
|
|
|
init_meterpreter(sock, opts)
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# Cleans up the meterpreter instance, terminating the dispatcher thread.
|
|
|
|
#
|
|
|
|
def cleanup_meterpreter
|
2017-07-18 10:57:46 +00:00
|
|
|
self.pivots.keys.each do |k|
|
|
|
|
pivot = self.pivots[k]
|
|
|
|
pivot.pivoted_session.kill('Pivot closed')
|
|
|
|
pivot.pivoted_session.shutdown_passive_dispatcher
|
|
|
|
end
|
|
|
|
|
|
|
|
if self.pivot_session
|
|
|
|
self.pivot_session.remove_pivot(self.session_guid)
|
|
|
|
end
|
|
|
|
|
2015-05-04 08:52:55 +00:00
|
|
|
if not self.skip_cleanup
|
2015-05-04 08:40:48 +00:00
|
|
|
ext.aliases.each_value do | extension |
|
|
|
|
extension.cleanup if extension.respond_to?( 'cleanup' )
|
|
|
|
end
|
2013-08-30 21:28:33 +00:00
|
|
|
end
|
2015-05-04 08:40:48 +00:00
|
|
|
|
2013-08-30 21:28:33 +00:00
|
|
|
dispatcher_thread.kill if dispatcher_thread
|
2015-05-04 08:40:48 +00:00
|
|
|
|
2015-05-04 08:52:55 +00:00
|
|
|
if not self.skip_cleanup
|
2015-05-04 08:40:48 +00:00
|
|
|
core.shutdown rescue nil
|
|
|
|
end
|
|
|
|
|
2013-08-30 21:28:33 +00:00
|
|
|
shutdown_passive_dispatcher
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# Initializes the meterpreter client instance
|
|
|
|
#
|
|
|
|
def init_meterpreter(sock,opts={})
|
|
|
|
self.sock = sock
|
|
|
|
self.parser = PacketParser.new
|
|
|
|
self.ext = ObjectAliases.new
|
|
|
|
self.ext_aliases = ObjectAliases.new
|
|
|
|
self.alive = true
|
|
|
|
self.target_id = opts[:target_id]
|
|
|
|
self.capabilities = opts[:capabilities] || {}
|
|
|
|
self.commands = []
|
2015-12-03 15:07:34 +00:00
|
|
|
self.last_checkin = Time.now
|
2013-08-30 21:28:33 +00:00
|
|
|
|
|
|
|
self.conn_id = opts[:conn_id]
|
|
|
|
self.url = opts[:url]
|
|
|
|
self.ssl = opts[:ssl]
|
2017-07-16 09:32:58 +00:00
|
|
|
|
2017-07-11 09:42:56 +00:00
|
|
|
self.pivot_session = opts[:pivot_session]
|
2017-07-16 09:32:58 +00:00
|
|
|
if self.pivot_session
|
|
|
|
self.expiration = self.pivot_session.expiration
|
|
|
|
self.comm_timeout = self.pivot_session.comm_timeout
|
|
|
|
self.retry_total = self.pivot_session.retry_total
|
|
|
|
self.retry_wait = self.pivot_session.retry_wait
|
|
|
|
else
|
|
|
|
self.expiration = opts[:expiration]
|
|
|
|
self.comm_timeout = opts[:comm_timeout]
|
|
|
|
self.retry_total = opts[:retry_total]
|
|
|
|
self.retry_wait = opts[:retry_wait]
|
|
|
|
self.passive_dispatcher = opts[:passive_dispatcher]
|
|
|
|
end
|
|
|
|
|
2013-08-30 21:28:33 +00:00
|
|
|
self.response_timeout = opts[:timeout] || self.class.default_timeout
|
|
|
|
self.send_keepalives = true
|
2014-11-22 21:35:00 +00:00
|
|
|
|
|
|
|
# TODO: Clarify why we don't allow unicode to be set in initial options
|
2013-08-30 21:28:33 +00:00
|
|
|
# self.encode_unicode = opts.has_key?(:encode_unicode) ? opts[:encode_unicode] : true
|
|
|
|
self.encode_unicode = false
|
|
|
|
|
2017-06-21 11:01:59 +00:00
|
|
|
self.aes_key = nil
|
2017-07-11 09:42:56 +00:00
|
|
|
self.session_guid = opts[:session_guid] || "\x00" * 16
|
2017-06-21 11:01:59 +00:00
|
|
|
|
2014-11-22 21:35:00 +00:00
|
|
|
# The SSL certificate is being passed down as a file path
|
|
|
|
if opts[:ssl_cert]
|
2016-04-20 12:11:34 +00:00
|
|
|
if ! ::File.exist? opts[:ssl_cert]
|
2014-11-22 21:35:00 +00:00
|
|
|
elog("SSL certificate at #{opts[:ssl_cert]} does not exist and will be ignored")
|
|
|
|
else
|
|
|
|
# Load the certificate the same way that SslTcpServer does it
|
|
|
|
self.ssl_cert = ::File.read(opts[:ssl_cert])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-07-11 09:42:56 +00:00
|
|
|
initialize_passive_dispatcher if opts[:passive_dispatcher]
|
2013-08-30 21:28:33 +00:00
|
|
|
|
2017-07-11 09:42:56 +00:00
|
|
|
register_extension_alias('core', ClientCore.new(self))
|
2013-08-30 21:28:33 +00:00
|
|
|
|
2017-07-11 09:42:56 +00:00
|
|
|
initialize_inbound_handlers
|
|
|
|
initialize_channels
|
|
|
|
initialize_pivots
|
2013-08-30 21:28:33 +00:00
|
|
|
|
2017-07-11 09:42:56 +00:00
|
|
|
# Register the channel and pivot inbound packet handlers
|
|
|
|
register_inbound_handler(Rex::Post::Meterpreter::Channel)
|
|
|
|
register_inbound_handler(Rex::Post::Meterpreter::Pivot)
|
2013-08-30 21:28:33 +00:00
|
|
|
|
2017-07-11 09:42:56 +00:00
|
|
|
unless opts[:passive_dispatcher] || opts[:pivot_session]
|
|
|
|
monitor_socket
|
2013-08-30 21:28:33 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def swap_sock_plain_to_ssl
|
|
|
|
# Create a new SSL session on the existing socket
|
|
|
|
ctx = generate_ssl_context()
|
|
|
|
ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx)
|
|
|
|
|
|
|
|
# Use non-blocking OpenSSL operations on Windows
|
|
|
|
if !( ssl.respond_to?(:accept_nonblock) and Rex::Compat.is_windows )
|
|
|
|
ssl.accept
|
|
|
|
else
|
|
|
|
begin
|
|
|
|
ssl.accept_nonblock
|
|
|
|
|
|
|
|
# Ruby 1.8.7 and 1.9.0/1.9.1 uses a standard Errno
|
|
|
|
rescue ::Errno::EAGAIN, ::Errno::EWOULDBLOCK
|
|
|
|
IO::select(nil, nil, nil, 0.10)
|
|
|
|
retry
|
|
|
|
|
|
|
|
# Ruby 1.9.2+ uses IO::WaitReadable/IO::WaitWritable
|
|
|
|
rescue ::Exception => e
|
|
|
|
if ::IO.const_defined?('WaitReadable') and e.kind_of?(::IO::WaitReadable)
|
|
|
|
IO::select( [ ssl ], nil, nil, 0.10 )
|
|
|
|
retry
|
|
|
|
end
|
|
|
|
|
|
|
|
if ::IO.const_defined?('WaitWritable') and e.kind_of?(::IO::WaitWritable)
|
|
|
|
IO::select( nil, [ ssl ], nil, 0.10 )
|
|
|
|
retry
|
|
|
|
end
|
|
|
|
|
|
|
|
raise e
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
self.sock.extend(Rex::Socket::SslTcp)
|
|
|
|
self.sock.sslsock = ssl
|
|
|
|
self.sock.sslctx = ctx
|
2015-04-06 02:46:23 +00:00
|
|
|
self.sock.sslhash = Rex::Text.sha1_raw(ctx.cert.to_der)
|
2013-08-30 21:28:33 +00:00
|
|
|
|
|
|
|
tag = self.sock.get_once(-1, 30)
|
|
|
|
if(not tag or tag !~ /^GET \//)
|
|
|
|
raise RuntimeError, "Could not read the HTTP hello token"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def swap_sock_ssl_to_plain
|
|
|
|
# Remove references to the SSLSocket and Context
|
|
|
|
self.sock.sslsock.close
|
|
|
|
self.sock.sslsock = nil
|
|
|
|
self.sock.sslctx = nil
|
2015-04-06 02:46:23 +00:00
|
|
|
self.sock.sslhash = nil
|
2013-08-30 21:28:33 +00:00
|
|
|
self.sock = self.sock.fd
|
|
|
|
self.sock.extend(::Rex::Socket::Tcp)
|
|
|
|
end
|
|
|
|
|
|
|
|
def generate_ssl_context
|
2014-11-22 21:35:00 +00:00
|
|
|
|
|
|
|
ctx = nil
|
2014-11-27 03:23:08 +00:00
|
|
|
ssl_cert_info = nil
|
2014-11-22 21:35:00 +00:00
|
|
|
|
2014-11-27 03:23:08 +00:00
|
|
|
loop do
|
2014-11-22 21:35:00 +00:00
|
|
|
|
2014-11-27 03:23:08 +00:00
|
|
|
# Load a custom SSL certificate if one has been specified
|
|
|
|
if self.ssl_cert
|
|
|
|
wlog("Loading custom SSL certificate for Meterpreter session")
|
|
|
|
ssl_cert_info = Rex::Socket::SslTcpServer.ssl_parse_pem(self.ssl_cert)
|
|
|
|
wlog("Loaded custom SSL certificate for Meterpreter session")
|
|
|
|
break
|
2014-11-22 21:35:00 +00:00
|
|
|
end
|
|
|
|
|
2014-11-27 03:23:08 +00:00
|
|
|
# Generate a certificate if necessary and cache it
|
|
|
|
if ! @@ssl_cached_cert
|
|
|
|
@@ssl_mutex.synchronize do
|
2014-11-22 21:35:00 +00:00
|
|
|
wlog("Generating SSL certificate for Meterpreter sessions")
|
2014-11-27 03:23:08 +00:00
|
|
|
@@ssl_cached_cert = Rex::Socket::SslTcpServer.ssl_generate_certificate
|
2014-11-22 21:35:00 +00:00
|
|
|
wlog("Generated SSL certificate for Meterpreter sessions")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-11-27 03:23:08 +00:00
|
|
|
# Use the cached certificate
|
|
|
|
ssl_cert_info = @@ssl_cached_cert
|
|
|
|
break
|
|
|
|
end
|
|
|
|
|
|
|
|
# Create a new context for each session
|
|
|
|
ctx = OpenSSL::SSL::SSLContext.new()
|
|
|
|
ctx.key = ssl_cert_info[0]
|
|
|
|
ctx.cert = ssl_cert_info[1]
|
|
|
|
ctx.extra_chain_cert = ssl_cert_info[2]
|
|
|
|
ctx.options = 0
|
|
|
|
ctx.session_id_context = Rex::Text.rand_text(16)
|
2013-08-30 21:28:33 +00:00
|
|
|
|
2014-11-22 21:35:00 +00:00
|
|
|
ctx
|
2013-08-30 21:28:33 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
#
|
|
|
|
# Accessors
|
|
|
|
#
|
|
|
|
##
|
|
|
|
|
|
|
|
#
|
|
|
|
# Returns the default timeout that request packets will use when
|
|
|
|
# waiting for a response.
|
|
|
|
#
|
|
|
|
def Client.default_timeout
|
|
|
|
return 300
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
#
|
|
|
|
# Alias processor
|
|
|
|
#
|
|
|
|
##
|
|
|
|
|
|
|
|
#
|
|
|
|
# Translates unhandled methods into registered extension aliases
|
|
|
|
# if a matching extension alias exists for the supplied symbol.
|
|
|
|
#
|
|
|
|
def method_missing(symbol, *args)
|
|
|
|
#$stdout.puts("method_missing: #{symbol}")
|
|
|
|
self.ext_aliases.aliases[symbol.to_s]
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
#
|
|
|
|
# Extension registration
|
|
|
|
#
|
|
|
|
##
|
|
|
|
|
|
|
|
#
|
|
|
|
# Loads the client half of the supplied extension and initializes it as a
|
|
|
|
# registered extension that can be reached through client.ext.[extension].
|
|
|
|
#
|
|
|
|
def add_extension(name, commands=[])
|
|
|
|
self.commands |= commands
|
|
|
|
|
|
|
|
# Check to see if this extension has already been loaded.
|
|
|
|
if ((klass = self.class.check_ext_hash(name.downcase)) == nil)
|
|
|
|
old = Rex::Post::Meterpreter::Extensions.constants
|
|
|
|
require("rex/post/meterpreter/extensions/#{name.downcase}/#{name.downcase}")
|
|
|
|
new = Rex::Post::Meterpreter::Extensions.constants
|
|
|
|
|
|
|
|
# No new constants added?
|
|
|
|
if ((diff = new - old).empty?)
|
|
|
|
diff = [ name.capitalize ]
|
|
|
|
end
|
|
|
|
|
|
|
|
klass = Rex::Post::Meterpreter::Extensions.const_get(diff[0]).const_get(diff[0])
|
|
|
|
|
|
|
|
# Save the module name to class association now that the code is
|
|
|
|
# loaded.
|
|
|
|
self.class.set_ext_hash(name.downcase, klass)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Create a new instance of the extension
|
|
|
|
inst = klass.new(self)
|
|
|
|
|
|
|
|
self.ext.aliases[inst.name] = inst
|
|
|
|
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# Deregisters an extension alias of the supplied name.
|
|
|
|
#
|
|
|
|
def deregister_extension(name)
|
|
|
|
self.ext.aliases.delete(name)
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# Enumerates all of the loaded extensions.
|
|
|
|
#
|
|
|
|
def each_extension(&block)
|
|
|
|
self.ext.aliases.each(block)
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# Registers an aliased extension that can be referenced through
|
|
|
|
# client.name.
|
|
|
|
#
|
|
|
|
def register_extension_alias(name, ext)
|
|
|
|
self.ext_aliases.aliases[name] = ext
|
|
|
|
# Whee! Syntactic sugar, where art thou?
|
|
|
|
#
|
|
|
|
# Create an instance method on this object called +name+ that returns
|
|
|
|
# +ext+. We have to do it this way instead of simply
|
|
|
|
# self.class.class_eval so that other meterpreter sessions don't get
|
|
|
|
# extension methods when this one does
|
|
|
|
(class << self; self; end).class_eval do
|
|
|
|
define_method(name.to_sym) do
|
|
|
|
ext
|
|
|
|
end
|
|
|
|
end
|
|
|
|
ext
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# Registers zero or more aliases that are provided in an array.
|
|
|
|
#
|
|
|
|
def register_extension_aliases(aliases)
|
|
|
|
aliases.each { |a|
|
|
|
|
register_extension_alias(a['name'], a['ext'])
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# Deregisters a previously registered extension alias.
|
|
|
|
#
|
|
|
|
def deregister_extension_alias(name)
|
|
|
|
self.ext_aliases.aliases.delete(name)
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# Dumps the extension tree.
|
|
|
|
#
|
|
|
|
def dump_extension_tree()
|
|
|
|
items = []
|
|
|
|
items.concat(self.ext.dump_alias_tree('client.ext'))
|
|
|
|
items.concat(self.ext_aliases.dump_alias_tree('client'))
|
|
|
|
|
|
|
|
return items.sort
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# Encodes (or not) a UTF-8 string
|
|
|
|
#
|
|
|
|
def unicode_filter_encode(str)
|
|
|
|
self.encode_unicode ? Rex::Text.unicode_filter_encode(str) : str
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# Decodes (or not) a UTF-8 string
|
|
|
|
#
|
|
|
|
def unicode_filter_decode(str)
|
|
|
|
self.encode_unicode ? Rex::Text.unicode_filter_decode(str) : str
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# The extension alias under which all extensions can be accessed by name.
|
|
|
|
# For example:
|
|
|
|
#
|
|
|
|
# client.ext.stdapi
|
|
|
|
#
|
|
|
|
#
|
|
|
|
attr_reader :ext
|
|
|
|
#
|
|
|
|
# The socket the client is communicating over.
|
|
|
|
#
|
|
|
|
attr_reader :sock
|
|
|
|
#
|
|
|
|
# The timeout value to use when waiting for responses.
|
|
|
|
#
|
|
|
|
attr_accessor :response_timeout
|
|
|
|
#
|
|
|
|
# Whether to send pings every so often to determine liveness.
|
|
|
|
#
|
|
|
|
attr_accessor :send_keepalives
|
|
|
|
#
|
|
|
|
# Whether this session is alive. If the socket is disconnected or broken,
|
|
|
|
# this will be false
|
|
|
|
#
|
|
|
|
attr_accessor :alive
|
|
|
|
#
|
|
|
|
# The unique target identifier for this payload
|
|
|
|
#
|
|
|
|
attr_accessor :target_id
|
|
|
|
#
|
|
|
|
# The libraries available to this meterpreter server
|
|
|
|
#
|
|
|
|
attr_accessor :capabilities
|
|
|
|
#
|
|
|
|
# The Connection ID
|
|
|
|
#
|
|
|
|
attr_accessor :conn_id
|
|
|
|
#
|
|
|
|
# The Connect URL
|
|
|
|
#
|
|
|
|
attr_accessor :url
|
|
|
|
#
|
|
|
|
# Use SSL (HTTPS)
|
|
|
|
#
|
|
|
|
attr_accessor :ssl
|
|
|
|
#
|
2014-11-22 21:35:00 +00:00
|
|
|
# Use this SSL Certificate (unified PEM)
|
|
|
|
#
|
|
|
|
attr_accessor :ssl_cert
|
|
|
|
#
|
2013-08-30 21:28:33 +00:00
|
|
|
# The Session Expiration Timeout
|
|
|
|
#
|
|
|
|
attr_accessor :expiration
|
|
|
|
#
|
|
|
|
# The Communication Timeout
|
|
|
|
#
|
|
|
|
attr_accessor :comm_timeout
|
|
|
|
#
|
2015-04-09 07:57:43 +00:00
|
|
|
# The total time for retrying connections
|
|
|
|
#
|
|
|
|
attr_accessor :retry_total
|
|
|
|
#
|
|
|
|
# The time to wait between retry attempts
|
|
|
|
#
|
|
|
|
attr_accessor :retry_wait
|
|
|
|
#
|
2013-08-30 21:28:33 +00:00
|
|
|
# The Passive Dispatcher
|
|
|
|
#
|
|
|
|
attr_accessor :passive_dispatcher
|
|
|
|
#
|
2017-07-11 09:42:56 +00:00
|
|
|
# Reference to a session to pivot through
|
|
|
|
#
|
|
|
|
attr_accessor :pivot_session
|
|
|
|
#
|
2013-08-30 21:28:33 +00:00
|
|
|
# Flag indicating whether to hex-encode UTF-8 file names and other strings
|
|
|
|
#
|
|
|
|
attr_accessor :encode_unicode
|
|
|
|
#
|
|
|
|
# A list of the commands
|
|
|
|
#
|
|
|
|
attr_reader :commands
|
2015-05-03 15:52:54 +00:00
|
|
|
#
|
|
|
|
# The timestamp of the last received response
|
|
|
|
#
|
|
|
|
attr_accessor :last_checkin
|
2010-04-03 05:21:15 +00:00
|
|
|
|
2005-04-12 05:37:11 +00:00
|
|
|
protected
|
2013-08-30 21:28:33 +00:00
|
|
|
attr_accessor :parser, :ext_aliases # :nodoc:
|
|
|
|
attr_writer :ext, :sock # :nodoc:
|
|
|
|
attr_writer :commands # :nodoc:
|
2005-04-10 01:17:19 +00:00
|
|
|
end
|
|
|
|
|
2009-06-26 23:18:53 +00:00
|
|
|
end; end; end
|
2009-10-31 20:44:23 +00:00
|
|
|
|