Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
@@ -0,0 +1,59 @@
# BufferedTokenizer takes a delimiter upon instantiation, or acts line-based
# by default. It allows input to be spoon-fed from some outside source which
# receives arbitrary length datagrams which may-or-may-not contain the token
# by which entities are delimited. In this respect it's ideally paired with
# something like EventMachine (http://rubyeventmachine.com/).
class BufferedTokenizer
# New BufferedTokenizers will operate on lines delimited by a delimiter,
# which is by default the global input delimiter $/ ("\n").
#
# The input buffer is stored as an array. This is by far the most efficient
# approach given language constraints (in C a linked list would be a more
# appropriate data structure). Segments of input data are stored in a list
# which is only joined when a token is reached, substantially reducing the
# number of objects required for the operation.
def initialize(delimiter = $/)
@delimiter = delimiter
@input = []
@tail = ''
@trim = @delimiter.length - 1
end
# Extract takes an arbitrary string of input data and returns an array of
# tokenized entities, provided there were any available to extract. This
# makes for easy processing of datagrams using a pattern like:
#
# tokenizer.extract(data).map { |entity| Decode(entity) }.each do ...
#
# Using -1 makes split to return "" if the token is at the end of
# the string, meaning the last element is the start of the next chunk.
def extract(data)
if @trim > 0
tail_end = @tail.slice!(-@trim, @trim) # returns nil if string is too short
data = tail_end + data if tail_end
end
@input << @tail
entities = data.split(@delimiter, -1)
@tail = entities.shift
unless entities.empty?
@input << @tail
entities.unshift @input.join
@input.clear
@tail = entities.pop
end
entities
end
# Flush the contents of the input buffer, i.e. return the input buffer even though
# a token has not yet been encountered
def flush
@input << @tail
buffer = @input.join
@input.clear
@tail = "" # @tail.clear is slightly faster, but not supported on 1.8.7
buffer
end
end
@@ -0,0 +1,58 @@
module EventMachine
# Utility method for coercing arguments to an object that responds to :call.
# Accepts an object and a method name to send to, or a block, or an object
# that responds to :call.
#
# @example EventMachine.Callback used with a block. Returns that block.
#
# cb = EventMachine.Callback do |msg|
# puts(msg)
# end
# # returned object is a callable
# cb.call('hello world')
#
#
# @example EventMachine.Callback used with an object (to be more specific, class object) and a method name, returns an object that responds to #call
#
# cb = EventMachine.Callback(Object, :puts)
# # returned object is a callable that delegates to Kernel#puts (in this case Object.puts)
# cb.call('hello world')
#
#
# @example EventMachine.Callback used with an object that responds to #call. Returns the argument.
#
# cb = EventMachine.Callback(proc{ |msg| puts(msg) })
# # returned object is a callable
# cb.call('hello world')
#
#
# @overload Callback(object, method)
# Wraps `method` invocation on `object` into an object that responds to #call that proxies all the arguments to that method
# @param [Object] Object to invoke method on
# @param [Symbol] Method name
# @return [<#call>] An object that responds to #call that takes any number of arguments and invokes method on object with those arguments
#
# @overload Callback(object)
# Returns callable object as is, without any coercion
# @param [<#call>] An object that responds to #call
# @return [<#call>] Its argument
#
# @overload Callback(&block)
# Returns block passed to it without any coercion
# @return [<#call>] Block passed to this method
#
# @raise [ArgumentError] When argument doesn't respond to #call, method name is missing or when invoked without arguments and block isn't given
#
# @return [<#call>]
def self.Callback(object = nil, method = nil, &blk)
if object && method
lambda { |*args| object.__send__ method, *args }
else
if object.respond_to? :call
object
else
blk || raise(ArgumentError)
end # if
end # if
end # self.Callback
end # EventMachine
@@ -0,0 +1,69 @@
module EventMachine
# Provides a simple thread-safe way to transfer data between (typically) long running
# tasks in {EventMachine.defer} and event loop thread.
#
# @example
#
# channel = EventMachine::Channel.new
# sid = channel.subscribe { |msg| p [:got, msg] }
#
# channel.push('hello world')
# channel.unsubscribe(sid)
#
#
class Channel
def initialize
@subs = {}
@uid = 0
end
# Return the number of current subscribers.
def num_subscribers
return @subs.size
end
# Takes any arguments suitable for EM::Callback() and returns a subscriber
# id for use when unsubscribing.
#
# @return [Integer] Subscribe identifier
# @see #unsubscribe
def subscribe(*a, &b)
name = gen_id
EM.schedule { @subs[name] = EM::Callback(*a, &b) }
name
end
# Removes subscriber from the list.
#
# @param [Integer] Subscriber identifier
# @see #subscribe
def unsubscribe(name)
EM.schedule { @subs.delete name }
end
# Add items to the channel, which are pushed out to all subscribers.
def push(*items)
items = items.dup
EM.schedule { items.each { |i| @subs.values.each { |s| s.call i } } }
end
alias << push
# Fetches one message from the channel.
def pop(*a, &b)
EM.schedule {
name = subscribe do |*args|
unsubscribe(name)
EM::Callback(*a, &b).call(*args)
end
}
end
private
# @private
def gen_id
@uid += 1
end
end
end
@@ -0,0 +1,304 @@
# = EM::Completion
#
# A completion is a callback container for various states of completion. In
# its most basic form it has a start state and a finish state.
#
# This implementation includes some hold-back from the EM::Deferrable
# interface in order to be compatible - but it has a much cleaner
# implementation.
#
# In general it is preferred that this implementation be used as a state
# callback container than EM::DefaultDeferrable or other classes including
# EM::Deferrable. This is because it is generally more sane to keep this level
# of state in a dedicated state-back container. This generally leads to more
# malleable interfaces and software designs, as well as eradicating nasty bugs
# that result from abstraction leakage.
#
# == Basic Usage
#
# As already mentioned, the basic usage of a Completion is simply for its two
# final states, :succeeded and :failed.
#
# An asynchronous operation will complete at some future point in time, and
# users often want to react to this event. API authors will want to expose
# some common interface to react to these events.
#
# In the following example, the user wants to know when a short lived
# connection has completed its exchange with the remote server. The simple
# protocol just waits for an ack to its message.
#
# class Protocol < EM::Connection
# include EM::P::LineText2
#
# def initialize(message, completion)
# @message, @completion = message, completion
# @completion.completion { close_connection }
# @completion.timeout(1, :timeout)
# end
#
# def post_init
# send_data(@message)
# end
#
# def receive_line(line)
# case line
# when /ACK/i
# @completion.succeed line
# when /ERR/i
# @completion.fail :error, line
# else
# @completion.fail :unknown, line
# end
# end
#
# def unbind
# @completion.fail :disconnected unless @completion.completed?
# end
# end
#
# class API
# attr_reader :host, :port
#
# def initialize(host = 'example.org', port = 8000)
# @host, @port = host, port
# end
#
# def request(message)
# completion = EM::Deferrable::Completion.new
# EM.connect(host, port, Protocol, message, completion)
# completion
# end
# end
#
# api = API.new
# completion = api.request('stuff')
# completion.callback do |line|
# puts "API responded with: #{line}"
# end
# completion.errback do |type, line|
# case type
# when :error
# puts "API error: #{line}"
# when :unknown
# puts "API returned unknown response: #{line}"
# when :disconnected
# puts "API server disconnected prematurely"
# when :timeout
# puts "API server did not respond in a timely fashion"
# end
# end
#
# == Advanced Usage
#
# This completion implementation also supports more state callbacks and
# arbitrary states (unlike the original Deferrable API). This allows for basic
# stateful process encapsulation. One might use this to setup state callbacks
# for various states in an exchange like in the basic usage example, except
# where the applicaiton could be made to react to "connected" and
# "disconnected" states additionally.
#
# class Protocol < EM::Connection
# def initialize(completion)
# @response = []
# @completion = completion
# @completion.stateback(:disconnected) do
# @completion.succeed @response.join
# end
# end
#
# def connection_completed
# @host, @port = Socket.unpack_sockaddr_in get_peername
# @completion.change_state(:connected, @host, @port)
# send_data("GET http://example.org/ HTTP/1.0\r\n\r\n")
# end
#
# def receive_data(data)
# @response << data
# end
#
# def unbind
# @completion.change_state(:disconnected, @host, @port)
# end
# end
#
# completion = EM::Deferrable::Completion.new
# completion.stateback(:connected) do |host, port|
# puts "Connected to #{host}:#{port}"
# end
# completion.stateback(:disconnected) do |host, port|
# puts "Disconnected from #{host}:#{port}"
# end
# completion.callback do |response|
# puts response
# end
#
# EM.connect('example.org', 80, Protocol, completion)
#
# == Timeout
#
# The Completion also has a timeout. The timeout is global and is not aware of
# states apart from completion states. The timeout is only engaged if #timeout
# is called, and it will call fail if it is reached.
#
# == Completion states
#
# By default there are two completion states, :succeeded and :failed. These
# states can be modified by subclassing and overrding the #completion_states
# method. Completion states are special, in that callbacks for all completion
# states are explcitly cleared when a completion state is entered. This
# prevents errors that could arise from accidental unterminated timeouts, and
# other such user errors.
#
# == Other notes
#
# Several APIs have been carried over from EM::Deferrable for compatibility
# reasons during a transitionary period. Specifically cancel_errback and
# cancel_callback are implemented, but their usage is to be strongly
# discouraged. Due to the already complex nature of reaction systems, dynamic
# callback deletion only makes the problem much worse. It is always better to
# add correct conditionals to the callback code, or use more states, than to
# address such implementaiton issues with conditional callbacks.
module EventMachine
class Completion
# This is totally not used (re-implemented), it's here in case people check
# for kind_of?
include EventMachine::Deferrable
attr_reader :state, :value
def initialize
@state = :unknown
@callbacks = Hash.new { |h,k| h[k] = [] }
@value = []
@timeout_timer = nil
end
# Enter the :succeeded state, setting the result value if given.
def succeed(*args)
change_state(:succeeded, *args)
end
# The old EM method:
alias set_deferred_success succeed
# Enter the :failed state, setting the result value if given.
def fail(*args)
change_state(:failed, *args)
end
# The old EM method:
alias set_deferred_failure fail
# Statebacks are called when you enter (or are in) the named state.
def stateback(state, *a, &b)
# The following is quite unfortunate special casing for :completed
# statebacks, but it's a necessary evil for latent completion
# definitions.
if :completed == state || !completed? || @state == state
@callbacks[state] << EM::Callback(*a, &b)
end
execute_callbacks
self
end
# Callbacks are called when you enter (or are in) a :succeeded state.
def callback(*a, &b)
stateback(:succeeded, *a, &b)
end
# Errbacks are called when you enter (or are in) a :failed state.
def errback(*a, &b)
stateback(:failed, *a, &b)
end
# Completions are called when you enter (or are in) either a :failed or a
# :succeeded state. They are stored as a special (reserved) state called
# :completed.
def completion(*a, &b)
stateback(:completed, *a, &b)
end
# Enter a new state, setting the result value if given. If the state is one
# of :succeeded or :failed, then :completed callbacks will also be called.
def change_state(state, *args)
@value = args
@state = state
EM.schedule { execute_callbacks }
end
# The old EM method:
alias set_deferred_status change_state
# Indicates that we've reached some kind of completion state, by default
# this is :succeeded or :failed. Due to these semantics, the :completed
# state is reserved for internal use.
def completed?
completion_states.any? { |s| state == s }
end
# Completion states simply returns a list of completion states, by default
# this is :succeeded and :failed.
def completion_states
[:succeeded, :failed]
end
# Schedule a time which if passes before we enter a completion state, this
# deferrable will be failed with the given arguments.
def timeout(time, *args)
cancel_timeout
@timeout_timer = EM::Timer.new(time) do
fail(*args) unless completed?
end
end
# Disable the timeout
def cancel_timeout
if @timeout_timer
@timeout_timer.cancel
@timeout_timer = nil
end
end
# Remove an errback. N.B. Some errbacks cannot be deleted. Usage is NOT
# recommended, this is an anti-pattern.
def cancel_errback(*a, &b)
@callbacks[:failed].delete(EM::Callback(*a, &b))
end
# Remove a callback. N.B. Some callbacks cannot be deleted. Usage is NOT
# recommended, this is an anti-pattern.
def cancel_callback(*a, &b)
@callbacks[:succeeded].delete(EM::Callback(*a, &b))
end
private
# Execute all callbacks for the current state. If in a completed state, then
# call any statebacks associated with the completed state.
def execute_callbacks
execute_state_callbacks(state)
if completed?
execute_state_callbacks(:completed)
clear_dead_callbacks
cancel_timeout
end
end
# Iterate all callbacks for a given state, and remove then call them.
def execute_state_callbacks(state)
while callback = @callbacks[state].shift
callback.call(*value)
end
end
# If we enter a completion state, clear other completion states after all
# callback chains are completed. This means that operation specific
# callbacks can't be dual-called, which is most common user error.
def clear_dead_callbacks
completion_states.each do |state|
@callbacks[state].clear
end
end
end
end
@@ -0,0 +1,770 @@
module EventMachine
class FileNotFoundException < Exception
end
# EventMachine::Connection is a class that is instantiated
# by EventMachine's processing loop whenever a new connection
# is created. (New connections can be either initiated locally
# to a remote server or accepted locally from a remote client.)
# When a Connection object is instantiated, it <i>mixes in</i>
# the functionality contained in the user-defined module
# specified in calls to {EventMachine.connect} or {EventMachine.start_server}.
# User-defined handler modules may redefine any or all of the standard
# methods defined here, as well as add arbitrary additional code
# that will also be mixed in.
#
# EventMachine manages one object inherited from EventMachine::Connection
# (and containing the mixed-in user code) for every network connection
# that is active at any given time.
# The event loop will automatically call methods on EventMachine::Connection
# objects whenever specific events occur on the corresponding connections,
# as described below.
#
# This class is never instantiated by user code, and does not publish an
# initialize method. The instance methods of EventMachine::Connection
# which may be called by the event loop are:
#
# * {#post_init}
# * {#connection_completed}
# * {#receive_data}
# * {#unbind}
# * {#ssl_verify_peer} (if TLS is used)
# * {#ssl_handshake_completed}
#
# All of the other instance methods defined here are called only by user code.
#
# @see file:docs/GettingStarted.md EventMachine tutorial
class Connection
# @private
attr_accessor :signature
# @private
alias original_method method
# Override .new so subclasses don't have to call super and can ignore
# connection-specific arguments
#
# @private
def self.new(sig, *args)
allocate.instance_eval do
# Store signature
@signature = sig
# associate_callback_target sig
# Call a superclass's #initialize if it has one
initialize(*args)
# post initialize callback
post_init
self
end
end
# Stubbed initialize so legacy superclasses can safely call super
#
# @private
def initialize(*args)
end
# Called by the event loop immediately after the network connection has been established,
# and before resumption of the network loop.
# This method is generally not called by user code, but is called automatically
# by the event loop. The base-class implementation is a no-op.
# This is a very good place to initialize instance variables that will
# be used throughout the lifetime of the network connection.
#
# @see #connection_completed
# @see #unbind
# @see #send_data
# @see #receive_data
def post_init
end
# Called by the event loop whenever data has been received by the network connection.
# It is never called by user code. {#receive_data} is called with a single parameter, a String containing
# the network protocol data, which may of course be binary. You will
# generally redefine this method to perform your own processing of the incoming data.
#
# Here's a key point which is essential to understanding the event-driven
# programming model: <i>EventMachine knows absolutely nothing about the protocol
# which your code implements.</i> You must not make any assumptions about
# the size of the incoming data packets, or about their alignment on any
# particular intra-message or PDU boundaries (such as line breaks).
# receive_data can and will send you arbitrary chunks of data, with the
# only guarantee being that the data is presented to your code in the order
# it was collected from the network. Don't even assume that the chunks of
# data will correspond to network packets, as EventMachine can and will coalesce
# several incoming packets into one, to improve performance. The implication for your
# code is that you generally will need to implement some kind of a state machine
# in your redefined implementation of receive_data. For a better understanding
# of this, read through the examples of specific protocol handlers in EventMachine::Protocols
#
# The base-class implementation (which will be invoked only if you didn't override it in your protocol handler)
# simply prints incoming data packet size to stdout.
#
# @param [String] data Opaque incoming data.
# @note Depending on the protocol, buffer sizes and OS networking stack configuration, incoming data may or may not be "a complete message".
# It is up to this handler to detect content boundaries to determine whether all the content (for example, full HTTP request)
# has been received and can be processed.
#
# @see #post_init
# @see #connection_completed
# @see #unbind
# @see #send_data
# @see file:docs/GettingStarted.md EventMachine tutorial
def receive_data data
puts "............>>>#{data.length}"
end
# Called by EventMachine when the SSL/TLS handshake has
# been completed, as a result of calling #start_tls to initiate SSL/TLS on the connection.
#
# This callback exists because {#post_init} and {#connection_completed} are **not** reliable
# for indicating when an SSL/TLS connection is ready to have its certificate queried for.
#
# @see #get_peer_cert
def ssl_handshake_completed
end
# Called by EventMachine when :verify_peer => true has been passed to {#start_tls}.
# It will be called with each certificate in the certificate chain provided by the remote peer.
#
# The cert will be passed as a String in PEM format, the same as in {#get_peer_cert}. It is up to user defined
# code to perform a check on the certificates. The return value from this callback is used to accept or deny the peer.
# A return value that is not nil or false triggers acceptance. If the peer is not accepted, the connection
# will be subsequently closed.
#
# @example This server always accepts all peers
#
# module AcceptServer
# def post_init
# start_tls(:verify_peer => true)
# end
#
# def ssl_verify_peer(cert)
# true
# end
#
# def ssl_handshake_completed
# $server_handshake_completed = true
# end
# end
#
#
# @example This server never accepts any peers
#
# module DenyServer
# def post_init
# start_tls(:verify_peer => true)
# end
#
# def ssl_verify_peer(cert)
# # Do not accept the peer. This should now cause the connection to shut down
# # without the SSL handshake being completed.
# false
# end
#
# def ssl_handshake_completed
# $server_handshake_completed = true
# end
# end
#
# @see #start_tls
def ssl_verify_peer(cert)
end
# called by the framework whenever a connection (either a server or client connection) is closed.
# The close can occur because your code intentionally closes it (using {#close_connection} and {#close_connection_after_writing}),
# because the remote peer closed the connection, or because of a network error.
# You may not assume that the network connection is still open and able to send or
# receive data when the callback to unbind is made. This is intended only to give
# you a chance to clean up associations your code may have made to the connection
# object while it was open.
#
# If you want to detect which peer has closed the connection, you can override {#close_connection} in your protocol handler
# and set an @ivar.
#
# @example Overriding Connection#close_connection to distinguish connections closed on our side
#
# class MyProtocolHandler < EventMachine::Connection
#
# # ...
#
# def close_connection(*args)
# @intentionally_closed_connection = true
# super(*args)
# end
#
# def unbind
# if @intentionally_closed_connection
# # ...
# end
# end
#
# # ...
#
# end
#
# @see #post_init
# @see #connection_completed
# @see file:docs/GettingStarted.md EventMachine tutorial
def unbind
end
# Called by the reactor after attempting to relay incoming data to a descriptor (set as a proxy target descriptor with
# {EventMachine.enable_proxy}) that has already been closed.
#
# @see EventMachine.enable_proxy
def proxy_target_unbound
end
# called when the reactor finished proxying all
# of the requested bytes.
def proxy_completed
end
# EventMachine::Connection#proxy_incoming_to is called only by user code. It sets up
# a low-level proxy relay for all data inbound for this connection, to the connection given
# as the argument. This is essentially just a helper method for enable_proxy.
#
# @see EventMachine.enable_proxy
def proxy_incoming_to(conn,bufsize=0)
EventMachine::enable_proxy(self, conn, bufsize)
end
# A helper method for {EventMachine.disable_proxy}
def stop_proxying
EventMachine::disable_proxy(self)
end
# The number of bytes proxied to another connection. Reset to zero when
# EventMachine::Connection#proxy_incoming_to is called, and incremented whenever data is proxied.
def get_proxied_bytes
EventMachine::get_proxied_bytes(@signature)
end
# EventMachine::Connection#close_connection is called only by user code, and never
# by the event loop. You may call this method against a connection object in any
# callback handler, whether or not the callback was made against the connection
# you want to close. close_connection <i>schedules</i> the connection to be closed
# at the next available opportunity within the event loop. You may not assume that
# the connection is closed when close_connection returns. In particular, the framework
# will callback the unbind method for the particular connection at a point shortly
# after you call close_connection. You may assume that the unbind callback will
# take place sometime after your call to close_connection completes. In other words,
# the unbind callback will not re-enter your code "inside" of your call to close_connection.
# However, it's not guaranteed that a future version of EventMachine will not change
# this behavior.
#
# {#close_connection} will *silently discard* any outbound data which you have
# sent to the connection using {EventMachine::Connection#send_data} but which has not
# yet been sent across the network. If you want to avoid this behavior, use
# {EventMachine::Connection#close_connection_after_writing}.
#
def close_connection after_writing = false
EventMachine::close_connection @signature, after_writing
end
# Removes given connection from the event loop.
# The connection's socket remains open and its file descriptor number is returned.
def detach
EventMachine::detach_fd @signature
end
def get_sock_opt level, option
EventMachine::get_sock_opt @signature, level, option
end
def set_sock_opt level, optname, optval
EventMachine::set_sock_opt @signature, level, optname, optval
end
# A variant of {#close_connection}.
# All of the descriptive comments given for close_connection also apply to
# close_connection_after_writing, *with one exception*: if the connection has
# outbound data sent using send_dat but which has not yet been sent across the network,
# close_connection_after_writing will schedule the connection to be closed *after*
# all of the outbound data has been safely written to the remote peer.
#
# Depending on the amount of outgoing data and the speed of the network,
# considerable time may elapse between your call to close_connection_after_writing
# and the actual closing of the socket (at which time the unbind callback will be called
# by the event loop). During this time, you *may not* call send_data to transmit
# additional data (that is, the connection is closed for further writes). In very
# rare cases, you may experience a receive_data callback after your call to {#close_connection_after_writing},
# depending on whether incoming data was in the process of being received on the connection
# at the moment when you called {#close_connection_after_writing}. Your protocol handler must
# be prepared to properly deal with such data (probably by ignoring it).
#
# @see #close_connection
# @see #send_data
def close_connection_after_writing
close_connection true
end
# Call this method to send data to the remote end of the network connection. It takes a single String argument,
# which may contain binary data. Data is buffered to be sent at the end of this event loop tick (cycle).
#
# When used in a method that is event handler (for example, {#post_init} or {#connection_completed}, it will send
# data to the other end of the connection that generated the event.
# You can also call {#send_data} to write to other connections. For more information see The Chat Server Example in the
# {file:docs/GettingStarted.md EventMachine tutorial}.
#
# If you want to send some data and then immediately close the connection, make sure to use {#close_connection_after_writing}
# instead of {#close_connection}.
#
#
# @param [String] data Data to send asynchronously
#
# @see file:docs/GettingStarted.md EventMachine tutorial
# @see Connection#receive_data
# @see Connection#post_init
# @see Connection#unbind
def send_data data
data = data.to_s
size = data.bytesize if data.respond_to?(:bytesize)
size ||= data.size
EventMachine::send_data @signature, data, size
end
# Returns true if the connection is in an error state, false otherwise.
#
# In general, you can detect the occurrence of communication errors or unexpected
# disconnection by the remote peer by handing the {#unbind} method. In some cases, however,
# it's useful to check the status of the connection using {#error?} before attempting to send data.
# This function is synchronous but it will return immediately without blocking.
#
# @return [Boolean] true if the connection is in an error state, false otherwise
def error?
errno = EventMachine::report_connection_error_status(@signature)
case errno
when 0
false
when -1
true
else
EventMachine::ERRNOS[errno]
end
end
# Called by the event loop when a remote TCP connection attempt completes successfully.
# You can expect to get this notification after calls to {EventMachine.connect}. Remember that EventMachine makes remote connections
# asynchronously, just as with any other kind of network event. This method
# is intended primarily to assist with network diagnostics. For normal protocol
# handling, use #post_init to perform initial work on a new connection (such as sending initial set of data).
# {Connection#post_init} will always be called. This method will only be called in case of a successful completion.
# A connection attempt which fails will result a call to {Connection#unbind} after the failure.
#
# @see Connection#post_init
# @see Connection#unbind
# @see file:docs/GettingStarted.md EventMachine tutorial
def connection_completed
end
# Call {#start_tls} at any point to initiate TLS encryption on connected streams.
# The method is smart enough to know whether it should perform a server-side
# or a client-side handshake. An appropriate place to call {#start_tls} is in
# your redefined {#post_init} method, or in the {#connection_completed} handler for
# an outbound connection.
#
#
# @option args [String] :cert_chain_file (nil) local path of a readable file that contants a chain of X509 certificates in
# the [PEM format](http://en.wikipedia.org/wiki/Privacy_Enhanced_Mail),
# with the most-resolved certificate at the top of the file, successive intermediate
# certs in the middle, and the root (or CA) cert at the bottom.
#
# @option args [String] :private_key_file (nil) local path of a readable file that must contain a private key in the [PEM format](http://en.wikipedia.org/wiki/Privacy_Enhanced_Mail).
#
# @option args [Boolean] :verify_peer (false) indicates whether a server should request a certificate from a peer, to be verified by user code.
# If true, the {#ssl_verify_peer} callback on the {EventMachine::Connection} object is called with each certificate
# in the certificate chain provided by the peer. See documentation on {#ssl_verify_peer} for how to use this.
#
# @option args [Boolean] :fail_if_no_peer_cert (false) Used in conjunction with verify_peer. If set the SSL handshake will be terminated if the peer does not provide a certificate.
#
#
# @option args [String] :cipher_list ("ALL:!ADH:!LOW:!EXP:!DES-CBC3-SHA:@STRENGTH") indicates the available SSL cipher values. Default value is "ALL:!ADH:!LOW:!EXP:!DES-CBC3-SHA:@STRENGTH". Check the format of the OpenSSL cipher string at http://www.openssl.org/docs/apps/ciphers.html#CIPHER_LIST_FORMAT.
#
# @option args [String] :ecdh_curve (nil) The curve for ECDHE ciphers. See available ciphers with 'openssl ecparam -list_curves'
#
# @option args [String] :dhparam (nil) The local path of a file containing DH parameters for EDH ciphers in [PEM format](http://en.wikipedia.org/wiki/Privacy_Enhanced_Mail) See: 'openssl dhparam'
#
# @option args [Array] :ssl_version (TLSv1 TLSv1_1 TLSv1_2) indicates the allowed SSL/TLS versions. Possible values are: {SSLv2}, {SSLv3}, {TLSv1}, {TLSv1_1}, {TLSv1_2}.
#
# @example Using TLS with EventMachine
#
# require 'rubygems'
# require 'eventmachine'
#
# module Handler
# def post_init
# start_tls(:private_key_file => '/tmp/server.key', :cert_chain_file => '/tmp/server.crt', :verify_peer => false)
# end
# end
#
# EventMachine.run do
# EventMachine.start_server("127.0.0.1", 9999, Handler)
# end
#
# @param [Hash] args
#
# @todo support passing an encryption parameter, which can be string or Proc, to get a passphrase
# for encrypted private keys.
# @todo support passing key material via raw strings or Procs that return strings instead of
# just filenames.
#
# @see #ssl_verify_peer
def start_tls args={}
priv_key = args[:private_key_file]
cert_chain = args[:cert_chain_file]
verify_peer = args[:verify_peer]
sni_hostname = args[:sni_hostname]
cipher_list = args[:cipher_list]
ssl_version = args[:ssl_version]
ecdh_curve = args[:ecdh_curve]
dhparam = args[:dhparam]
fail_if_no_peer_cert = args[:fail_if_no_peer_cert]
[priv_key, cert_chain].each do |file|
next if file.nil? or file.empty?
raise FileNotFoundException,
"Could not find #{file} for start_tls" unless File.exist? file
end
protocols_bitmask = 0
if ssl_version.nil?
protocols_bitmask |= EventMachine::EM_PROTO_TLSv1
protocols_bitmask |= EventMachine::EM_PROTO_TLSv1_1
protocols_bitmask |= EventMachine::EM_PROTO_TLSv1_2
else
[ssl_version].flatten.each do |p|
case p.to_s.downcase
when 'sslv2'
protocols_bitmask |= EventMachine::EM_PROTO_SSLv2
when 'sslv3'
protocols_bitmask |= EventMachine::EM_PROTO_SSLv3
when 'tlsv1'
protocols_bitmask |= EventMachine::EM_PROTO_TLSv1
when 'tlsv1_1'
protocols_bitmask |= EventMachine::EM_PROTO_TLSv1_1
when 'tlsv1_2'
protocols_bitmask |= EventMachine::EM_PROTO_TLSv1_2
else
raise("Unrecognized SSL/TLS Protocol: #{p}")
end
end
end
EventMachine::set_tls_parms(@signature, priv_key || '', cert_chain || '', verify_peer, fail_if_no_peer_cert, sni_hostname || '', cipher_list || '', ecdh_curve || '', dhparam || '', protocols_bitmask)
EventMachine::start_tls @signature
end
# If [TLS](http://en.wikipedia.org/wiki/Transport_Layer_Security) is active on the connection, returns the remote [X509 certificate](http://en.wikipedia.org/wiki/X.509)
# as a string, in the popular [PEM format](http://en.wikipedia.org/wiki/Privacy_Enhanced_Mail). This can then be used for arbitrary validation
# of a peer's certificate in your code.
#
# This should be called in/after the {#ssl_handshake_completed} callback, which indicates
# that SSL/TLS is active. Using this callback is important, because the certificate may not
# be available until the time it is executed. Using #post_init or #connection_completed is
# not adequate, because the SSL handshake may still be taking place.
#
# This method will return `nil` if:
#
# * EventMachine is not built with [OpenSSL](http://www.openssl.org) support
# * [TLS](http://en.wikipedia.org/wiki/Transport_Layer_Security) is not active on the connection
# * TLS handshake is not yet complete
# * Remote peer for any other reason has not presented a certificate
#
#
# @example Getting peer TLS certificate information in EventMachine
#
# module Handler
# def post_init
# puts "Starting TLS"
# start_tls
# end
#
# def ssl_handshake_completed
# puts get_peer_cert
# close_connection
# end
#
# def unbind
# EventMachine::stop_event_loop
# end
# end
#
# EventMachine.run do
# EventMachine.connect "mail.google.com", 443, Handler
# end
#
# # Will output:
# # -----BEGIN CERTIFICATE-----
# # MIIDIjCCAougAwIBAgIQbldpChBPqv+BdPg4iwgN8TANBgkqhkiG9w0BAQUFADBM
# # MQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkg
# # THRkLjEWMBQGA1UEAxMNVGhhd3RlIFNHQyBDQTAeFw0wODA1MDIxNjMyNTRaFw0w
# # OTA1MDIxNjMyNTRaMGkxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh
# # MRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKEwpHb29nbGUgSW5jMRgw
# # FgYDVQQDEw9tYWlsLmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ
# # AoGBALlkxdh2QXegdElukCSOV2+8PKiONIS+8Tu9K7MQsYpqtLNC860zwOPQ2NLI
# # 3Zp4jwuXVTrtzGuiqf5Jioh35Ig3CqDXtLyZoypjZUQcq4mlLzHlhIQ4EhSjDmA7
# # Ffw9y3ckSOQgdBQWNLbquHh9AbEUjmhkrYxIqKXeCnRKhv6nAgMBAAGjgecwgeQw
# # KAYDVR0lBCEwHwYIKwYBBQUHAwEGCCsGAQUFBwMCBglghkgBhvhCBAEwNgYDVR0f
# # BC8wLTAroCmgJ4YlaHR0cDovL2NybC50aGF3dGUuY29tL1RoYXd0ZVNHQ0NBLmNy
# # bDByBggrBgEFBQcBAQRmMGQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9vY3NwLnRoYXd0
# # ZS5jb20wPgYIKwYBBQUHMAKGMmh0dHA6Ly93d3cudGhhd3RlLmNvbS9yZXBvc2l0
# # b3J5L1RoYXd0ZV9TR0NfQ0EuY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQEF
# # BQADgYEAsRwpLg1dgCR1gYDK185MFGukXMeQFUvhGqF8eT/CjpdvezyKVuz84gSu
# # 6ccMXgcPQZGQN/F4Xug+Q01eccJjRSVfdvR5qwpqCj+6BFl5oiKDBsveSkrmL5dz
# # s2bn7TdTSYKcLeBkjXxDLHGBqLJ6TNCJ3c4/cbbG5JhGvoema94=
# # -----END CERTIFICATE-----
#
# You can do whatever you want with the certificate String, such as load it
# as a certificate object using the OpenSSL library, and check its fields.
#
# @return [String] the remote [X509 certificate](http://en.wikipedia.org/wiki/X.509), in the popular [PEM format](http://en.wikipedia.org/wiki/Privacy_Enhanced_Mail),
# if TLS is active on the connection
#
# @see Connection#start_tls
# @see Connection#ssl_handshake_completed
def get_peer_cert
EventMachine::get_peer_cert @signature
end
def get_cipher_bits
EventMachine::get_cipher_bits @signature
end
def get_cipher_name
EventMachine::get_cipher_name @signature
end
def get_cipher_protocol
EventMachine::get_cipher_protocol @signature
end
def get_sni_hostname
EventMachine::get_sni_hostname @signature
end
# Sends UDP messages.
#
# This method may be called from any Connection object that refers
# to an open datagram socket (see EventMachine#open_datagram_socket).
# The method sends a UDP (datagram) packet containing the data you specify,
# to a remote peer specified by the IP address and port that you give
# as parameters to the method.
# Observe that you may send a zero-length packet (empty string).
# However, you may not send an arbitrarily-large data packet because
# your operating system will enforce a platform-specific limit on
# the size of the outbound packet. (Your kernel
# will respond in a platform-specific way if you send an overlarge
# packet: some will send a truncated packet, some will complain, and
# some will silently drop your request).
# On LANs, it's usually OK to send datagrams up to about 4000 bytes in length,
# but to be really safe, send messages smaller than the Ethernet-packet
# size (typically about 1400 bytes). Some very restrictive WANs
# will either drop or truncate packets larger than about 500 bytes.
#
# @param [String] data Data to send asynchronously
# @param [String] recipient_address IP address of the recipient
# @param [String] recipient_port Port of the recipient
def send_datagram data, recipient_address, recipient_port
data = data.to_s
size = data.bytesize if data.respond_to?(:bytesize)
size ||= data.size
EventMachine::send_datagram @signature, data, size, recipient_address, Integer(recipient_port)
end
# This method is used with stream-connections to obtain the identity
# of the remotely-connected peer. If a peername is available, this method
# returns a sockaddr structure. The method returns nil if no peername is available.
# You can use Socket.unpack_sockaddr_in and its variants to obtain the
# values contained in the peername structure returned from #get_peername.
#
# @example How to get peer IP address and port with EventMachine
#
# require 'socket'
#
# module Handler
# def receive_data data
# port, ip = Socket.unpack_sockaddr_in(get_peername)
# puts "got #{data.inspect} from #{ip}:#{port}"
# end
# end
def get_peername
EventMachine::get_peername @signature
end
# Used with stream-connections to obtain the identity
# of the local side of the connection. If a local name is available, this method
# returns a sockaddr structure. The method returns nil if no local name is available.
# You can use {Socket.unpack_sockaddr_in} and its variants to obtain the
# values contained in the local-name structure returned from this method.
#
# @example
#
# require 'socket'
#
# module Handler
# def receive_data data
# port, ip = Socket.unpack_sockaddr_in(get_sockname)
# puts "got #{data.inspect}"
# end
# end
def get_sockname
EventMachine::get_sockname @signature
end
# Returns the PID (kernel process identifier) of a subprocess
# associated with this Connection object. For use with {EventMachine.popen}
# and similar methods. Returns nil when there is no meaningful subprocess.
#
# @return [Integer]
def get_pid
EventMachine::get_subprocess_pid @signature
end
# Returns a subprocess exit status. Only useful for {EventMachine.popen}. Call it in your
# {#unbind} handler.
#
# @return [Integer]
def get_status
EventMachine::get_subprocess_status @signature
end
# The number of seconds since the last send/receive activity on this connection.
def get_idle_time
EventMachine::get_idle_time @signature
end
# comm_inactivity_timeout returns the current value (float in seconds) of the inactivity-timeout
# property of network-connection and datagram-socket objects. A nonzero value
# indicates that the connection or socket will automatically be closed if no read or write
# activity takes place for at least that number of seconds.
# A zero value (the default) specifies that no automatic timeout will take place.
def comm_inactivity_timeout
EventMachine::get_comm_inactivity_timeout @signature
end
# Allows you to set the inactivity-timeout property for
# a network connection or datagram socket. Specify a non-negative float value in seconds.
# If the value is greater than zero, the connection or socket will automatically be closed
# if no read or write activity takes place for at least that number of seconds.
# Specify a value of zero to indicate that no automatic timeout should take place.
# Zero is the default value.
def comm_inactivity_timeout= value
EventMachine::set_comm_inactivity_timeout @signature, value.to_f
end
alias set_comm_inactivity_timeout comm_inactivity_timeout=
# The duration after which a TCP connection in the connecting state will fail.
# It is important to distinguish this value from {EventMachine::Connection#comm_inactivity_timeout},
# which looks at how long since data was passed on an already established connection.
# The value is a float in seconds.
#
# @return [Float] The duration after which a TCP connection in the connecting state will fail, in seconds.
def pending_connect_timeout
EventMachine::get_pending_connect_timeout @signature
end
# Sets the duration after which a TCP connection in a
# connecting state will fail.
#
# @param [Float, #to_f] value Connection timeout in seconds
def pending_connect_timeout= value
EventMachine::set_pending_connect_timeout @signature, value.to_f
end
alias set_pending_connect_timeout pending_connect_timeout=
# Reconnect to a given host/port with the current instance
#
# @param [String] server Hostname or IP address
# @param [Integer] port Port to reconnect to
def reconnect server, port
EventMachine::reconnect server, port, self
end
# Like {EventMachine::Connection#send_data}, this sends data to the remote end of
# the network connection. {EventMachine::Connection#send_file_data} takes a
# filename as an argument, though, and sends the contents of the file, in one
# chunk.
#
# @param [String] filename Local path of the file to send
#
# @see #send_data
# @author Kirk Haines
def send_file_data filename
EventMachine::send_file_data @signature, filename
end
# Open a file on the filesystem and send it to the remote peer. This returns an
# object of type {EventMachine::Deferrable}. The object's callbacks will be executed
# on the reactor main thread when the file has been completely scheduled for
# transmission to the remote peer. Its errbacks will be called in case of an error (such as file-not-found).
# This method employs various strategies to achieve the fastest possible performance,
# balanced against minimum consumption of memory.
#
# Warning: this feature has an implicit dependency on an outboard extension,
# evma_fastfilereader. You must install this extension in order to use {#stream_file_data}
# with files larger than a certain size (currently 8192 bytes).
#
# @option args [Boolean] :http_chunks (false) If true, this method will stream the file data in a format
# compatible with the HTTP chunked-transfer encoding
#
# @param [String] filename Local path of the file to stream
# @param [Hash] args Options
#
# @return [EventMachine::Deferrable]
def stream_file_data filename, args={}
EventMachine::FileStreamer.new( self, filename, args )
end
# Watches connection for readability. Only possible if the connection was created
# using {EventMachine.attach} and had {EventMachine.notify_readable}/{EventMachine.notify_writable} defined on the handler.
#
# @see #notify_readable?
def notify_readable= mode
EventMachine::set_notify_readable @signature, mode
end
# @return [Boolean] true if the connection is being watched for readability.
def notify_readable?
EventMachine::is_notify_readable @signature
end
# Watches connection for writeability. Only possible if the connection was created
# using {EventMachine.attach} and had {EventMachine.notify_readable}/{EventMachine.notify_writable} defined on the handler.
#
# @see #notify_writable?
def notify_writable= mode
EventMachine::set_notify_writable @signature, mode
end
# Returns true if the connection is being watched for writability.
def notify_writable?
EventMachine::is_notify_writable @signature
end
# Pause a connection so that {#send_data} and {#receive_data} events are not fired until {#resume} is called.
# @see #resume
def pause
EventMachine::pause_connection @signature
end
# Resume a connection's {#send_data} and {#receive_data} events.
# @see #pause
def resume
EventMachine::resume_connection @signature
end
# @return [Boolean] true if the connect was paused using {EventMachine::Connection#pause}.
# @see #pause
# @see #resume
def paused?
EventMachine::connection_paused? @signature
end
end
end
@@ -0,0 +1,210 @@
#--
#
# Author:: Francis Cianfrocca (gmail: blackhedd)
# Homepage:: http://rubyeventmachine.com
# Date:: 16 Jul 2006
#
# See EventMachine and EventMachine::Connection for documentation and
# usage examples.
#
#----------------------------------------------------------------------------
#
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
# Gmail: blackhedd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of either: 1) 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; or 2) Ruby's License.
#
# See the file COPYING for complete licensing information.
#
#---------------------------------------------------------------------------
#
#
module EventMachine
module Deferrable
autoload :Pool, 'em/deferrable/pool'
# Specify a block to be executed if and when the Deferrable object receives
# a status of :succeeded. See #set_deferred_status for more information.
#
# Calling this method on a Deferrable object whose status is not yet known
# will cause the callback block to be stored on an internal list.
# If you call this method on a Deferrable whose status is :succeeded, the
# block will be executed immediately, receiving the parameters given to the
# prior #set_deferred_status call.
#
#--
# If there is no status, add a callback to an internal list.
# If status is succeeded, execute the callback immediately.
# If status is failed, do nothing.
#
def callback &block
return unless block
@deferred_status ||= :unknown
if @deferred_status == :succeeded
block.call(*@deferred_args)
elsif @deferred_status != :failed
@callbacks ||= []
@callbacks.unshift block # << block
end
self
end
# Cancels an outstanding callback to &block if any. Undoes the action of #callback.
#
def cancel_callback block
@callbacks ||= []
@callbacks.delete block
end
# Specify a block to be executed if and when the Deferrable object receives
# a status of :failed. See #set_deferred_status for more information.
#--
# If there is no status, add an errback to an internal list.
# If status is failed, execute the errback immediately.
# If status is succeeded, do nothing.
#
def errback &block
return unless block
@deferred_status ||= :unknown
if @deferred_status == :failed
block.call(*@deferred_args)
elsif @deferred_status != :succeeded
@errbacks ||= []
@errbacks.unshift block # << block
end
self
end
# Cancels an outstanding errback to &block if any. Undoes the action of #errback.
#
def cancel_errback block
@errbacks ||= []
@errbacks.delete block
end
# Sets the "disposition" (status) of the Deferrable object. See also the large set of
# sugarings for this method.
# Note that if you call this method without arguments,
# no arguments will be passed to the callback/errback.
# If the user has coded these with arguments, then the
# user code will throw an argument exception.
# Implementors of deferrable classes <b>must</b>
# document the arguments they will supply to user callbacks.
#
# OBSERVE SOMETHING VERY SPECIAL here: you may call this method even
# on the INSIDE of a callback. This is very useful when a previously-registered
# callback wants to change the parameters that will be passed to subsequently-registered
# ones.
#
# You may give either :succeeded or :failed as the status argument.
#
# If you pass :succeeded, then all of the blocks passed to the object using the #callback
# method (if any) will be executed BEFORE the #set_deferred_status method returns. All of the blocks
# passed to the object using #errback will be discarded.
#
# If you pass :failed, then all of the blocks passed to the object using the #errback
# method (if any) will be executed BEFORE the #set_deferred_status method returns. All of the blocks
# passed to the object using # callback will be discarded.
#
# If you pass any arguments to #set_deferred_status in addition to the status argument,
# they will be passed as arguments to any callbacks or errbacks that are executed.
# It's your responsibility to ensure that the argument lists specified in your callbacks and
# errbacks match the arguments given in calls to #set_deferred_status, otherwise Ruby will raise
# an ArgumentError.
#
#--
# We're shifting callbacks off and discarding them as we execute them.
# This is valid because by definition callbacks are executed no more than
# once. It also has the magic effect of permitting recursive calls, which
# means that a callback can call #set_deferred_status and change the parameters
# that will be sent to subsequent callbacks down the chain.
#
# Changed @callbacks and @errbacks from push/shift to unshift/pop, per suggestion
# by Kirk Haines, to work around the memory leak bug that still exists in many Ruby
# versions.
#
# Changed 15Sep07: after processing callbacks or errbacks, CLEAR the other set of
# handlers. This gets us a little closer to the behavior of Twisted's "deferred,"
# which only allows status to be set once. Prior to making this change, it was possible
# to "succeed" a Deferrable (triggering its callbacks), and then immediately "fail" it,
# triggering its errbacks! That is clearly undesirable, but it's just as undesirable
# to raise an exception is status is set more than once on a Deferrable. The latter
# behavior would invalidate the idiom of resetting arguments by setting status from
# within a callback or errback, but more seriously it would cause spurious errors
# if a Deferrable was timed out and then an attempt was made to succeed it. See the
# comments under the new method #timeout.
#
def set_deferred_status status, *args
cancel_timeout
@errbacks ||= nil
@callbacks ||= nil
@deferred_status = status
@deferred_args = args
case @deferred_status
when :succeeded
if @callbacks
while cb = @callbacks.pop
cb.call(*@deferred_args)
end
end
@errbacks.clear if @errbacks
when :failed
if @errbacks
while eb = @errbacks.pop
eb.call(*@deferred_args)
end
end
@callbacks.clear if @callbacks
end
end
# Setting a timeout on a Deferrable causes it to go into the failed state after
# the Timeout expires (passing no arguments to the object's errbacks).
# Setting the status at any time prior to a call to the expiration of the timeout
# will cause the timer to be cancelled.
def timeout seconds, *args
cancel_timeout
me = self
@deferred_timeout = EventMachine::Timer.new(seconds) {me.fail(*args)}
self
end
# Cancels an outstanding timeout if any. Undoes the action of #timeout.
#
def cancel_timeout
@deferred_timeout ||= nil
if @deferred_timeout
@deferred_timeout.cancel
@deferred_timeout = nil
end
end
# Sugar for set_deferred_status(:succeeded, ...)
#
def succeed *args
set_deferred_status :succeeded, *args
end
alias set_deferred_success succeed
# Sugar for set_deferred_status(:failed, ...)
#
def fail *args
set_deferred_status :failed, *args
end
alias set_deferred_failure fail
end
# DefaultDeferrable is an otherwise empty class that includes Deferrable.
# This is very useful when you just need to return a Deferrable object
# as a way of communicating deferred status to some other part of a program.
class DefaultDeferrable
include Deferrable
end
end
@@ -0,0 +1,2 @@
warn "EM::Deferrable::Pool is deprecated, please use EM::Pool"
EM::Deferrable::Pool = EM::Pool
@@ -0,0 +1,73 @@
module EventMachine
# Utility class that is useful for file monitoring. Supported events are
#
# * File is modified
# * File is deleted
# * File is moved
#
# @note On Mac OS X, file watching only works when kqueue is enabled
#
# @see EventMachine.watch_file
class FileWatch < Connection
# @private
Cmodified = 'modified'.freeze
# @private
Cdeleted = 'deleted'.freeze
# @private
Cmoved = 'moved'.freeze
# @private
def receive_data(data)
case data
when Cmodified
file_modified
when Cdeleted
file_deleted
when Cmoved
file_moved
end
end
# Returns the path that is being monitored.
#
# @note Current implementation does not pick up on the new filename after a rename occurs.
#
# @return [String]
# @see EventMachine.watch_file
def path
@path
end
# Will be called when the file is modified. Supposed to be redefined by subclasses.
#
# @abstract
def file_modified
end
# Will be called when the file is deleted. Supposed to be redefined by subclasses.
# When the file is deleted, stop_watching will be called after this to make sure everything is
# cleaned up correctly.
#
# @note On Linux (with {http://en.wikipedia.org/wiki/Inotify inotify}), this method will not be called until *all* open file descriptors to
# the file have been closed.
#
# @abstract
def file_deleted
end
# Will be called when the file is moved or renamed. Supposed to be redefined by subclasses.
#
# @abstract
def file_moved
end
# Discontinue monitoring of the file.
#
# This involves cleaning up the underlying monitoring details with kqueue/inotify, and in turn firing {EventMachine::Connection#unbind}.
# This will be called automatically when a file is deleted. User code may call it as well.
def stop_watching
EventMachine::unwatch_filename(@signature)
end # stop_watching
end # FileWatch
end # EventMachine
@@ -0,0 +1,61 @@
#--
#
# Author:: Francis Cianfrocca (gmail: blackhedd)
# Homepage:: http://rubyeventmachine.com
# Date:: 16 Jul 2006
#
# See EventMachine and EventMachine::Connection for documentation and
# usage examples.
#
#----------------------------------------------------------------------------
#
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
# Gmail: blackhedd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of either: 1) 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; or 2) Ruby's License.
#
# See the file COPYING for complete licensing information.
#
#---------------------------------------------------------------------------
#
#
#--
# This defines EventMachine::Deferrable#future, which requires
# that the rest of EventMachine::Deferrable has already been seen.
# (It's in deferrable.rb.)
module EventMachine
module Deferrable
# A future is a sugaring of a typical deferrable usage.
#--
# Evaluate arg (which may be an expression or a block).
# What's the class of arg?
# If arg is an ordinary expression, then return it.
# If arg is deferrable (responds to :set_deferred_status),
# then look at the arguments. If either callback or errback
# are defined, then use them. If neither are defined, then
# use the supplied block (if any) as the callback.
# Then return arg.
def self.future arg, cb=nil, eb=nil, &blk
arg = arg.call if arg.respond_to?(:call)
if arg.respond_to?(:set_deferred_status)
if cb || eb
arg.callback(&cb) if cb
arg.errback(&eb) if eb
else
arg.callback(&blk) if blk
end
end
arg
end
end
end
@@ -0,0 +1,252 @@
module EventMachine
# A simple iterator for concurrent asynchronous work.
#
# Unlike ruby's built-in iterators, the end of the current iteration cycle is signaled manually,
# instead of happening automatically after the yielded block finishes executing. For example:
#
# (0..10).each{ |num| }
#
# becomes:
#
# EM::Iterator.new(0..10).each{ |num,iter| iter.next }
#
# This is especially useful when doing asynchronous work via reactor libraries and
# functions. For example, given a sync and async http api:
#
# response = sync_http_get(url); ...
# async_http_get(url){ |response| ... }
#
# a synchronous iterator such as:
#
# responses = urls.map{ |url| sync_http_get(url) }
# ...
# puts 'all done!'
#
# could be written as:
#
# EM::Iterator.new(urls).map(proc{ |url,iter|
# async_http_get(url){ |res|
# iter.return(res)
# }
# }, proc{ |responses|
# ...
# puts 'all done!'
# })
#
# Now, you can take advantage of the asynchronous api to issue requests in parallel. For example,
# to fetch 10 urls at a time, simply pass in a concurrency of 10:
#
# EM::Iterator.new(urls, 10).each do |url,iter|
# async_http_get(url){ iter.next }
# end
#
class Iterator
Stop = "EM::Stop"
# Create a new parallel async iterator with specified concurrency.
#
# i = EM::Iterator.new(1..100, 10)
#
# will create an iterator over the range that processes 10 items at a time. Iteration
# is started via #each, #map or #inject
#
# The list may either be an array-like object, or a proc that returns a new object
# to be processed each time it is called. If a proc is used, it must return
# EventMachine::Iterator::Stop to signal the end of the iterations.
#
def initialize(list, concurrency = 1)
raise ArgumentError, 'concurrency must be bigger than zero' unless (concurrency > 0)
if list.respond_to?(:call)
@list = nil
@list_proc = list
elsif list.respond_to?(:to_a)
@list = list.to_a.dup
@list_proc = nil
else
raise ArgumentError, 'argument must be a proc or an array'
end
@concurrency = concurrency
@started = false
@ended = false
end
# Change the concurrency of this iterator. Workers will automatically be spawned or destroyed
# to accomodate the new concurrency level.
#
def concurrency=(val)
old = @concurrency
@concurrency = val
spawn_workers if val > old and @started and !@ended
end
attr_reader :concurrency
# Iterate over a set of items using the specified block or proc.
#
# EM::Iterator.new(1..100).each do |num, iter|
# puts num
# iter.next
# end
#
# An optional second proc is invoked after the iteration is complete.
#
# EM::Iterator.new(1..100).each(
# proc{ |num,iter| iter.next },
# proc{ puts 'all done' }
# )
#
def each(foreach=nil, after=nil, &blk)
raise ArgumentError, 'proc or block required for iteration' unless foreach ||= blk
raise RuntimeError, 'cannot iterate over an iterator more than once' if @started or @ended
@started = true
@pending = 0
@workers = 0
all_done = proc{
after.call if after and @ended and @pending == 0
}
@process_next = proc{
# p [:process_next, :pending=, @pending, :workers=, @workers, :ended=, @ended, :concurrency=, @concurrency, :list=, @list]
unless @ended or @workers > @concurrency
item = next_item()
if item.equal?(Stop)
@ended = true
@workers -= 1
all_done.call
else
@pending += 1
is_done = false
on_done = proc{
raise RuntimeError, 'already completed this iteration' if is_done
is_done = true
@pending -= 1
if @ended
all_done.call
else
EM.next_tick(@process_next)
end
}
class << on_done
alias :next :call
end
foreach.call(item, on_done)
end
else
@workers -= 1
end
}
spawn_workers
self
end
# Collect the results of an asynchronous iteration into an array.
#
# EM::Iterator.new(%w[ pwd uptime uname date ], 2).map(proc{ |cmd,iter|
# EM.system(cmd){ |output,status|
# iter.return(output)
# }
# }, proc{ |results|
# p results
# })
#
def map(foreach, after)
index = 0
inject([], proc{ |results,item,iter|
i = index
index += 1
is_done = false
on_done = proc{ |res|
raise RuntimeError, 'already returned a value for this iteration' if is_done
is_done = true
results[i] = res
iter.return(results)
}
class << on_done
alias :return :call
def next
raise NoMethodError, 'must call #return on a map iterator'
end
end
foreach.call(item, on_done)
}, proc{ |results|
after.call(results)
})
end
# Inject the results of an asynchronous iteration onto a given object.
#
# EM::Iterator.new(%w[ pwd uptime uname date ], 2).inject({}, proc{ |hash,cmd,iter|
# EM.system(cmd){ |output,status|
# hash[cmd] = status.exitstatus == 0 ? output.strip : nil
# iter.return(hash)
# }
# }, proc{ |results|
# p results
# })
#
def inject(obj, foreach, after)
each(proc{ |item,iter|
is_done = false
on_done = proc{ |res|
raise RuntimeError, 'already returned a value for this iteration' if is_done
is_done = true
obj = res
iter.next
}
class << on_done
alias :return :call
def next
raise NoMethodError, 'must call #return on an inject iterator'
end
end
foreach.call(obj, item, on_done)
}, proc{
after.call(obj)
})
end
private
# Spawn workers to consume items from the iterator's enumerator based on the current concurrency level.
#
def spawn_workers
EM.next_tick(start_worker = proc{
if @workers < @concurrency and !@ended
# p [:spawning_worker, :workers=, @workers, :concurrency=, @concurrency, :ended=, @ended]
@workers += 1
@process_next.call
EM.next_tick(start_worker)
end
})
nil
end
# Return the next item from @list or @list_proc.
# Once items have run out, will return EM::Iterator::Stop. Procs must supply this themselves
def next_item
if @list_proc
@list_proc.call
else
@list.empty? ? Stop : @list.shift
end
end
end
end
# TODO: pass in one object instead of two? .each{ |iter| puts iter.current; iter.next }
# TODO: support iter.pause/resume/stop/break/continue?
# TODO: create some exceptions instead of using RuntimeError
@@ -0,0 +1,66 @@
#--
#
# Author:: Francis Cianfrocca (gmail: blackhedd)
# Homepage:: http://rubyeventmachine.com
# Date:: 16 Jul 2006
#
# See EventMachine and EventMachine::Connection for documentation and
# usage examples.
#
#----------------------------------------------------------------------------
#
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
# Gmail: blackhedd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of either: 1) 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; or 2) Ruby's License.
#
# See the file COPYING for complete licensing information.
#
#---------------------------------------------------------------------------
#
#
=begin
Message Routing in EventMachine.
The goal here is to enable "routing points," objects that can send and receive
"messages," which are delimited streams of bytes. The boundaries of a message
are preserved as it passes through the reactor system.
There will be several module methods defined in EventMachine to create route-point
objects (which will probably have a base class of EventMachine::MessageRouter
until someone suggests a better name).
As with I/O objects, routing objects will receive events by having the router
core call methods on them. And of course user code can and will define handlers
to deal with events of interest.
The message router base class only really needs a receive_message method. There will
be an EM module-method to send messages, in addition to the module methods to create
the various kinds of message receivers.
The simplest kind of message receiver object can receive messages by being named
explicitly in a parameter to EM#send_message. More sophisticated receivers can define
pub-sub selectors and message-queue names. And they can also define channels for
route-points in other processes or even on other machines.
A message is NOT a marshallable entity. Rather, it's a chunk of flat content more like
an Erlang message. Initially, all content submitted for transmission as a message will
have the to_s method called on it. Eventually, we'll be able to transmit certain structured
data types (XML and YAML documents, Structs within limits) and have them reconstructed
on the other end.
A fundamental goal of the message-routing capability is to interoperate seamlessly with
external systems, including non-Ruby systems like ActiveMQ. We will define various protocol
handlers for things like Stomp and possibly AMQP, but these will be wrapped up and hidden
from the users of the basic routing capability.
As with Erlang, a critical goal is for programs that are built to use message-passing to work
WITHOUT CHANGE when the code is re-based on a multi-process system.
=end
+151
View File
@@ -0,0 +1,151 @@
module EventMachine
# A simple async resource pool based on a resource and work queue. Resources
# are enqueued and work waits for resources to become available.
#
# @example
# require 'em-http-request'
#
# EM.run do
# pool = EM::Pool.new
# spawn = lambda { pool.add EM::HttpRequest.new('http://example.org') }
# 10.times { spawn[] }
# done, scheduled = 0, 0
#
# check = lambda do
# done += 1
# if done >= scheduled
# EM.stop
# end
# end
#
# pool.on_error { |conn| spawn[] }
#
# 100.times do |i|
# scheduled += 1
# pool.perform do |conn|
# req = conn.get :path => '/', :keepalive => true
#
# req.callback do
# p [:success, conn.object_id, i, req.response.size]
# check[]
# end
#
# req.errback { check[] }
#
# req
# end
# end
# end
#
# Resources are expected to be controlled by an object responding to a
# deferrable/completion style API with callback and errback blocks.
#
class Pool
def initialize
@resources = EM::Queue.new
@removed = []
@contents = []
@on_error = nil
end
def add resource
@contents << resource
requeue resource
end
def remove resource
@contents.delete resource
@removed << resource
end
# Returns a list for introspection purposes only. You should *NEVER* call
# modification or work oriented methods on objects in this list. A good
# example use case is periodic statistics collection against a set of
# connection resources.
#
# @example
# pool.contents.inject(0) { |sum, connection| connection.num_bytes }
def contents
@contents.dup
end
# Define a default catch-all for when the deferrables returned by work
# blocks enter a failed state. By default all that happens is that the
# resource is returned to the pool. If on_error is defined, this block is
# responsible for re-adding the resource to the pool if it is still usable.
# In other words, it is generally assumed that on_error blocks explicitly
# handle the rest of the lifetime of the resource.
def on_error *a, &b
@on_error = EM::Callback(*a, &b)
end
# Perform a given #call-able object or block. The callable object will be
# called with a resource from the pool as soon as one is available, and is
# expected to return a deferrable.
#
# The deferrable will have callback and errback added such that when the
# deferrable enters a finished state, the object is returned to the pool.
#
# If on_error is defined, then objects are not automatically returned to the
# pool.
def perform(*a, &b)
work = EM::Callback(*a, &b)
@resources.pop do |resource|
if removed? resource
@removed.delete resource
reschedule work
else
process work, resource
end
end
end
alias reschedule perform
# A peek at the number of enqueued jobs waiting for resources
def num_waiting
@resources.num_waiting
end
# Removed will show resources in a partial pruned state. Resources in the
# removed list may not appear in the contents list if they are currently in
# use.
def removed? resource
@removed.include? resource
end
protected
def requeue resource
@resources.push resource
end
def failure resource
if @on_error
@contents.delete resource
@on_error.call resource
# Prevent users from calling a leak.
@removed.delete resource
else
requeue resource
end
end
def completion deferrable, resource
deferrable.callback { requeue resource }
deferrable.errback { failure resource }
end
def process work, resource
deferrable = work.call resource
if deferrable.kind_of?(EM::Deferrable)
completion deferrable, resource
else
raise ArgumentError, "deferrable expected from work"
end
rescue
failure resource
raise
end
end
end
@@ -0,0 +1,45 @@
module EventMachine
# This is subclassed from EventMachine::Connection for use with the process monitoring API. Read the
# documentation on the instance methods of this class, and for a full explanation see EventMachine.watch_process.
class ProcessWatch < Connection
# @private
Cfork = 'fork'.freeze
# @private
Cexit = 'exit'.freeze
# @private
def receive_data(data)
case data
when Cfork
process_forked
when Cexit
process_exited
end
end
# Returns the pid that EventMachine::watch_process was originally called with.
def pid
@pid
end
# Should be redefined with the user's custom callback that will be fired when the prcess is forked.
#
# There is currently not an easy way to get the pid of the forked child.
def process_forked
end
# Should be redefined with the user's custom callback that will be fired when the process exits.
#
# stop_watching is called automatically after this callback
def process_exited
end
# Discontinue monitoring of the process.
# This will be called automatically when a process dies. User code may call it as well.
def stop_watching
EventMachine::unwatch_pid(@signature)
end
end
end
@@ -0,0 +1,123 @@
#--
#
# Author:: Francis Cianfrocca (gmail: blackhedd)
# Homepage:: http://rubyeventmachine.com
# Date:: 13 Dec 07
#
# See EventMachine and EventMachine::Connection for documentation and
# usage examples.
#
#----------------------------------------------------------------------------
#
# Copyright (C) 2006-08 by Francis Cianfrocca. All Rights Reserved.
# Gmail: blackhedd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of either: 1) 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; or 2) Ruby's License.
#
# See the file COPYING for complete licensing information.
#
#---------------------------------------------------------------------------
#
#
module EventMachine
# EM::DeferrableChildProcess is a sugaring of a common use-case
# involving EM::popen.
# Call the #open method on EM::DeferrableChildProcess, passing
# a command-string. #open immediately returns an EM::Deferrable
# object. It also schedules the forking of a child process, which
# will execute the command passed to #open.
# When the forked child terminates, the Deferrable will be signalled
# and execute its callbacks, passing the data that the child process
# wrote to stdout.
#
class DeferrableChildProcess < EventMachine::Connection
include EventMachine::Deferrable
# @private
def initialize
super
@data = []
end
# Sugars a common use-case involving forked child processes.
# #open takes a String argument containing an shell command
# string (including arguments if desired). #open immediately
# returns an EventMachine::Deferrable object, without blocking.
#
# It also invokes EventMachine#popen to run the passed-in
# command in a forked child process.
#
# When the forked child terminates, the Deferrable that
# #open calls its callbacks, passing the data returned
# from the child process.
#
def self.open cmd
EventMachine.popen( cmd, DeferrableChildProcess )
end
# @private
def receive_data data
@data << data
end
# @private
def unbind
succeed( @data.join )
end
end
# @private
class SystemCmd < EventMachine::Connection
def initialize cb
@cb = cb
@output = []
end
def receive_data data
@output << data
end
def unbind
@cb.call @output.join(''), get_status if @cb
end
end
# EM::system is a simple wrapper for EM::popen. It is similar to Kernel::system, but requires a
# single string argument for the command and performs no shell expansion.
#
# The block or proc passed to EM::system is called with two arguments: the output generated by the command,
# and a Process::Status that contains information about the command's execution.
#
# EM.run{
# EM.system('ls'){ |output,status| puts output if status.exitstatus == 0 }
# }
#
# You can also supply an additional proc to send some data to the process:
#
# EM.run{
# EM.system('sh', proc{ |process|
# process.send_data("echo hello\n")
# process.send_data("exit\n")
# }, proc{ |out,status|
# puts(out)
# })
# }
#
# Like EventMachine.popen, EventMachine.system currently does not work on windows.
# It returns the pid of the spawned process.
def EventMachine::system cmd, *args, &cb
cb ||= args.pop if args.last.is_a? Proc
init = args.pop if args.last.is_a? Proc
# merge remaining arguments into the command
cmd = [cmd, *args] if args.any?
EM.get_subprocess_pid(EM.popen(cmd, SystemCmd, cb) do |c|
init[c] if init
end.signature)
end
end
@@ -0,0 +1,37 @@
module EventMachine
# This module contains various protocol implementations, including:
# - HttpClient and HttpClient2
# - Stomp
# - Memcache
# - SmtpClient and SmtpServer
# - SASLauth and SASLauthclient
# - LineProtocol, LineAndTextProtocol and LineText2
# - HeaderAndContentProtocol
# - Postgres3
# - ObjectProtocol
#
# The protocol implementations live in separate files in the protocols/ subdirectory,
# but are auto-loaded when they are first referenced in your application.
#
# EventMachine::Protocols is also aliased to EM::P for easier usage.
#
module Protocols
# TODO : various autotools are completely useless with the lack of naming
# convention, we need to correct that!
autoload :TcpConnectTester, 'em/protocols/tcptest'
autoload :HttpClient, 'em/protocols/httpclient'
autoload :HttpClient2, 'em/protocols/httpclient2'
autoload :LineAndTextProtocol, 'em/protocols/line_and_text'
autoload :HeaderAndContentProtocol, 'em/protocols/header_and_content'
autoload :LineText2, 'em/protocols/linetext2'
autoload :Stomp, 'em/protocols/stomp'
autoload :SmtpClient, 'em/protocols/smtpclient'
autoload :SmtpServer, 'em/protocols/smtpserver'
autoload :SASLauth, 'em/protocols/saslauth'
autoload :Memcache, 'em/protocols/memcache'
autoload :Postgres3, 'em/protocols/postgres3'
autoload :ObjectProtocol, 'em/protocols/object_protocol'
autoload :Socks4, 'em/protocols/socks4'
autoload :LineProtocol, 'em/protocols/line_protocol'
end
end
@@ -0,0 +1,138 @@
#--
#
# Author:: Francis Cianfrocca (gmail: blackhedd)
# Homepage:: http://rubyeventmachine.com
# Date:: 15 Nov 2006
#
# See EventMachine and EventMachine::Connection for documentation and
# usage examples.
#
#----------------------------------------------------------------------------
#
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
# Gmail: blackhedd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of either: 1) 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; or 2) Ruby's License.
#
# See the file COPYING for complete licensing information.
#
#---------------------------------------------------------------------------
#
#
module EventMachine
module Protocols
# === Usage
#
# class RequestHandler < EM::P::HeaderAndContentProtocol
# def receive_request headers, content
# p [:request, headers, content]
# end
# end
#
# EM.run{
# EM.start_server 'localhost', 80, RequestHandler
# }
#
#--
# Originally, this subclassed LineAndTextProtocol, which in
# turn relies on BufferedTokenizer, which doesn't gracefully
# handle the transitions between lines and binary text.
# Changed 13Sep08 by FCianfrocca.
class HeaderAndContentProtocol < Connection
include LineText2
ContentLengthPattern = /Content-length:\s*(\d+)/i
def initialize *args
super
init_for_request
end
def receive_line line
case @hc_mode
when :discard_blanks
unless line == ""
@hc_mode = :headers
receive_line line
end
when :headers
if line == ""
raise "unrecognized state" unless @hc_headers.length > 0
if respond_to?(:receive_headers)
receive_headers @hc_headers
end
# @hc_content_length will be nil, not 0, if there was no content-length header.
if @hc_content_length.to_i > 0
set_binary_mode @hc_content_length
else
dispatch_request
end
else
@hc_headers << line
if ContentLengthPattern =~ line
# There are some attacks that rely on sending multiple content-length
# headers. This is a crude protection, but needs to become tunable.
raise "extraneous content-length header" if @hc_content_length
@hc_content_length = $1.to_i
end
if @hc_headers.length == 1 and respond_to?(:receive_first_header_line)
receive_first_header_line line
end
end
else
raise "internal error, unsupported mode"
end
end
def receive_binary_data text
@hc_content = text
dispatch_request
end
def dispatch_request
if respond_to?(:receive_request)
receive_request @hc_headers, @hc_content
end
init_for_request
end
private :dispatch_request
def init_for_request
@hc_mode = :discard_blanks
@hc_headers = []
# originally was @hc_headers ||= []; @hc_headers.clear to get a performance
# boost, but it's counterproductive because a subclassed handler will have to
# call dup to use the header array we pass in receive_headers.
@hc_content_length = nil
@hc_content = ""
end
private :init_for_request
# Basically a convenience method. We might create a subclass that does this
# automatically. But it's such a performance killer.
def headers_2_hash hdrs
self.class.headers_2_hash hdrs
end
class << self
def headers_2_hash hdrs
hash = {}
hdrs.each {|h|
if /\A([^\s:]+)\s*:\s*/ =~ h
tail = $'.dup
hash[ $1.downcase.gsub(/-/,"_").intern ] = tail
end
}
hash
end
end
end
end
end
@@ -0,0 +1,300 @@
#--
#
# Author:: Francis Cianfrocca (gmail: blackhedd)
# Homepage:: http://rubyeventmachine.com
# Date:: 16 July 2006
#
# See EventMachine and EventMachine::Connection for documentation and
# usage examples.
#
#----------------------------------------------------------------------------
#
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
# Gmail: blackhedd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of either: 1) 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; or 2) Ruby's License.
#
# See the file COPYING for complete licensing information.
#
#---------------------------------------------------------------------------
#
#
module EventMachine
module Protocols
# <b>Note:</b> This class is deprecated and will be removed. Please use EM-HTTP-Request instead.
#
# @example
# EventMachine.run {
# http = EventMachine::Protocols::HttpClient.request(
# :host => server,
# :port => 80,
# :request => "/index.html",
# :query_string => "parm1=value1&parm2=value2"
# )
# http.callback {|response|
# puts response[:status]
# puts response[:headers]
# puts response[:content]
# }
# }
#--
# TODO:
# Add streaming so we can support enormous POSTs. Current max is 20meg.
# Timeout for connections that run too long or hang somewhere in the middle.
# Persistent connections (HTTP/1.1), may need a associated delegate object.
# DNS: Some way to cache DNS lookups for hostnames we connect to. Ruby's
# DNS lookups are unbelievably slow.
# HEAD requests.
# Convenience methods for requests. get, post, url, etc.
# SSL.
# Handle status codes like 304, 100, etc.
# Refactor this code so that protocol errors all get handled one way (an exception?),
# instead of sprinkling set_deferred_status :failed calls everywhere.
class HttpClient < Connection
include EventMachine::Deferrable
MaxPostContentLength = 20 * 1024 * 1024
def initialize
warn "HttpClient is deprecated and will be removed. EM-Http-Request should be used instead."
@connected = false
end
# @param args [Hash] The request arguments
# @option args [String] :host The host IP/DNS name
# @option args [Integer] :port The port to connect too
# @option args [String] :verb The request type [GET | POST | DELETE | PUT]
# @option args [String] :request The request path
# @option args [Hash] :basic_auth The basic auth credentials (:username and :password)
# @option args [String] :content The request content
# @option args [String] :contenttype The content type (e.g. text/plain)
# @option args [String] :query_string The query string
# @option args [String] :host_header The host header to set
# @option args [String] :cookie Cookies to set
def self.request( args = {} )
args[:port] ||= 80
EventMachine.connect( args[:host], args[:port], self ) {|c|
# According to the docs, we will get here AFTER post_init is called.
c.instance_eval {@args = args}
}
end
def post_init
@start_time = Time.now
@data = ""
@read_state = :base
end
# We send the request when we get a connection.
# AND, we set an instance variable to indicate we passed through here.
# That allows #unbind to know whether there was a successful connection.
# NB: This naive technique won't work when we have to support multiple
# requests on a single connection.
def connection_completed
@connected = true
send_request @args
end
def send_request args
args[:verb] ||= args[:method] # Support :method as an alternative to :verb.
args[:verb] ||= :get # IS THIS A GOOD IDEA, to default to GET if nothing was specified?
verb = args[:verb].to_s.upcase
unless ["GET", "POST", "PUT", "DELETE", "HEAD"].include?(verb)
set_deferred_status :failed, {:status => 0} # TODO, not signalling the error type
return # NOTE THE EARLY RETURN, we're not sending any data.
end
request = args[:request] || "/"
unless request[0,1] == "/"
request = "/" + request
end
qs = args[:query_string] || ""
if qs.length > 0 and qs[0,1] != '?'
qs = "?" + qs
end
version = args[:version] || "1.1"
# Allow an override for the host header if it's not the connect-string.
host = args[:host_header] || args[:host] || "_"
# For now, ALWAYS tuck in the port string, although we may want to omit it if it's the default.
port = args[:port].to_i != 80 ? ":#{args[:port]}" : ""
# POST items.
postcontenttype = args[:contenttype] || "application/octet-stream"
postcontent = args[:content] || ""
raise "oversized content in HTTP POST" if postcontent.length > MaxPostContentLength
# ESSENTIAL for the request's line-endings to be CRLF, not LF. Some servers misbehave otherwise.
# TODO: We ASSUME the caller wants to send a 1.1 request. May not be a good assumption.
req = [
"#{verb} #{request}#{qs} HTTP/#{version}",
"Host: #{host}#{port}",
"User-agent: Ruby EventMachine",
]
if verb == "POST" || verb == "PUT"
req << "Content-type: #{postcontenttype}"
req << "Content-length: #{postcontent.length}"
end
# TODO, this cookie handler assumes it's getting a single, semicolon-delimited string.
# Eventually we will want to deal intelligently with arrays and hashes.
if args[:cookie]
req << "Cookie: #{args[:cookie]}"
end
# Allow custom HTTP headers, e.g. SOAPAction
args[:custom_headers].each do |k,v|
req << "#{k}: #{v}"
end if args[:custom_headers]
# Basic-auth stanza contributed by Matt Murphy.
if args[:basic_auth]
basic_auth_string = ["#{args[:basic_auth][:username]}:#{args[:basic_auth][:password]}"].pack('m').strip.gsub(/\n/,'')
req << "Authorization: Basic #{basic_auth_string}"
end
req << ""
reqstring = req.map {|l| "#{l}\r\n"}.join
send_data reqstring
if verb == "POST" || verb == "PUT"
send_data postcontent
end
end
def receive_data data
while data and data.length > 0
case @read_state
when :base
# Perform any per-request initialization here and don't consume any data.
@data = ""
@headers = []
@content_length = nil # not zero
@content = ""
@status = nil
@chunked = false
@chunk_length = nil
@read_state = :header
@connection_close = nil
when :header
ary = data.split( /\r?\n/m, 2 )
if ary.length == 2
data = ary.last
if ary.first == ""
if (@content_length and @content_length > 0) || @chunked || @connection_close
@read_state = :content
else
dispatch_response
@read_state = :base
end
else
@headers << ary.first
if @headers.length == 1
parse_response_line
elsif ary.first =~ /\Acontent-length:\s*/i
# Only take the FIRST content-length header that appears,
# which we can distinguish because @content_length is nil.
# TODO, it's actually a fatal error if there is more than one
# content-length header, because the caller is presumptively
# a bad guy. (There is an exploit that depends on multiple
# content-length headers.)
@content_length ||= $'.to_i
elsif ary.first =~ /\Aconnection:\s*close/i
@connection_close = true
elsif ary.first =~ /\Atransfer-encoding:\s*chunked/i
@chunked = true
end
end
else
@data << data
data = ""
end
when :content
if @chunked && @chunk_length
bytes_needed = @chunk_length - @chunk_read
new_data = data[0, bytes_needed]
@chunk_read += new_data.length
@content += new_data
data = data[bytes_needed..-1] || ""
if @chunk_length == @chunk_read && data[0,2] == "\r\n"
@chunk_length = nil
data = data[2..-1]
end
elsif @chunked
if (m = data.match(/\A(\S*)\r\n/m))
data = data[m[0].length..-1]
@chunk_length = m[1].to_i(16)
@chunk_read = 0
if @chunk_length == 0
dispatch_response
@read_state = :base
end
end
elsif @content_length
# If there was no content-length header, we have to wait until the connection
# closes. Everything we get until that point is content.
# TODO: Must impose a content-size limit, and also must implement chunking.
# Also, must support either temporary files for large content, or calling
# a content-consumer block supplied by the user.
bytes_needed = @content_length - @content.length
@content += data[0, bytes_needed]
data = data[bytes_needed..-1] || ""
if @content_length == @content.length
dispatch_response
@read_state = :base
end
else
@content << data
data = ""
end
end
end
end
# We get called here when we have received an HTTP response line.
# It's an opportunity to throw an exception or trigger other exceptional
# handling.
def parse_response_line
if @headers.first =~ /\AHTTP\/1\.[01] ([\d]{3})/
@status = $1.to_i
else
set_deferred_status :failed, {
:status => 0 # crappy way of signifying an unrecognized response. TODO, find a better way to do this.
}
close_connection
end
end
private :parse_response_line
def dispatch_response
@read_state = :base
set_deferred_status :succeeded, {
:content => @content,
:headers => @headers,
:status => @status
}
# TODO, we close the connection for now, but this is wrong for persistent clients.
close_connection
end
def unbind
if !@connected
set_deferred_status :failed, {:status => 0} # YECCCCH. Find a better way to signal no-connect/network error.
elsif (@read_state == :content and @content_length == nil)
dispatch_response
end
end
end
end
end
@@ -0,0 +1,600 @@
#--
#
# Author:: Francis Cianfrocca (gmail: blackhedd)
# Homepage:: http://rubyeventmachine.com
# Date:: 16 July 2006
#
# See EventMachine and EventMachine::Connection for documentation and
# usage examples.
#
#----------------------------------------------------------------------------
#
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
# Gmail: blackhedd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of either: 1) 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; or 2) Ruby's License.
#
# See the file COPYING for complete licensing information.
#
#---------------------------------------------------------------------------
#
#
module EventMachine
module Protocols
# <b>Note:</b> This class is deprecated and will be removed. Please use EM-HTTP-Request instead.
#
# === Usage
#
# EM.run{
# conn = EM::Protocols::HttpClient2.connect 'google.com', 80
#
# req = conn.get('/')
# req.callback{ |response|
# p(response.status)
# p(response.headers)
# p(response.content)
# }
# }
class HttpClient2 < Connection
include LineText2
def initialize
warn "HttpClient2 is deprecated and will be removed. EM-Http-Request should be used instead."
@authorization = nil
@closed = nil
@requests = nil
end
# @private
class Request
include Deferrable
attr_reader :version
attr_reader :status
attr_reader :header_lines
attr_reader :headers
attr_reader :content
attr_reader :internal_error
def initialize conn, args
@conn = conn
@args = args
@header_lines = []
@headers = {}
@blanks = 0
@chunk_trailer = nil
@chunking = nil
end
def send_request
az = @args[:authorization] and az = "Authorization: #{az}\r\n"
r = [
"#{@args[:verb]} #{@args[:uri]} HTTP/#{@args[:version] || "1.1"}\r\n",
"Host: #{@args[:host_header] || "_"}\r\n",
az || "",
"\r\n"
]
@conn.send_data r.join
end
#--
#
def receive_line ln
if @chunk_trailer
receive_chunk_trailer(ln)
elsif @chunking
receive_chunk_header(ln)
else
receive_header_line(ln)
end
end
#--
#
def receive_chunk_trailer ln
if ln.length == 0
@conn.pop_request
succeed(self)
else
p "Received chunk trailer line"
end
end
#--
# Allow up to ten blank lines before we get a real response line.
# Allow no more than 100 lines in the header.
#
def receive_header_line ln
if ln.length == 0
if @header_lines.length > 0
process_header
else
@blanks += 1
if @blanks > 10
@conn.close_connection
end
end
else
@header_lines << ln
if @header_lines.length > 100
@internal_error = :bad_header
@conn.close_connection
end
end
end
#--
# Cf RFC 2616 pgh 3.6.1 for the format of HTTP chunks.
#
def receive_chunk_header ln
if ln.length > 0
chunksize = ln.to_i(16)
if chunksize > 0
@conn.set_text_mode(ln.to_i(16))
else
@content = @content ? @content.join : ''
@chunk_trailer = true
end
else
# We correctly come here after each chunk gets read.
# p "Got A BLANK chunk line"
end
end
#--
# We get a single chunk. Append it to the incoming content and switch back to line mode.
#
def receive_chunked_text text
# p "RECEIVED #{text.length} CHUNK"
(@content ||= []) << text
end
#--
# TODO, inefficient how we're handling this. Part of it is done so as to
# make sure we don't have problems in detecting chunked-encoding, content-length,
# etc.
#
HttpResponseRE = /\AHTTP\/(1.[01]) ([\d]{3})/i
ClenRE = /\AContent-length:\s*(\d+)/i
ChunkedRE = /\ATransfer-encoding:\s*chunked/i
ColonRE = /\:\s*/
def process_header
unless @header_lines.first =~ HttpResponseRE
@conn.close_connection
@internal_error = :bad_request
end
@version = $1.dup
@status = $2.dup.to_i
clen = nil
chunks = nil
@header_lines.each_with_index do |e,ix|
if ix > 0
hdr,val = e.split(ColonRE,2)
(@headers[hdr.downcase] ||= []) << val
end
if clen == nil and e =~ ClenRE
clen = $1.dup.to_i
end
if e =~ ChunkedRE
chunks = true
end
end
if clen
# If the content length is zero we should not call set_text_mode,
# because a value of zero will make it wait forever, hanging the
# connection. Just return success instead, with empty content.
if clen == 0 then
@content = ""
@conn.pop_request
succeed(self)
else
@conn.set_text_mode clen
end
elsif chunks
@chunking = true
else
# Chunked transfer, multipart, or end-of-connection.
# For end-of-connection, we need to go the unbind
# method and suppress its desire to fail us.
p "NO CLEN"
p @args[:uri]
p @header_lines
@internal_error = :unsupported_clen
@conn.close_connection
end
end
private :process_header
def receive_text text
@chunking ? receive_chunked_text(text) : receive_sized_text(text)
end
#--
# At the present time, we only handle contents that have a length
# specified by the content-length header.
#
def receive_sized_text text
@content = text
@conn.pop_request
succeed(self)
end
end
# Make a connection to a remote HTTP server.
# Can take either a pair of arguments (which will be interpreted as
# a hostname/ip-address and a port), or a hash.
# If the arguments are a hash, then supported values include:
# :host => a hostname or ip-address
# :port => a port number
# :ssl => true to enable ssl
def self.connect *args
if args.length == 2
args = {:host=>args[0], :port=>args[1]}
else
args = args.first
end
h,prt,ssl = args[:host], Integer(args[:port]), (args[:tls] || args[:ssl])
conn = EM.connect( h, prt, self )
conn.start_tls if ssl
conn.set_default_host_header( h, prt, ssl )
conn
end
# Get a url
#
# req = conn.get(:uri => '/')
# req.callback{|response| puts response.content }
#
def get args
if args.is_a?(String)
args = {:uri=>args}
end
args[:verb] = "GET"
request args
end
# Post to a url
#
# req = conn.post('/data')
# req.callback{|response| puts response.content }
#--
# XXX there's no way to supply a POST body.. wtf?
def post args
if args.is_a?(String)
args = {:uri=>args}
end
args[:verb] = "POST"
request args
end
#--
# Compute and remember a string to be used as the host header in HTTP requests
# unless the user overrides it with an argument to #request.
#
# @private
def set_default_host_header host, port, ssl
if (ssl and port != 443) or (!ssl and port != 80)
@host_header = "#{host}:#{port}"
else
@host_header = host
end
end
# @private
def post_init
super
@connected = EM::DefaultDeferrable.new
end
# @private
def connection_completed
super
@connected.succeed
end
#--
# All pending requests, if any, must fail.
# We might come here without ever passing through connection_completed
# in case we can't connect to the server. We'll also get here when the
# connection closes (either because the server closes it, or we close it
# due to detecting an internal error or security violation).
# In either case, run down all pending requests, if any, and signal failure
# on them.
#
# Set and remember a flag (@closed) so we can immediately fail any
# subsequent requests.
#
# @private
def unbind
super
@closed = true
(@requests || []).each {|r| r.fail}
end
# @private
def request args
args[:host_header] = @host_header unless args.has_key?(:host_header)
args[:authorization] = @authorization unless args.has_key?(:authorization)
r = Request.new self, args
if @closed
r.fail
else
(@requests ||= []).unshift r
@connected.callback {r.send_request}
end
r
end
# @private
def receive_line ln
if req = @requests.last
req.receive_line ln
else
p "??????????"
p ln
end
end
# @private
def receive_binary_data text
@requests.last.receive_text text
end
#--
# Called by a Request object when it completes.
#
# @private
def pop_request
@requests.pop
end
end
=begin
class HttpClient2x < Connection
include LineText2
# TODO: Make this behave appropriate in case a #connect fails.
# Currently, this produces no errors.
# Make a connection to a remote HTTP server.
# Can take either a pair of arguments (which will be interpreted as
# a hostname/ip-address and a port), or a hash.
# If the arguments are a hash, then supported values include:
# :host => a hostname or ip-address;
# :port => a port number
#--
# TODO, support optional encryption arguments like :ssl
def self.connect *args
if args.length == 2
args = {:host=>args[0], :port=>args[1]}
else
args = args.first
end
h,prt = args[:host],Integer(args[:port])
EM.connect( h, prt, self, h, prt )
end
#--
# Sugars a connection that makes a single request and then
# closes the connection. Matches the behavior and the arguments
# of the original implementation of class HttpClient.
#
# Intended primarily for back compatibility, but the idiom
# is probably useful so it's not deprecated.
# We return a Deferrable, as did the original implementation.
#
# Because we're improving the way we deal with errors and exceptions
# (specifically, HTTP response codes other than 2xx will trigger the
# errback rather than the callback), this may break some existing code.
#
def self.request args
c = connect args
end
#--
# Requests can be pipelined. When we get a request, add it to the
# front of a queue as an array. The last element of the @requests
# array is always the oldest request received. Each element of the
# @requests array is a two-element array consisting of a hash with
# the original caller's arguments, and an initially-empty Ostruct
# containing the data we retrieve from the server's response.
# Maintain the instance variable @current_response, which is the response
# of the oldest pending request. That's just to make other code a little
# easier. If the variable doesn't exist when we come here, we're
# obviously the first request being made on the connection.
#
# The reason for keeping this method private (and requiring use of the
# convenience methods #get, #post, #head, etc) is to avoid the small
# performance penalty of canonicalizing the verb.
#
def request args
d = EventMachine::DefaultDeferrable.new
if @closed
d.fail
return d
end
o = OpenStruct.new
o.deferrable = d
(@requests ||= []).unshift [args, o]
@current_response ||= @requests.last.last
@connected.callback {
az = args[:authorization] and az = "Authorization: #{az}\r\n"
r = [
"#{args[:verb]} #{args[:uri]} HTTP/#{args[:version] || "1.1"}\r\n",
"Host: #{args[:host_header] || @host_header}\r\n",
az || "",
"\r\n"
]
p r
send_data r.join
}
o.deferrable
end
private :request
def get args
if args.is_a?(String)
args = {:uri=>args}
end
args[:verb] = "GET"
request args
end
def initialize host, port
super
@host_header = "#{host}:#{port}"
end
def post_init
super
@connected = EM::DefaultDeferrable.new
end
def connection_completed
super
@connected.succeed
end
#--
# Make sure to throw away any leftover incoming data if we've
# been closed due to recognizing an error.
#
# Generate an internal error if we get an unreasonable number of
# header lines. It could be malicious.
#
def receive_line ln
p ln
return if @closed
if ln.length > 0
(@current_response.headers ||= []).push ln
abort_connection if @current_response.headers.length > 100
else
process_received_headers
end
end
#--
# We come here when we've seen all the headers for a particular request.
# What we do next depends on the response line (which should be the
# first line in the header set), and whether there is content to read.
# We may transition into a text-reading state to read content, or
# we may abort the connection, or we may go right back into parsing
# responses for the next response in the chain.
#
# We make an ASSUMPTION that the first line is an HTTP response.
# Anything else produces an error that aborts the connection.
# This may not be enough, because it may be that responses to pipelined
# requests will come with a blank-line delimiter.
#
# Any non-2xx response will be treated as a fatal error, and abort the
# connection. We will set up the status and other response parameters.
# TODO: we will want to properly support 1xx responses, which some versions
# of IIS copiously generate.
# TODO: We need to give the option of not aborting the connection with certain
# non-200 responses, in order to work with NTLM and other authentication
# schemes that work at the level of individual connections.
#
# Some error responses will get sugarings. For example, we'll return the
# Location header in the response in case of a 301/302 response.
#
# Possible dispositions here:
# 1) No content to read (either content-length is zero or it's a HEAD request);
# 2) Switch to text mode to read a specific number of bytes;
# 3) Read a chunked or multipart response;
# 4) Read till the server closes the connection.
#
# Our reponse to the client can be either to wait till all the content
# has been read and then to signal caller's deferrable, or else to signal
# it when we finish the processing the headers and then expect the caller
# to have given us a block to call as the content comes in. And of course
# the latter gets stickier with chunks and multiparts.
#
HttpResponseRE = /\AHTTP\/(1.[01]) ([\d]{3})/i
ClenRE = /\AContent-length:\s*(\d+)/i
def process_received_headers
abort_connection unless @current_response.headers.first =~ HttpResponseRE
@current_response.version = $1.dup
st = $2.dup
@current_response.status = st.to_i
abort_connection unless st[0,1] == "2"
clen = nil
@current_response.headers.each do |e|
if clen == nil and e =~ ClenRE
clen = $1.dup.to_i
end
end
if clen
set_text_mode clen
end
end
private :process_received_headers
def receive_binary_data text
@current_response.content = text
@current_response.deferrable.succeed @current_response
@requests.pop
@current_response = (@requests.last || []).last
set_line_mode
end
# We've received either a server error or an internal error.
# Close the connection and abort any pending requests.
#--
# When should we call close_connection? It will cause #unbind
# to be fired. Should the user expect to see #unbind before
# we call #receive_http_error, or the other way around?
#
# Set instance variable @closed. That's used to inhibit further
# processing of any inbound data after an error has been recognized.
#
# We shouldn't have to worry about any leftover outbound data,
# because we call close_connection (not close_connection_after_writing).
# That ensures that any pipelined requests received after an error
# DO NOT get streamed out to the server on this connection.
# Very important. TODO, write a unit-test to establish that behavior.
#
def abort_connection
close_connection
@closed = true
@current_response.deferrable.fail( @current_response )
end
#------------------------
# Below here are user-overridable methods.
end
=end
end
end
@@ -0,0 +1,125 @@
#--
#
# Author:: Francis Cianfrocca (gmail: blackhedd)
# Homepage:: http://rubyeventmachine.com
# Date:: 15 November 2006
#
# See EventMachine and EventMachine::Connection for documentation and
# usage examples.
#
#----------------------------------------------------------------------------
#
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
# Gmail: blackhedd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of either: 1) 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; or 2) Ruby's License.
#
# See the file COPYING for complete licensing information.
#
#---------------------------------------------------------------------------
#
#
#
module EventMachine
module Protocols
# A protocol that handles line-oriented data with interspersed binary text.
#
# This version is optimized for performance. See EventMachine::Protocols::LineText2
# for a version which is optimized for correctness with regard to binary text blocks
# that can switch back to line mode.
class LineAndTextProtocol < Connection
MaxBinaryLength = 32*1024*1024
def initialize *args
super
lbp_init_line_state
end
def receive_data data
if @lbp_mode == :lines
begin
@lpb_buffer.extract(data).each do |line|
receive_line(line.chomp) if respond_to?(:receive_line)
end
rescue
receive_error('overlength line') if respond_to?(:receive_error)
close_connection
return
end
else
if @lbp_binary_limit > 0
wanted = @lbp_binary_limit - @lbp_binary_bytes_received
chunk = nil
if data.length > wanted
chunk = data.slice!(0...wanted)
else
chunk = data
data = ""
end
@lbp_binary_buffer[@lbp_binary_bytes_received...(@lbp_binary_bytes_received+chunk.length)] = chunk
@lbp_binary_bytes_received += chunk.length
if @lbp_binary_bytes_received == @lbp_binary_limit
receive_binary_data(@lbp_binary_buffer) if respond_to?(:receive_binary_data)
lbp_init_line_state
end
receive_data(data) if data.length > 0
else
receive_binary_data(data) if respond_to?(:receive_binary_data)
data = ""
end
end
end
def unbind
if @lbp_mode == :binary and @lbp_binary_limit > 0
if respond_to?(:receive_binary_data)
receive_binary_data( @lbp_binary_buffer[0...@lbp_binary_bytes_received] )
end
end
end
# Set up to read the supplied number of binary bytes.
# This recycles all the data currently waiting in the line buffer, if any.
# If the limit is nil, then ALL subsequent data will be treated as binary
# data and passed to the upstream protocol handler as we receive it.
# If a limit is given, we'll hold the incoming binary data and not
# pass it upstream until we've seen it all, or until there is an unbind
# (in which case we'll pass up a partial).
# Specifying nil for the limit (the default) means there is no limit.
# Specifiyng zero for the limit will cause an immediate transition back to line mode.
#
def set_binary_mode size = nil
if @lbp_mode == :lines
if size == 0
receive_binary_data("") if respond_to?(:receive_binary_data)
# Do no more work here. Stay in line mode and keep consuming data.
else
@lbp_binary_limit = size.to_i # (nil will be stored as zero)
if @lbp_binary_limit > 0
raise "Overlength" if @lbp_binary_limit > MaxBinaryLength # arbitrary sanity check
@lbp_binary_buffer = "\0" * @lbp_binary_limit
@lbp_binary_bytes_received = 0
end
@lbp_mode = :binary
receive_data @lpb_buffer.flush
end
else
raise "invalid operation"
end
end
#--
# For internal use, establish protocol baseline for handling lines.
def lbp_init_line_state
@lpb_buffer = BufferedTokenizer.new("\n")
@lbp_mode = :lines
end
private :lbp_init_line_state
end
end
end
@@ -0,0 +1,29 @@
module EventMachine
module Protocols
# LineProtocol will parse out newline terminated strings from a receive_data stream
#
# module Server
# include EM::P::LineProtocol
#
# def receive_line(line)
# send_data("you said: #{line}")
# end
# end
#
module LineProtocol
# @private
def receive_data data
(@buf ||= '') << data
while @buf.slice!(/(.*?)\r?\n/)
receive_line($1)
end
end
# Invoked with lines received over the network
def receive_line(line)
# stub
end
end
end
end
@@ -0,0 +1,179 @@
#--
#
# Author:: Francis Cianfrocca (gmail: blackhedd)
# Homepage:: http://rubyeventmachine.com
# Date:: 15 November 2006
#
# See EventMachine and EventMachine::Connection for documentation and
# usage examples.
#
#----------------------------------------------------------------------------
#
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
# Gmail: blackhedd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of either: 1) 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; or 2) Ruby's License.
#
# See the file COPYING for complete licensing information.
#
#---------------------------------------------------------------------------
#
#
module EventMachine
module Protocols
# In the grand, time-honored tradition of re-inventing the wheel, we offer
# here YET ANOTHER protocol that handles line-oriented data with interspersed
# binary text. This one trades away some of the performance optimizations of
# EventMachine::Protocols::LineAndTextProtocol in order to get better correctness
# with regard to binary text blocks that can switch back to line mode. It also
# permits the line-delimiter to change in midstream.
# This was originally written to support Stomp.
module LineText2
# TODO! We're not enforcing the limits on header lengths and text-lengths.
# When we get around to that, call #receive_error if the user defined it, otherwise
# throw exceptions.
MaxBinaryLength = 32*1024*1024
#--
# Will loop internally until there's no data left to read.
# That way the user-defined handlers we call can modify the
# handling characteristics on a per-token basis.
#
def receive_data data
return unless (data and data.length > 0)
# Do this stuff in lieu of a constructor.
@lt2_mode ||= :lines
@lt2_delimiter ||= "\n"
@lt2_linebuffer ||= []
remaining_data = data
while remaining_data.length > 0
if @lt2_mode == :lines
delimiter_string = case @lt2_delimiter
when Regexp
remaining_data.slice(@lt2_delimiter)
else
@lt2_delimiter
end
ix = remaining_data.index(delimiter_string) if delimiter_string
if ix
@lt2_linebuffer << remaining_data[0...ix]
ln = @lt2_linebuffer.join
@lt2_linebuffer.clear
if @lt2_delimiter == "\n"
ln.chomp!
end
receive_line ln
remaining_data = remaining_data[(ix+delimiter_string.length)..-1]
else
@lt2_linebuffer << remaining_data
remaining_data = ""
end
elsif @lt2_mode == :text
if @lt2_textsize
needed = @lt2_textsize - @lt2_textpos
will_take = if remaining_data.length > needed
needed
else
remaining_data.length
end
@lt2_textbuffer << remaining_data[0...will_take]
tail = remaining_data[will_take..-1]
@lt2_textpos += will_take
if @lt2_textpos >= @lt2_textsize
# Reset line mode (the default behavior) BEFORE calling the
# receive_binary_data. This makes it possible for user code
# to call set_text_mode, enabling chains of text blocks
# (which can possibly be of different sizes).
set_line_mode
receive_binary_data @lt2_textbuffer.join
receive_end_of_binary_data
end
remaining_data = tail
else
receive_binary_data remaining_data
remaining_data = ""
end
end
end
end
# The line delimiter may be a regular expression or a string. Anything
# passed to set_delimiter other than a regular expression will be
# converted to a string.
def set_delimiter delim
@lt2_delimiter = case delim
when Regexp
delim
else
delim.to_s
end
end
# Called internally but also exposed to user code, for the case in which
# processing of binary data creates a need to transition back to line mode.
# We support an optional parameter to "throw back" some data, which might
# be an umprocessed chunk of the transmitted binary data, or something else
# entirely.
def set_line_mode data=""
@lt2_mode = :lines
(@lt2_linebuffer ||= []).clear
receive_data data.to_s
end
def set_text_mode size=nil
if size == 0
set_line_mode
else
@lt2_mode = :text
(@lt2_textbuffer ||= []).clear
@lt2_textsize = size # which can be nil, signifying no limit
@lt2_textpos = 0
end
end
# Alias for #set_text_mode, added for back-compatibility with LineAndTextProtocol.
def set_binary_mode size=nil
set_text_mode size
end
# In case of a dropped connection, we'll send a partial buffer to user code
# when in sized text mode. User overrides of #receive_binary_data need to
# be aware that they may get a short buffer.
def unbind
@lt2_mode ||= nil
if @lt2_mode == :text and @lt2_textpos > 0
receive_binary_data @lt2_textbuffer.join
end
end
# Stub. Should be subclassed by user code.
def receive_line ln
# no-op
end
# Stub. Should be subclassed by user code.
def receive_binary_data data
# no-op
end
# Stub. Should be subclassed by user code.
# This is called when transitioning internally from text mode
# back to line mode. Useful when client code doesn't want
# to keep track of how much data it's received.
def receive_end_of_binary_data
# no-op
end
end
end
end
@@ -0,0 +1,331 @@
module EventMachine
module Protocols
# Implements the Memcache protocol (http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt).
# Requires memcached >= 1.2.4 w/ noreply support
#
# == Usage example
#
# EM.run{
# cache = EM::P::Memcache.connect 'localhost', 11211
#
# cache.set :a, 'hello'
# cache.set :b, 'hi'
# cache.set :c, 'how are you?'
# cache.set :d, ''
#
# cache.get(:a){ |v| p v }
# cache.get_hash(:a, :b, :c, :d){ |v| p v }
# cache.get(:a,:b,:c,:d){ |a,b,c,d| p [a,b,c,d] }
#
# cache.get(:a,:z,:b,:y,:d){ |a,z,b,y,d| p [a,z,b,y,d] }
#
# cache.get(:missing){ |m| p [:missing=, m] }
# cache.set(:missing, 'abc'){ p :stored }
# cache.get(:missing){ |m| p [:missing=, m] }
# cache.del(:missing){ p :deleted }
# cache.get(:missing){ |m| p [:missing=, m] }
# }
#
module Memcache
include EM::Deferrable
##
# constants
unless defined? Cempty
# @private
Cstored = 'STORED'.freeze
# @private
Cend = 'END'.freeze
# @private
Cdeleted = 'DELETED'.freeze
# @private
Cunknown = 'NOT_FOUND'.freeze
# @private
Cerror = 'ERROR'.freeze
# @private
Cempty = ''.freeze
# @private
Cdelimiter = "\r\n".freeze
end
##
# commands
# Get the value associated with one or multiple keys
#
# cache.get(:a){ |v| p v }
# cache.get(:a,:b,:c,:d){ |a,b,c,d| p [a,b,c,d] }
#
def get *keys
raise ArgumentError unless block_given?
callback{
keys = keys.map{|k| k.to_s.gsub(/\s/,'_') }
send_data "get #{keys.join(' ')}\r\n"
@get_cbs << [keys, proc{ |values|
yield *keys.map{ |k| values[k] }
}]
}
end
# Set the value for a given key
#
# cache.set :a, 'hello'
# cache.set(:missing, 'abc'){ puts "stored the value!" }
#
def set key, val, exptime = 0, &cb
callback{
val = val.to_s
send_cmd :set, key, 0, exptime, val.respond_to?(:bytesize) ? val.bytesize : val.size, !block_given?
send_data val
send_data Cdelimiter
@set_cbs << cb if cb
}
end
# Gets multiple values as a hash
#
# cache.get_hash(:a, :b, :c, :d){ |h| puts h[:a] }
#
def get_hash *keys
raise ArgumentError unless block_given?
get *keys do |*values|
yield keys.inject({}){ |hash, k| hash.update k => values[keys.index(k)] }
end
end
# Delete the value associated with a key
#
# cache.del :a
# cache.del(:b){ puts "deleted the value!" }
#
def delete key, expires = 0, &cb
callback{
send_data "delete #{key} #{expires}#{cb ? '' : ' noreply'}\r\n"
@del_cbs << cb if cb
}
end
alias del delete
# Connect to a memcached server (must support NOREPLY, memcached >= 1.2.4)
def self.connect host = 'localhost', port = 11211
EM.connect host, port, self, host, port
end
def send_cmd cmd, key, flags = 0, exptime = 0, bytes = 0, noreply = false
send_data "#{cmd} #{key} #{flags} #{exptime} #{bytes}#{noreply ? ' noreply' : ''}\r\n"
end
private :send_cmd
##
# errors
# @private
class ParserError < StandardError
end
##
# em hooks
# @private
def initialize host, port = 11211
@host, @port = host, port
end
# @private
def connection_completed
@get_cbs = []
@set_cbs = []
@del_cbs = []
@values = {}
@reconnecting = false
@connected = true
succeed
# set_delimiter "\r\n"
# set_line_mode
end
#--
# 19Feb09 Switched to a custom parser, LineText2 is recursive and can cause
# stack overflows when there is too much data.
# include EM::P::LineText2
# @private
def receive_data data
(@buffer||='') << data
while index = @buffer.index(Cdelimiter)
begin
line = @buffer.slice!(0,index+2)
process_cmd line
rescue ParserError
@buffer[0...0] = line
break
end
end
end
#--
# def receive_line line
# @private
def process_cmd line
case line.strip
when /^VALUE\s+(.+?)\s+(\d+)\s+(\d+)/ # VALUE <key> <flags> <bytes>
bytes = Integer($3)
# set_binary_mode bytes+2
# @cur_key = $1
if @buffer.size >= bytes + 2
@values[$1] = @buffer.slice!(0,bytes)
@buffer.slice!(0,2) # \r\n
else
raise ParserError
end
when Cend # END
if entry = @get_cbs.shift
keys, cb = entry
cb.call(@values)
end
@values = {}
when Cstored # STORED
if cb = @set_cbs.shift
cb.call(true)
end
when Cdeleted # DELETED
if cb = @del_cbs.shift
cb.call(true)
end
when Cunknown # NOT_FOUND
if cb = @del_cbs.shift
cb.call(false)
end
else
p [:MEMCACHE_UNKNOWN, line]
end
end
#--
# def receive_binary_data data
# @values[@cur_key] = data[0..-3]
# end
# @private
def unbind
if @connected or @reconnecting
EM.add_timer(1){ reconnect @host, @port }
@connected = false
@reconnecting = true
@deferred_status = nil
else
raise 'Unable to connect to memcached server'
end
end
end
end
end
if __FILE__ == $0
# ruby -I ext:lib -r eventmachine -rubygems lib/protocols/memcache.rb
require 'em/spec'
# @private
class TestConnection
include EM::P::Memcache
def send_data data
sent_data << data
end
def sent_data
@sent_data ||= ''
end
def initialize
connection_completed
end
end
EM.describe EM::Protocols::Memcache do
before{
@c = TestConnection.new
}
should 'send get requests' do
@c.get('a'){}
@c.sent_data.should == "get a\r\n"
done
end
should 'send set requests' do
@c.set('a', 1){}
@c.sent_data.should == "set a 0 0 1\r\n1\r\n"
done
end
should 'use noreply on set without block' do
@c.set('a', 1)
@c.sent_data.should == "set a 0 0 1 noreply\r\n1\r\n"
done
end
should 'send delete requests' do
@c.del('a')
@c.sent_data.should == "delete a 0 noreply\r\n"
done
end
should 'work when get returns no values' do
@c.get('a'){ |a|
a.should.be.nil
done
}
@c.receive_data "END\r\n"
end
should 'invoke block on set' do
@c.set('a', 1){
done
}
@c.receive_data "STORED\r\n"
end
should 'invoke block on delete' do
@c.delete('a'){ |found|
found.should.be.false
}
@c.delete('b'){ |found|
found.should.be.true
done
}
@c.receive_data "NOT_FOUND\r\n"
@c.receive_data "DELETED\r\n"
end
should 'parse split responses' do
@c.get('a'){ |a|
a.should == 'abc'
done
}
@c.receive_data "VAL"
@c.receive_data "UE a 0 "
@c.receive_data "3\r\n"
@c.receive_data "ab"
@c.receive_data "c"
@c.receive_data "\r\n"
@c.receive_data "EN"
@c.receive_data "D\r\n"
end
end
end
@@ -0,0 +1,46 @@
module EventMachine
module Protocols
# ObjectProtocol allows for easy communication using marshaled ruby objects
#
# module RubyServer
# include EM::P::ObjectProtocol
#
# def receive_object obj
# send_object({'you said' => obj})
# end
# end
#
module ObjectProtocol
# By default returns Marshal, override to return JSON or YAML, or any
# other serializer/deserializer responding to #dump and #load.
def serializer
Marshal
end
# @private
def receive_data data
(@buf ||= '') << data
while @buf.size >= 4
if @buf.size >= 4+(size=@buf.unpack('N').first)
@buf.slice!(0,4)
receive_object serializer.load(@buf.slice!(0,size))
else
break
end
end
end
# Invoked with ruby objects received over the network
def receive_object obj
# stub
end
# Sends a ruby object over the network
def send_object obj
data = serializer.dump(obj)
send_data [data.respond_to?(:bytesize) ? data.bytesize : data.size, data].pack('Na*')
end
end
end
end
@@ -0,0 +1,246 @@
#--
#
# Author:: Francis Cianfrocca (gmail: blackhedd)
# Homepage:: http://rubyeventmachine.com
# Date:: 15 November 2006
#
# See EventMachine and EventMachine::Connection for documentation and
# usage examples.
#
#----------------------------------------------------------------------------
#
# Copyright (C) 2006-08 by Francis Cianfrocca. All Rights Reserved.
# Gmail: blackhedd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of either: 1) 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; or 2) Ruby's License.
#
# See the file COPYING for complete licensing information.
#
#---------------------------------------------------------------------------
#
#
#
require 'postgres-pr/message'
require 'postgres-pr/connection'
require 'stringio'
# @private
class StringIO
# Reads exactly +n+ bytes.
#
# If the data read is nil an EOFError is raised.
#
# If the data read is too short an IOError is raised
def readbytes(n)
str = read(n)
if str == nil
raise EOFError, "End of file reached"
end
if str.size < n
raise IOError, "data truncated"
end
str
end
alias read_exactly_n_bytes readbytes
end
module EventMachine
module Protocols
# PROVISIONAL IMPLEMENTATION of an evented Postgres client.
# This implements version 3 of the Postgres wire protocol, which will work
# with any Postgres version from roughly 7.4 onward.
#
# Objective: we want to access Postgres databases without requiring threads.
# Until now this has been a problem because the Postgres client implementations
# have all made use of blocking I/O calls, which is incompatible with a
# thread-free evented model.
#
# But rather than re-implement the Postgres Wire3 protocol, we're taking advantage
# of the existing postgres-pr library, which was originally written by Michael
# Neumann but (at this writing) appears to be no longer maintained. Still, it's
# in basically a production-ready state, and the wire protocol isn't that complicated
# anyway.
#
# We're tucking in a bunch of require statements that may not be present in garden-variety
# EM installations. Until we find a good way to only require these if a program
# requires postgres, this file will need to be required explicitly.
#
# We need to monkeypatch StringIO because it lacks the #readbytes method needed
# by postgres-pr.
# The StringIO monkeypatch is lifted from the standard library readbytes.rb,
# which adds method #readbytes directly to class IO. But StringIO is not a subclass of IO.
# It is modified to raise an IOError instead of TruncatedDataException since the exception is unused.
#
# We cloned the handling of postgres messages from lib/postgres-pr/connection.rb
# in the postgres-pr library, and modified it for event-handling.
#
# TODO: The password handling in dispatch_conn_message is totally incomplete.
#
#
# We return Deferrables from the user-level operations surfaced by this interface.
# Experimentally, we're using the pattern of always returning a boolean value as the
# first argument of a deferrable callback to indicate success or failure. This is
# instead of the traditional pattern of calling Deferrable#succeed or #fail, and
# requiring the user to define both a callback and an errback function.
#
# === Usage
# EM.run {
# db = EM.connect_unix_domain( "/tmp/.s.PGSQL.5432", EM::P::Postgres3 )
# db.connect( dbname, username, psw ).callback do |status|
# if status
# db.query( "select * from some_table" ).callback do |status, result, errors|
# if status
# result.rows.each do |row|
# p row
# end
# end
# end
# end
# end
# }
class Postgres3 < EventMachine::Connection
include PostgresPR
def initialize
@data = ""
@params = {}
end
def connect db, user, psw=nil
d = EM::DefaultDeferrable.new
d.timeout 15
if @pending_query || @pending_conn
d.succeed false, "Operation already in progress"
else
@pending_conn = d
prms = {"user"=>user, "database"=>db}
@user = user
if psw
@password = psw
#prms["password"] = psw
end
send_data PostgresPR::StartupMessage.new( 3 << 16, prms ).dump
end
d
end
def query sql
d = EM::DefaultDeferrable.new
d.timeout 15
if @pending_query || @pending_conn
d.succeed false, "Operation already in progress"
else
@r = PostgresPR::Connection::Result.new
@e = []
@pending_query = d
send_data PostgresPR::Query.dump(sql)
end
d
end
def receive_data data
@data << data
while @data.length >= 5
pktlen = @data[1...5].unpack("N").first
if @data.length >= (1 + pktlen)
pkt = @data.slice!(0...(1+pktlen))
m = StringIO.open( pkt, "r" ) {|io| PostgresPR::Message.read( io ) }
if @pending_conn
dispatch_conn_message m
elsif @pending_query
dispatch_query_message m
else
raise "Unexpected message from database"
end
else
break # very important, break out of the while
end
end
end
def unbind
if o = (@pending_query || @pending_conn)
o.succeed false, "lost connection"
end
end
# Cloned and modified from the postgres-pr.
def dispatch_conn_message msg
case msg
when AuthentificationClearTextPassword
raise ArgumentError, "no password specified" if @password.nil?
send_data PasswordMessage.new(@password).dump
when AuthentificationCryptPassword
raise ArgumentError, "no password specified" if @password.nil?
send_data PasswordMessage.new(@password.crypt(msg.salt)).dump
when AuthentificationMD5Password
raise ArgumentError, "no password specified" if @password.nil?
require 'digest/md5'
m = Digest::MD5.hexdigest(@password + @user)
m = Digest::MD5.hexdigest(m + msg.salt)
m = 'md5' + m
send_data PasswordMessage.new(m).dump
when AuthentificationKerberosV4, AuthentificationKerberosV5, AuthentificationSCMCredential
raise "unsupported authentification"
when AuthentificationOk
when ErrorResponse
raise msg.field_values.join("\t")
when NoticeResponse
@notice_processor.call(msg) if @notice_processor
when ParameterStatus
@params[msg.key] = msg.value
when BackendKeyData
# TODO
#p msg
when ReadyForQuery
# TODO: use transaction status
pc,@pending_conn = @pending_conn,nil
pc.succeed true
else
raise "unhandled message type"
end
end
# Cloned and modified from the postgres-pr.
def dispatch_query_message msg
case msg
when DataRow
@r.rows << msg.columns
when CommandComplete
@r.cmd_tag = msg.cmd_tag
when ReadyForQuery
pq,@pending_query = @pending_query,nil
pq.succeed true, @r, @e
when RowDescription
@r.fields = msg.fields
when CopyInResponse
when CopyOutResponse
when EmptyQueryResponse
when ErrorResponse
# TODO
@e << msg
when NoticeResponse
@notice_processor.call(msg) if @notice_processor
else
# TODO
end
end
end
end
end
@@ -0,0 +1,175 @@
#--
#
# Author:: Francis Cianfrocca (gmail: blackhedd)
# Homepage:: http://rubyeventmachine.com
# Date:: 15 November 2006
#
# See EventMachine and EventMachine::Connection for documentation and
# usage examples.
#
#----------------------------------------------------------------------------
#
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
# Gmail: blackhedd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of either: 1) 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; or 2) Ruby's License.
#
# See the file COPYING for complete licensing information.
#
#---------------------------------------------------------------------------
#
#
#
module EventMachine
module Protocols
# Implements SASL authd.
# This is a very, very simple protocol that mimics the one used
# by saslauthd and pwcheck, two outboard daemons included in the
# standard SASL library distro.
# The only thing this is really suitable for is SASL PLAIN
# (user+password) authentication, but the SASL libs that are
# linked into standard servers (like imapd and sendmail) implement
# the other ones.
#
# SASL-auth is intended for reasonably fast operation inside a
# single machine, so it has no transport-security (although there
# have been multi-machine extensions incorporating transport-layer
# encryption).
#
# The standard saslauthd module generally runs privileged and does
# its work by referring to the system-account files.
#
# This feature was added to EventMachine to enable the development
# of custom authentication/authorization engines for standard servers.
#
# To use SASLauth, include it in a class that subclasses EM::Connection,
# and reimplement the validate method.
#
# The typical way to incorporate this module into an authentication
# daemon would be to set it as the handler for a UNIX-domain socket.
# The code might look like this:
#
# EM.start_unix_domain_server( "/var/run/saslauthd/mux", MyHandler )
# File.chmod( 0777, "/var/run/saslauthd/mux")
#
# The chmod is probably needed to ensure that unprivileged clients can
# access the UNIX-domain socket.
#
# It's also a very good idea to drop superuser privileges (if any), after
# the UNIX-domain socket has been opened.
#--
# Implementation details: assume the client can send us pipelined requests,
# and that the client will close the connection.
#
# The client sends us four values, each encoded as a two-byte length field in
# network order followed by the specified number of octets.
# The fields specify the username, password, service name (such as imap),
# and the "realm" name. We send back the barest minimum reply, a single
# field also encoded as a two-octet length in network order, followed by
# either "NO" or "OK" - simplicity itself.
#
# We enforce a maximum field size just as a sanity check.
# We do NOT automatically time out the connection.
#
# The code we use to parse out the values is ugly and probably slow.
# Improvements welcome.
#
module SASLauth
MaxFieldSize = 128*1024
def post_init
super
@sasl_data = ""
@sasl_values = []
end
def receive_data data
@sasl_data << data
while @sasl_data.length >= 2
len = (@sasl_data[0,2].unpack("n")).first
raise "SASL Max Field Length exceeded" if len > MaxFieldSize
if @sasl_data.length >= (len + 2)
@sasl_values << @sasl_data[2,len]
@sasl_data.slice!(0...(2+len))
if @sasl_values.length == 4
send_data( validate(*@sasl_values) ? "\0\002OK" : "\0\002NO" )
@sasl_values.clear
end
else
break
end
end
end
def validate username, psw, sysname, realm
p username
p psw
p sysname
p realm
true
end
end
# Implements the SASL authd client protocol.
# This is a very, very simple protocol that mimics the one used
# by saslauthd and pwcheck, two outboard daemons included in the
# standard SASL library distro.
# The only thing this is really suitable for is SASL PLAIN
# (user+password) authentication, but the SASL libs that are
# linked into standard servers (like imapd and sendmail) implement
# the other ones.
#
# You can use this module directly as a handler for EM Connections,
# or include it in a module or handler class of your own.
#
# First connect to a SASL server (it's probably a TCP server, or more
# likely a Unix-domain socket). Then call the #validate? method,
# passing at least a username and a password. #validate? returns
# a Deferrable which will either succeed or fail, depending
# on the status of the authentication operation.
#
module SASLauthclient
MaxFieldSize = 128*1024
def validate? username, psw, sysname=nil, realm=nil
str = [username, psw, sysname, realm].map {|m|
[(m || "").length, (m || "")]
}.flatten.pack( "nA*" * 4 )
send_data str
d = EM::DefaultDeferrable.new
@queries.unshift d
d
end
def post_init
@sasl_data = ""
@queries = []
end
def receive_data data
@sasl_data << data
while @sasl_data.length > 2
len = (@sasl_data[0,2].unpack("n")).first
raise "SASL Max Field Length exceeded" if len > MaxFieldSize
if @sasl_data.length >= (len + 2)
val = @sasl_data[2,len]
@sasl_data.slice!(0...(2+len))
q = @queries.pop
(val == "NO") ? q.fail : q.succeed
else
break
end
end
end
end
end
end
@@ -0,0 +1,394 @@
#--
#
# Author:: Francis Cianfrocca (gmail: blackhedd)
# Homepage:: http://rubyeventmachine.com
# Date:: 16 July 2006
#
# See EventMachine and EventMachine::Connection for documentation and
# usage examples.
#
#----------------------------------------------------------------------------
#
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
# Gmail: blackhedd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of either: 1) 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; or 2) Ruby's License.
#
# See the file COPYING for complete licensing information.
#
#---------------------------------------------------------------------------
#
#
require 'ostruct'
module EventMachine
module Protocols
# Simple SMTP client
#
# @example
# email = EM::Protocols::SmtpClient.send(
# :domain=>"example.com",
# :host=>'localhost',
# :port=>25, # optional, defaults 25
# :starttls=>true, # use ssl
# :from=>"sender@example.com",
# :to=> ["to_1@example.com", "to_2@example.com"],
# :header=> {"Subject" => "This is a subject line"},
# :body=> "This is the body of the email"
# )
# email.callback{
# puts 'Email sent!'
# }
# email.errback{ |e|
# puts 'Email failed!'
# }
#
# Sending generated emails (using Mail)
#
# mail = Mail.new do
# from 'alice@example.com'
# to 'bob@example.com'
# subject 'This is a test email'
# body 'Hello, world!'
# end
#
# email = EM::P::SmtpClient.send(
# :domain=>'example.com',
# :from=>mail.from.first,
# :to=>mail.to,
# :message=>mail.to_s
# )
#
class SmtpClient < Connection
include EventMachine::Deferrable
include EventMachine::Protocols::LineText2
def initialize
@succeeded = nil
@responder = nil
@code = nil
@msg = nil
end
# :host => required String
# a string containing the IP address or host name of the SMTP server to connect to.
# :port => optional
# defaults to 25.
# :domain => required String
# This is passed as the argument to the EHLO command.
# :starttls => optional Boolean
# If it evaluates true, then the client will initiate STARTTLS with
# the server, and abort the connection if the negotiation doesn't succeed.
# TODO, need to be able to pass certificate parameters with this option.
# :auth => optional Hash of auth parameters
# If not given, then no auth will be attempted.
# (In that case, the connection will be aborted if the server requires auth.)
# Specify the hash value :type to determine the auth type, along with additional parameters
# depending on the type.
# Currently only :type => :plain is supported. Pass additional parameters :username (String),
# and :password (either a String or a Proc that will be called at auth-time).
#
# @example
# :auth => {:type=>:plain, :username=>"mickey@disney.com", :password=>"mouse"}
#
# :from => required String
# Specifies the sender of the message. Will be passed as the argument
# to the MAIL FROM. Do NOT enclose the argument in angle-bracket (<>) characters.
# The connection will abort if the server rejects the value.
# :to => required String or Array of Strings
# The recipient(s) of the message. Do NOT enclose
# any of the values in angle-brackets (<>) characters. It's NOT a fatal error if one or more
# recipients are rejected by the server. (Of course, if ALL of them are, the server will most
# likely trigger an error when we try to send data.) An array of codes containing the status
# of each requested recipient is available after the call completes. TODO, we should define
# an overridable stub that will be called on rejection of a recipient or a sender, giving
# user code the chance to try again or abort the connection.
#
# One of either :message, :content, or :header and :body is required:
#
# :message => String
# A valid RFC2822 Internet Message.
# :content => String
# Raw data which MUST be in correct SMTP body format, with escaped leading dots and a trailing
# dot line.
# :header => String or Hash of values to be transmitted in the header of the message.
# The hash keys are the names of the headers (do NOT append a trailing colon), and the values
# are strings containing the header values. TODO, support Arrays of header values, which would
# cause us to send that specific header line more than once.
#
# @example
# :header => {"Subject" => "Bogus", "CC" => "myboss@example.com"}
#
# :body => Optional String or Array of Strings, defaults blank.
# This will be passed as the body of the email message.
# TODO, this needs to be significantly beefed up. As currently written, this requires the caller
# to properly format the input into CRLF-delimited lines of 7-bit characters in the standard
# SMTP transmission format. We need to be able to automatically convert binary data, and add
# correct line-breaks to text data.
#
# :verbose => Optional.
# If true, will cause a lot of information (including the server-side of the
# conversation) to be dumped to $>.
#
def self.send args={}
args[:port] ||= 25
args[:body] ||= ""
=begin
(I don't think it's possible for EM#connect to throw an exception under normal
circumstances, so this original code is stubbed out. A connect-failure will result
in the #unbind method being called without calling #connection_completed.)
begin
EventMachine.connect( args[:host], args[:port], self) {|c|
# According to the EM docs, we will get here AFTER post_init is called.
c.args = args
c.set_comm_inactivity_timeout 60
}
rescue
# We'll get here on a connect error. This code mimics the effect
# of a call to invoke_internal_error. Would be great to DRY this up.
# (Actually, it may be that we never get here, if EM#connect catches
# its errors internally.)
d = EM::DefaultDeferrable.new
d.set_deferred_status(:failed, {:error=>[:connect, 500, "unable to connect to server"]})
d
end
=end
EventMachine.connect( args[:host], args[:port], self) {|c|
# According to the EM docs, we will get here AFTER post_init is called.
c.args = args
c.set_comm_inactivity_timeout 60
}
end
attr_writer :args
# @private
def post_init
@return_values = OpenStruct.new
@return_values.start_time = Time.now
end
# @private
def connection_completed
@responder = :receive_signon
@msg = []
end
# We can get here in a variety of ways, all of them being failures unless
# the @succeeded flag is set. If a protocol success was recorded, then don't
# set a deferred success because the caller will already have done it
# (no need to wait until the connection closes to invoke the callbacks).
#
# @private
def unbind
unless @succeeded
@return_values.elapsed_time = Time.now - @return_values.start_time
@return_values.responder = @responder
@return_values.code = @code
@return_values.message = @msg
set_deferred_status(:failed, @return_values)
end
end
# @private
def receive_line ln
$>.puts ln if @args[:verbose]
@range = ln[0...1].to_i
@code = ln[0...3].to_i
@msg << ln[4..-1]
unless ln[3...4] == '-'
$>.puts @responder if @args[:verbose]
send @responder
@msg.clear
end
end
private
# We encountered an error from the server and will close the connection.
# Use the error and message the server returned.
#
def invoke_error
@return_values.elapsed_time = Time.now - @return_values.start_time
@return_values.responder = @responder
@return_values.code = @code
@return_values.message = @msg
set_deferred_status :failed, @return_values
send_data "QUIT\r\n"
close_connection_after_writing
end
# We encountered an error on our side of the protocol and will close the connection.
# Use an extra-protocol error code (900) and use the message from the caller.
#
def invoke_internal_error msg = "???"
@return_values.elapsed_time = Time.now - @return_values.start_time
@return_values.responder = @responder
@return_values.code = 900
@return_values.message = msg
set_deferred_status :failed, @return_values
send_data "QUIT\r\n"
close_connection_after_writing
end
def send_ehlo
send_data "EHLO #{@args[:domain]}\r\n"
end
def receive_signon
return invoke_error unless @range == 2
send_ehlo
@responder = :receive_ehlo_response
end
def receive_ehlo_response
return invoke_error unless @range == 2
@server_caps = @msg
invoke_starttls
end
def invoke_starttls
if @args[:starttls]
# It would be more sociable to first ask if @server_caps contains
# the string "STARTTLS" before we invoke it, but hey, life's too short.
send_data "STARTTLS\r\n"
@responder = :receive_starttls_response
else
invoke_auth
end
end
def receive_starttls_response
return invoke_error unless @range == 2
start_tls
invoke_ehlo_over_tls
end
def invoke_ehlo_over_tls
send_ehlo
@responder = :receive_ehlo_over_tls_response
end
def receive_ehlo_over_tls_response
return invoke_error unless @range == 2
invoke_auth
end
# Perform an authentication. If the caller didn't request one, then fall through
# to the mail-from state.
def invoke_auth
if @args[:auth]
if @args[:auth][:type] == :plain
psw = @args[:auth][:password]
if psw.respond_to?(:call)
psw = psw.call
end
#str = Base64::encode64("\0#{@args[:auth][:username]}\0#{psw}").chomp
str = ["\0#{@args[:auth][:username]}\0#{psw}"].pack("m").gsub(/\n/, '')
send_data "AUTH PLAIN #{str}\r\n"
@responder = :receive_auth_response
else
return invoke_internal_error("unsupported auth type")
end
else
invoke_mail_from
end
end
def receive_auth_response
return invoke_error unless @range == 2
invoke_mail_from
end
def invoke_mail_from
send_data "MAIL FROM: <#{@args[:from]}>\r\n"
@responder = :receive_mail_from_response
end
def receive_mail_from_response
return invoke_error unless @range == 2
invoke_rcpt_to
end
def invoke_rcpt_to
@rcpt_responses ||= []
l = @rcpt_responses.length
to = @args[:to].is_a?(Array) ? @args[:to] : [@args[:to].to_s]
if l < to.length
send_data "RCPT TO: <#{to[l]}>\r\n"
@responder = :receive_rcpt_to_response
else
e = @rcpt_responses.select {|rr| rr.last == 2}
if e and e.length > 0
invoke_data
else
invoke_error
end
end
end
def receive_rcpt_to_response
@rcpt_responses << [@code, @msg, @range]
invoke_rcpt_to
end
def escape_leading_dots(s)
s.gsub(/^\./, '..')
end
def invoke_data
send_data "DATA\r\n"
@responder = :receive_data_response
end
def receive_data_response
return invoke_error unless @range == 3
# The data to send can be given in either @args[:message], @args[:content], or the
# combination of @args[:header] and @args[:body].
#
# - @args[:message] (String) MUST be a valid RFC2822 Internet Message
#
# - @args[:content] (String) MUST be in correct SMTP body format, with escaped
# leading dots and a trailing dot line
#
# - @args[:header] (Hash or String)
# - @args[:body] (Array or String)
if @args[:message]
send_data escape_leading_dots(@args[:message].to_s)
send_data "\r\n.\r\n"
elsif @args[:content]
send_data @args[:content].to_s
else
# The header can be a hash or an array.
if @args[:header].is_a?(Hash)
(@args[:header] || {}).each {|k,v| send_data escape_leading_dots("#{k}: #{v}\r\n") }
else
send_data escape_leading_dots(@args[:header].to_s)
end
send_data "\r\n"
if @args[:body].is_a?(Array)
@args[:body].each {|e| send_data escape_leading_dots(e)}
else
send_data escape_leading_dots(@args[:body].to_s)
end
send_data "\r\n.\r\n"
end
@responder = :receive_message_response
end
def receive_message_response
return invoke_error unless @range == 2
send_data "QUIT\r\n"
close_connection_after_writing
@succeeded = true
@return_values.elapsed_time = Time.now - @return_values.start_time
@return_values.responder = @responder
@return_values.code = @code
@return_values.message = @msg
set_deferred_status :succeeded, @return_values
end
end
end
end
@@ -0,0 +1,666 @@
#--
#
# Author:: Francis Cianfrocca (gmail: blackhedd)
# Homepage:: http://rubyeventmachine.com
# Date:: 16 July 2006
#
# See EventMachine and EventMachine::Connection for documentation and
# usage examples.
#
#----------------------------------------------------------------------------
#
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
# Gmail: blackhedd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of either: 1) 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; or 2) Ruby's License.
#
# See the file COPYING for complete licensing information.
#
#---------------------------------------------------------------------------
#
#
module EventMachine
module Protocols
# This is a protocol handler for the server side of SMTP.
# It's NOT a complete SMTP server obeying all the semantics of servers conforming to
# RFC2821. Rather, it uses overridable method stubs to communicate protocol states
# and data to user code. User code is responsible for doing the right things with the
# data in order to get complete and correct SMTP server behavior.
#
# Simple SMTP server example:
#
# class EmailServer < EM::P::SmtpServer
# def receive_plain_auth(user, pass)
# true
# end
#
# def get_server_domain
# "mock.smtp.server.local"
# end
#
# def get_server_greeting
# "mock smtp server greets you with impunity"
# end
#
# def receive_sender(sender)
# current.sender = sender
# true
# end
#
# def receive_recipient(recipient)
# current.recipient = recipient
# true
# end
#
# def receive_message
# current.received = true
# current.completed_at = Time.now
#
# p [:received_email, current]
# @current = OpenStruct.new
# true
# end
#
# def receive_ehlo_domain(domain)
# @ehlo_domain = domain
# true
# end
#
# def receive_data_command
# current.data = ""
# true
# end
#
# def receive_data_chunk(data)
# current.data << data.join("\n")
# true
# end
#
# def receive_transaction
# if @ehlo_domain
# current.ehlo_domain = @ehlo_domain
# @ehlo_domain = nil
# end
# true
# end
#
# def current
# @current ||= OpenStruct.new
# end
#
# def self.start(host = 'localhost', port = 1025)
# require 'ostruct'
# @server = EM.start_server host, port, self
# end
#
# def self.stop
# if @server
# EM.stop_server @server
# @server = nil
# end
# end
#
# def self.running?
# !!@server
# end
# end
#
# EM.run{ EmailServer.start }
#
#--
# Useful paragraphs in RFC-2821:
# 4.3.2: Concise list of command-reply sequences, in essence a text representation
# of the command state-machine.
#
# STARTTLS is defined in RFC2487.
# Observe that there are important rules governing whether a publicly-referenced server
# (meaning one whose Internet address appears in public MX records) may require the
# non-optional use of TLS.
# Non-optional TLS does not apply to EHLO, NOOP, QUIT or STARTTLS.
class SmtpServer < EventMachine::Connection
include Protocols::LineText2
HeloRegex = /\AHELO\s*/i
EhloRegex = /\AEHLO\s*/i
QuitRegex = /\AQUIT/i
MailFromRegex = /\AMAIL FROM:\s*/i
RcptToRegex = /\ARCPT TO:\s*/i
DataRegex = /\ADATA/i
NoopRegex = /\ANOOP/i
RsetRegex = /\ARSET/i
VrfyRegex = /\AVRFY\s+/i
ExpnRegex = /\AEXPN\s+/i
HelpRegex = /\AHELP/i
StarttlsRegex = /\ASTARTTLS/i
AuthRegex = /\AAUTH\s+/i
# Class variable containing default parameters that can be overridden
# in application code.
# Individual objects of this class will make an instance-local copy of
# the class variable, so that they can be reconfigured on a per-instance
# basis.
#
# Chunksize is the number of data lines we'll buffer before
# sending them to the application. TODO, make this user-configurable.
#
@@parms = {
:chunksize => 4000,
:verbose => false
}
def self.parms= parms={}
@@parms.merge!(parms)
end
def initialize *args
super
@parms = @@parms
init_protocol_state
end
def parms= parms={}
@parms.merge!(parms)
end
# In SMTP, the server talks first. But by a (perhaps flawed) axiom in EM,
# #post_init will execute BEFORE the block passed to #start_server, for any
# given accepted connection. Since in this class we'll probably be getting
# a lot of initialization parameters, we want the guts of post_init to
# run AFTER the application has initialized the connection object. So we
# use a spawn to schedule the post_init to run later.
# It's a little weird, I admit. A reasonable alternative would be to set
# parameters as a class variable and to do that before accepting any connections.
#
# OBSOLETE, now we have @@parms. But the spawn is nice to keep as an illustration.
#
def post_init
#send_data "220 #{get_server_greeting}\r\n" (ORIGINAL)
#(EM.spawn {|x| x.send_data "220 #{x.get_server_greeting}\r\n"}).notify(self)
(EM.spawn {|x| x.send_server_greeting}).notify(self)
end
def send_server_greeting
send_data "220 #{get_server_greeting}\r\n"
end
def receive_line ln
@@parms[:verbose] and $>.puts ">>> #{ln}"
return process_data_line(ln) if @state.include?(:data)
return process_auth_line(ln) if @state.include?(:auth_incomplete)
case ln
when EhloRegex
process_ehlo $'.dup
when HeloRegex
process_helo $'.dup
when MailFromRegex
process_mail_from $'.dup
when RcptToRegex
process_rcpt_to $'.dup
when DataRegex
process_data
when RsetRegex
process_rset
when VrfyRegex
process_vrfy
when ExpnRegex
process_expn
when HelpRegex
process_help
when NoopRegex
process_noop
when QuitRegex
process_quit
when StarttlsRegex
process_starttls
when AuthRegex
process_auth $'.dup
else
process_unknown
end
end
# TODO - implement this properly, the implementation is a stub!
def process_help
send_data "250 Ok, but unimplemented\r\n"
end
# RFC2821, 3.5.3 Meaning of VRFY or EXPN Success Response:
# A server MUST NOT return a 250 code in response to a VRFY or EXPN
# command unless it has actually verified the address. In particular,
# a server MUST NOT return 250 if all it has done is to verify that the
# syntax given is valid. In that case, 502 (Command not implemented)
# or 500 (Syntax error, command unrecognized) SHOULD be returned.
#
# TODO - implement this properly, the implementation is a stub!
def process_vrfy
send_data "502 Command not implemented\r\n"
end
# TODO - implement this properly, the implementation is a stub!
def process_expn
send_data "502 Command not implemented\r\n"
end
#--
# This is called at several points to restore the protocol state
# to a pre-transaction state. In essence, we "forget" having seen
# any valid command except EHLO and STARTTLS.
# We also have to callback user code, in case they're keeping track
# of senders, recipients, and whatnot.
#
# We try to follow the convention of avoiding the verb "receive" for
# internal method names except receive_line (which we inherit), and
# using only receive_xxx for user-overridable stubs.
#
# init_protocol_state is called when we initialize the connection as
# well as during reset_protocol_state. It does NOT call the user
# override method. This enables us to promise the users that they
# won't see the overridable fire except after EHLO and RSET, and
# after a message has been received. Although the latter may be wrong.
# The standard may allow multiple DATA segments with the same set of
# senders and recipients.
#
def reset_protocol_state
init_protocol_state
s,@state = @state,[]
@state << :starttls if s.include?(:starttls)
@state << :ehlo if s.include?(:ehlo)
receive_transaction
end
def init_protocol_state
@state ||= []
end
#--
# EHLO/HELO is always legal, per the standard. On success
# it always clears buffers and initiates a mail "transaction."
# Which means that a MAIL FROM must follow.
#
# Per the standard, an EHLO/HELO or a RSET "initiates" an email
# transaction. Thereafter, MAIL FROM must be received before
# RCPT TO, before DATA. Not sure what this specific ordering
# achieves semantically, but it does make it easier to
# implement. We also support user-specified requirements for
# STARTTLS and AUTH. We make it impossible to proceed to MAIL FROM
# without fulfilling tls and/or auth, if the user specified either
# or both as required. We need to check the extension standard
# for auth to see if a credential is discarded after a RSET along
# with all the rest of the state. We'll behave as if it is.
# Now clearly, we can't discard tls after its been negotiated
# without dropping the connection, so that flag doesn't get cleared.
#
def process_ehlo domain
if receive_ehlo_domain domain
send_data "250-#{get_server_domain}\r\n"
if @@parms[:starttls]
send_data "250-STARTTLS\r\n"
end
if @@parms[:auth]
send_data "250-AUTH PLAIN\r\n"
end
send_data "250-NO-SOLICITING\r\n"
# TODO, size needs to be configurable.
send_data "250 SIZE 20000000\r\n"
reset_protocol_state
@state << :ehlo
else
send_data "550 Requested action not taken\r\n"
end
end
def process_helo domain
if receive_ehlo_domain domain.dup
send_data "250 #{get_server_domain}\r\n"
reset_protocol_state
@state << :ehlo
else
send_data "550 Requested action not taken\r\n"
end
end
def process_quit
send_data "221 Ok\r\n"
close_connection_after_writing
end
def process_noop
send_data "250 Ok\r\n"
end
def process_unknown
send_data "500 Unknown command\r\n"
end
#--
# So far, only AUTH PLAIN is supported but we should do at least LOGIN as well.
# TODO, support clients that send AUTH PLAIN with no parameter, expecting a 3xx
# response and a continuation of the auth conversation.
#
def process_auth str
if @state.include?(:auth)
send_data "503 auth already issued\r\n"
elsif str =~ /\APLAIN\s?/i
if $'.length == 0
# we got a partial response, so let the client know to send the rest
@state << :auth_incomplete
send_data("334 \r\n")
else
# we got the initial response, so go ahead & process it
process_auth_line($')
end
#elsif str =~ /\ALOGIN\s+/i
else
send_data "504 auth mechanism not available\r\n"
end
end
def process_auth_line(line)
plain = line.unpack("m").first
_,user,psw = plain.split("\000")
succeeded = proc {
send_data "235 authentication ok\r\n"
@state << :auth
}
failed = proc {
send_data "535 invalid authentication\r\n"
}
auth = receive_plain_auth user,psw
if auth.respond_to?(:callback)
auth.callback(&succeeded)
auth.errback(&failed)
else
(auth ? succeeded : failed).call
end
@state.delete :auth_incomplete
end
#--
# Unusually, we can deal with a Deferrable returned from the user application.
# This was added to deal with a special case in a particular application, but
# it would be a nice idea to add it to the other user-code callbacks.
#
def process_data
unless @state.include?(:rcpt)
send_data "503 Operation sequence error\r\n"
else
succeeded = proc {
send_data "354 Send it\r\n"
@state << :data
@databuffer = []
}
failed = proc {
send_data "550 Operation failed\r\n"
}
d = receive_data_command
if d.respond_to?(:callback)
d.callback(&succeeded)
d.errback(&failed)
else
(d ? succeeded : failed).call
end
end
end
def process_rset
reset_protocol_state
receive_reset
send_data "250 Ok\r\n"
end
def unbind
connection_ended
end
#--
# STARTTLS may not be issued before EHLO, or unless the user has chosen
# to support it.
#
# If :starttls_options is present and :starttls is set in the parms
# pass the options in :starttls_options to start_tls. Do this if you want to use
# your own certificate
# e.g. {:cert_chain_file => "/etc/ssl/cert.pem", :private_key_file => "/etc/ssl/private/cert.key"}
def process_starttls
if @@parms[:starttls]
if @state.include?(:starttls)
send_data "503 TLS Already negotiated\r\n"
elsif ! @state.include?(:ehlo)
send_data "503 EHLO required before STARTTLS\r\n"
else
send_data "220 Start TLS negotiation\r\n"
start_tls(@@parms[:starttls_options] || {})
@state << :starttls
end
else
process_unknown
end
end
#--
# Requiring TLS is touchy, cf RFC2784.
# Requiring AUTH seems to be much more reasonable.
# We don't currently support any notion of deriving an authentication from the TLS
# negotiation, although that would certainly be reasonable.
# We DON'T allow MAIL FROM to be given twice.
# We DON'T enforce all the various rules for validating the sender or
# the reverse-path (like whether it should be null), and notifying the reverse
# path in case of delivery problems. All of that is left to the calling application.
#
def process_mail_from sender
if (@@parms[:starttls]==:required and !@state.include?(:starttls))
send_data "550 This server requires STARTTLS before MAIL FROM\r\n"
elsif (@@parms[:auth]==:required and !@state.include?(:auth))
send_data "550 This server requires authentication before MAIL FROM\r\n"
elsif @state.include?(:mail_from)
send_data "503 MAIL already given\r\n"
else
unless receive_sender sender
send_data "550 sender is unacceptable\r\n"
else
send_data "250 Ok\r\n"
@state << :mail_from
end
end
end
#--
# Since we require :mail_from to have been seen before we process RCPT TO,
# we don't need to repeat the tests for TLS and AUTH.
# Note that we don't remember or do anything else with the recipients.
# All of that is on the user code.
# TODO: we should enforce user-definable limits on the total number of
# recipients per transaction.
# We might want to make sure that a given recipient is only seen once, but
# for now we'll let that be the user's problem.
#
# User-written code can return a deferrable from receive_recipient.
#
def process_rcpt_to rcpt
unless @state.include?(:mail_from)
send_data "503 MAIL is required before RCPT\r\n"
else
succeeded = proc {
send_data "250 Ok\r\n"
@state << :rcpt unless @state.include?(:rcpt)
}
failed = proc {
send_data "550 recipient is unacceptable\r\n"
}
d = receive_recipient rcpt
if d.respond_to?(:set_deferred_status)
d.callback(&succeeded)
d.errback(&failed)
else
(d ? succeeded : failed).call
end
=begin
unless receive_recipient rcpt
send_data "550 recipient is unacceptable\r\n"
else
send_data "250 Ok\r\n"
@state << :rcpt unless @state.include?(:rcpt)
end
=end
end
end
# Send the incoming data to the application one chunk at a time, rather than
# one line at a time. That lets the application be a little more flexible about
# storing to disk, etc.
# Since we clear the chunk array every time we submit it, the caller needs to be
# aware to do things like dup it if he wants to keep it around across calls.
#
# Resets the transaction upon disposition of the incoming message.
# RFC5321 says this about the MAIL FROM command:
# "This command tells the SMTP-receiver that a new mail transaction is
# starting and to reset all its state tables and buffers, including any
# recipients or mail data."
#
# Equivalent behaviour is implemented by resetting after a completed transaction.
#
# User-written code can return a Deferrable as a response from receive_message.
#
def process_data_line ln
if ln == "."
if @databuffer.length > 0
receive_data_chunk @databuffer
@databuffer.clear
end
succeeded = proc {
send_data "250 Message accepted\r\n"
reset_protocol_state
}
failed = proc {
send_data "550 Message rejected\r\n"
reset_protocol_state
}
d = receive_message
if d.respond_to?(:set_deferred_status)
d.callback(&succeeded)
d.errback(&failed)
else
(d ? succeeded : failed).call
end
@state.delete :data
else
# slice off leading . if any
ln.slice!(0...1) if ln[0] == ?.
@databuffer << ln
if @databuffer.length > @@parms[:chunksize]
receive_data_chunk @databuffer
@databuffer.clear
end
end
end
#------------------------------------------
# Everything from here on can be overridden in user code.
# The greeting returned in the initial connection message to the client.
def get_server_greeting
"EventMachine SMTP Server"
end
# The domain name returned in the first line of the response to a
# successful EHLO or HELO command.
def get_server_domain
"Ok EventMachine SMTP Server"
end
# A false response from this user-overridable method will cause a
# 550 error to be returned to the remote client.
#
def receive_ehlo_domain domain
true
end
# Return true or false to indicate that the authentication is acceptable.
def receive_plain_auth user, password
true
end
# Receives the argument of the MAIL FROM command. Return false to
# indicate to the remote client that the sender is not accepted.
# This can only be successfully called once per transaction.
#
def receive_sender sender
true
end
# Receives the argument of a RCPT TO command. Can be given multiple
# times per transaction. Return false to reject the recipient.
#
def receive_recipient rcpt
true
end
# Sent when the remote peer issues the RSET command.
# Since RSET is not allowed to fail (according to the protocol),
# we ignore any return value from user overrides of this method.
#
def receive_reset
end
# Sent when the remote peer has ended the connection.
#
def connection_ended
end
# Called when the remote peer sends the DATA command.
# Returning false will cause us to send a 550 error to the peer.
# This can be useful for dealing with problems that arise from processing
# the whole set of sender and recipients.
#
def receive_data_command
true
end
# Sent when data from the remote peer is available. The size can be controlled
# by setting the :chunksize parameter. This call can be made multiple times.
# The goal is to strike a balance between sending the data to the application one
# line at a time, and holding all of a very large message in memory.
#
def receive_data_chunk data
@smtps_msg_size ||= 0
@smtps_msg_size += data.join.length
STDERR.write "<#{@smtps_msg_size}>"
end
# Sent after a message has been completely received. User code
# must return true or false to indicate whether the message has
# been accepted for delivery.
def receive_message
@@parms[:verbose] and $>.puts "Received complete message"
true
end
# This is called when the protocol state is reset. It happens
# when the remote client calls EHLO/HELO or RSET.
def receive_transaction
end
end
end
end
@@ -0,0 +1,66 @@
module EventMachine
module Protocols
# Basic SOCKS v4 client implementation
#
# Use as you would any regular connection:
#
# class MyConn < EM::P::Socks4
# def post_init
# send_data("sup")
# end
#
# def receive_data(data)
# send_data("you said: #{data}")
# end
# end
#
# EM.connect socks_host, socks_port, MyConn, host, port
#
class Socks4 < Connection
def initialize(host, port)
@host = Socket.gethostbyname(host).last
@port = port
@socks_error_code = nil
@buffer = ''
setup_methods
end
def setup_methods
class << self
def post_init; socks_post_init; end
def receive_data(*a); socks_receive_data(*a); end
end
end
def restore_methods
class << self
remove_method :post_init
remove_method :receive_data
end
end
def socks_post_init
header = [4, 1, @port, @host, 0].flatten.pack("CCnA4C")
send_data(header)
end
def socks_receive_data(data)
@buffer << data
return if @buffer.size < 8
header_resp = @buffer.slice! 0, 8
_, r = header_resp.unpack("cc")
if r != 90
@socks_error_code = r
close_connection
return
end
restore_methods
post_init
receive_data(@buffer) unless @buffer.empty?
end
end
end
end
@@ -0,0 +1,205 @@
#--
#
# Author:: Francis Cianfrocca (gmail: blackhedd)
# Homepage:: http://rubyeventmachine.com
# Date:: 15 November 2006
#
# See EventMachine and EventMachine::Connection for documentation and
# usage examples.
#
#----------------------------------------------------------------------------
#
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
# Gmail: blackhedd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of either: 1) 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; or 2) Ruby's License.
#
# See the file COPYING for complete licensing information.
#
#---------------------------------------------------------------------------
#
#
#
module EventMachine
module Protocols
# Implements Stomp (http://docs.codehaus.org/display/STOMP/Protocol).
#
# == Usage example
#
# module StompClient
# include EM::Protocols::Stomp
#
# def connection_completed
# connect :login => 'guest', :passcode => 'guest'
# end
#
# def receive_msg msg
# if msg.command == "CONNECTED"
# subscribe '/some/topic'
# else
# p ['got a message', msg]
# puts msg.body
# end
# end
# end
#
# EM.run{
# EM.connect 'localhost', 61613, StompClient
# }
#
module Stomp
include LineText2
class Message
# The command associated with the message, usually 'CONNECTED' or 'MESSAGE'
attr_accessor :command
# Hash containing headers such as destination and message-id
attr_accessor :header
alias :headers :header
# Body of the message
attr_accessor :body
# @private
def initialize
@header = {}
@state = :precommand
@content_length = nil
end
# @private
def consume_line line
if @state == :precommand
unless line =~ /\A\s*\Z/
@command = line
@state = :headers
end
elsif @state == :headers
if line == ""
if @content_length
yield( [:sized_text, @content_length+1] )
else
@state = :body
yield( [:unsized_text] )
end
elsif line =~ /\A([^:]+):(.+)\Z/
k = $1.dup.strip
v = $2.dup.strip
@header[k] = v
if k == "content-length"
@content_length = v.to_i
end
else
# This is a protocol error. How to signal it?
end
elsif @state == :body
@body = line
yield( [:dispatch] )
end
end
end
# @private
def send_frame verb, headers={}, body=""
body = body.to_s
ary = [verb, "\n"]
body_bytesize = body.bytesize if body.respond_to? :bytesize
body_bytesize ||= body.size
headers.each {|k,v| ary << "#{k}:#{v}\n" }
ary << "content-length: #{body_bytesize}\n"
ary << "content-type: text/plain; charset=UTF-8\n" unless headers.has_key? 'content-type'
ary << "\n"
ary << body
ary << "\0"
send_data ary.join
end
# @private
def receive_line line
@stomp_initialized || init_message_reader
@stomp_message.consume_line(line) {|outcome|
if outcome.first == :sized_text
set_text_mode outcome[1]
elsif outcome.first == :unsized_text
set_delimiter "\0"
elsif outcome.first == :dispatch
receive_msg(@stomp_message) if respond_to?(:receive_msg)
init_message_reader
end
}
end
# @private
def receive_binary_data data
@stomp_message.body = data[0..-2]
receive_msg(@stomp_message) if respond_to?(:receive_msg)
init_message_reader
end
# @private
def init_message_reader
@stomp_initialized = true
set_delimiter "\n"
set_line_mode
@stomp_message = Message.new
end
# Invoked with an incoming Stomp::Message received from the STOMP server
def receive_msg msg
# stub, overwrite this in your handler
end
# CONNECT command, for authentication
#
# connect :login => 'guest', :passcode => 'guest'
#
def connect parms={}
send_frame "CONNECT", parms
end
# SEND command, for publishing messages to a topic
#
# send '/topic/name', 'some message here'
#
def send destination, body, parms={}
send_frame "SEND", parms.merge( :destination=>destination ), body.to_s
end
# SUBSCRIBE command, for subscribing to topics
#
# subscribe '/topic/name', false
#
def subscribe dest, ack=false
send_frame "SUBSCRIBE", {:destination=>dest, :ack=>(ack ? "client" : "auto")}
end
# ACK command, for acknowledging receipt of messages
#
# module StompClient
# include EM::P::Stomp
#
# def connection_completed
# connect :login => 'guest', :passcode => 'guest'
# # subscribe with ack mode
# subscribe '/some/topic', true
# end
#
# def receive_msg msg
# if msg.command == "MESSAGE"
# ack msg.headers['message-id']
# puts msg.body
# end
# end
# end
#
def ack msgid
send_frame "ACK", 'message-id'=> msgid
end
end
end
end
@@ -0,0 +1,54 @@
#--
#
# Author:: Francis Cianfrocca (gmail: blackhedd)
# Homepage:: http://rubyeventmachine.com
# Date:: 16 July 2006
#
# See EventMachine and EventMachine::Connection for documentation and
# usage examples.
#
#----------------------------------------------------------------------------
#
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
# Gmail: blackhedd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of either: 1) 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; or 2) Ruby's License.
#
# See the file COPYING for complete licensing information.
#
#---------------------------------------------------------------------------
#
#
#
module EventMachine
module Protocols
# @private
class TcpConnectTester < Connection
include EventMachine::Deferrable
def self.test( host, port )
EventMachine.connect( host, port, self )
end
def post_init
@start_time = Time.now
end
def connection_completed
@completed = true
set_deferred_status :succeeded, (Time.now - @start_time)
close_connection
end
def unbind
set_deferred_status :failed, (Time.now - @start_time) unless @completed
end
end
end
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,80 @@
module EventMachine
# A cross thread, reactor scheduled, linear queue.
#
# This class provides a simple queue abstraction on top of the reactor
# scheduler. It services two primary purposes:
#
# * API sugar for stateful protocols
# * Pushing processing onto the reactor thread
#
# @example
#
# q = EM::Queue.new
# q.push('one', 'two', 'three')
# 3.times do
# q.pop { |msg| puts(msg) }
# end
#
class Queue
def initialize
@sink = []
@drain = []
@popq = []
end
# Pop items off the queue, running the block on the reactor thread. The pop
# will not happen immediately, but at some point in the future, either in
# the next tick, if the queue has data, or when the queue is populated.
#
# @return [NilClass] nil
def pop(*a, &b)
cb = EM::Callback(*a, &b)
EM.schedule do
if @drain.empty?
@drain = @sink
@sink = []
end
if @drain.empty?
@popq << cb
else
cb.call @drain.shift
end
end
nil # Always returns nil
end
# Push items onto the queue in the reactor thread. The items will not appear
# in the queue immediately, but will be scheduled for addition during the
# next reactor tick.
def push(*items)
EM.schedule do
@sink.push(*items)
unless @popq.empty?
@drain = @sink
@sink = []
@popq.shift.call @drain.shift until @drain.empty? || @popq.empty?
end
end
end
alias :<< :push
# @return [Boolean]
# @note This is a peek, it's not thread safe, and may only tend toward accuracy.
def empty?
@drain.empty? && @sink.empty?
end
# @return [Integer] Queue size
# @note This is a peek, it's not thread safe, and may only tend toward accuracy.
def size
@drain.size + @sink.size
end
# @return [Integer] Waiting size
# @note This is a peek at the number of jobs that are currently waiting on the Queue
def num_waiting
@popq.size
end
end # Queue
end # EventMachine
@@ -0,0 +1,232 @@
module EventMachine
module DNS
class Resolver
def self.windows?
if RUBY_PLATFORM =~ /mswin32|cygwin|mingw|bccwin/
require 'win32/resolv'
true
else
false
end
end
HOSTS_FILE = windows? ? Win32::Resolv.get_hosts_path : '/etc/hosts'
@hosts = nil
@nameservers = nil
@socket = nil
def self.resolve(hostname)
Request.new(socket, hostname)
end
def self.socket
if @socket && @socket.error?
@socket = Socket.open
else
@socket ||= Socket.open
end
end
def self.nameservers=(ns)
@nameservers = ns
end
def self.nameservers
return @nameservers if @nameservers
if windows?
_, ns = Win32::Resolv.get_resolv_info
return @nameservers = ns || []
end
@nameservers = []
IO.readlines('/etc/resolv.conf').each do |line|
if line =~ /^nameserver (.+)$/
@nameservers << $1.split(/\s+/).first
end
end
@nameservers
rescue
@nameservers = []
end
def self.nameserver
nameservers.shuffle.first
end
def self.hosts
return @hosts if @hosts
@hosts = {}
IO.readlines(HOSTS_FILE).each do |line|
next if line =~ /^#/
addr, host = line.split(/\s+/)
next unless addr && host
@hosts[host] ||= []
@hosts[host] << addr
end
@hosts
rescue
@hosts = {}
end
end
class RequestIdAlreadyUsed < RuntimeError; end
class Socket < EventMachine::Connection
def self.open
EventMachine::open_datagram_socket('0.0.0.0', 0, self)
end
def initialize
@nameserver = nil
end
def post_init
@requests = {}
end
def start_timer
@timer ||= EM.add_periodic_timer(0.1, &method(:tick))
end
def stop_timer
EM.cancel_timer(@timer)
@timer = nil
end
def unbind
end
def tick
@requests.each do |id,req|
req.tick
end
end
def register_request(id, req)
if @requests.has_key?(id)
raise RequestIdAlreadyUsed
else
@requests[id] = req
end
start_timer
end
def deregister_request(id, req)
@requests.delete(id)
stop_timer if @requests.length == 0
end
def send_packet(pkt)
send_datagram(pkt, nameserver, 53)
end
def nameserver=(ns)
@nameserver = ns
end
def nameserver
@nameserver || Resolver.nameserver
end
# Decodes the packet, looks for the request and passes the
# response over to the requester
def receive_data(data)
msg = nil
begin
msg = Resolv::DNS::Message.decode data
rescue
else
req = @requests[msg.id]
if req
@requests.delete(msg.id)
stop_timer if @requests.length == 0
req.receive_answer(msg)
end
end
end
end
class Request
include Deferrable
attr_accessor :retry_interval, :max_tries
def initialize(socket, hostname)
@socket = socket
@hostname = hostname
@tries = 0
@last_send = Time.at(0)
@retry_interval = 3
@max_tries = 5
if addrs = Resolver.hosts[hostname]
succeed addrs
else
EM.next_tick { tick }
end
end
def tick
# Break early if nothing to do
return if @last_send + @retry_interval > Time.now
if @tries < @max_tries
send
else
@socket.deregister_request(@id, self)
fail 'retries exceeded'
end
end
def receive_answer(msg)
addrs = []
msg.each_answer do |name,ttl,data|
if data.kind_of?(Resolv::DNS::Resource::IN::A) ||
data.kind_of?(Resolv::DNS::Resource::IN::AAAA)
addrs << data.address.to_s
end
end
if addrs.empty?
fail "rcode=#{msg.rcode}"
else
succeed addrs
end
end
private
def send
@tries += 1
@last_send = Time.now
@socket.send_packet(packet.encode)
end
def id
begin
@id = rand(65535)
@socket.register_request(@id, self)
rescue RequestIdAlreadyUsed
retry
end unless defined?(@id)
@id
end
def packet
msg = Resolv::DNS::Message.new
msg.id = id
msg.rd = 1
msg.add_question @hostname, Resolv::DNS::Resource::IN::A
msg
end
end
end
end
@@ -0,0 +1,84 @@
#--
#
# Author:: Francis Cianfrocca (gmail: blackhedd)
# Homepage:: http://rubyeventmachine.com
# Date:: 25 Aug 2007
#
# See EventMachine and EventMachine::Connection for documentation and
# usage examples.
#
#----------------------------------------------------------------------------
#
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
# Gmail: blackhedd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of either: 1) 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; or 2) Ruby's License.
#
# See the file COPYING for complete licensing information.
#
#---------------------------------------------------------------------------
#
#
module EventMachine
# Support for Erlang-style processes.
#
class SpawnedProcess
# Send a message to the spawned process
def notify *x
me = self
EM.next_tick {
# A notification executes in the context of this
# SpawnedProcess object. That makes self and notify
# work as one would expect.
#
y = me.call(*x)
if y and y.respond_to?(:pull_out_yield_block)
a,b = y.pull_out_yield_block
set_receiver a
self.notify if b
end
}
end
alias_method :resume, :notify
alias_method :run, :notify # for formulations like (EM.spawn {xxx}).run
def set_receiver blk
(class << self ; self ; end).class_eval do
remove_method :call if method_defined? :call
define_method :call, blk
end
end
end
# @private
class YieldBlockFromSpawnedProcess
def initialize block, notify
@block = [block,notify]
end
def pull_out_yield_block
@block
end
end
# Spawn an erlang-style process
def self.spawn &block
s = SpawnedProcess.new
s.set_receiver block
s
end
# @private
def self.yield &block
return YieldBlockFromSpawnedProcess.new( block, false )
end
# @private
def self.yield_and_notify &block
return YieldBlockFromSpawnedProcess.new( block, true )
end
end
@@ -0,0 +1,118 @@
module EventMachine
# Streams a file over a given connection. Streaming begins once the object is
# instantiated. Typically FileStreamer instances are not reused.
#
# Streaming uses buffering for files larger than 16K and uses so-called fast file reader (a C++ extension)
# if available (it is part of eventmachine gem itself).
#
# @example
#
# module FileSender
# def post_init
# streamer = EventMachine::FileStreamer.new(self, '/tmp/bigfile.tar')
# streamer.callback{
# # file was sent successfully
# close_connection_after_writing
# }
# end
# end
#
#
# @author Francis Cianfrocca
class FileStreamer
include Deferrable
# Use mapped streamer for files bigger than 16k
MappingThreshold = 16384
# Wait until next tick to send more data when 50k is still in the outgoing buffer
BackpressureLevel = 50000
# Send 16k chunks at a time
ChunkSize = 16384
# @param [EventMachine::Connection] connection
# @param [String] filename File path
#
# @option args [Boolean] :http_chunks (false) Use HTTP 1.1 style chunked-encoding semantics.
def initialize connection, filename, args = {}
@connection = connection
@http_chunks = args[:http_chunks]
if File.exist?(filename)
@size = File.size(filename)
if @size <= MappingThreshold
stream_without_mapping filename
else
stream_with_mapping filename
end
else
fail "file not found"
end
end
# @private
def stream_without_mapping filename
if @http_chunks
@connection.send_data "#{@size.to_s(16)}\r\n"
@connection.send_file_data filename
@connection.send_data "\r\n0\r\n\r\n"
else
@connection.send_file_data filename
end
succeed
end
private :stream_without_mapping
# @private
def stream_with_mapping filename
ensure_mapping_extension_is_present
@position = 0
@mapping = EventMachine::FastFileReader::Mapper.new filename
stream_one_chunk
end
private :stream_with_mapping
# Used internally to stream one chunk at a time over multiple reactor ticks
# @private
def stream_one_chunk
loop {
if @position < @size
if @connection.get_outbound_data_size > BackpressureLevel
EventMachine::next_tick {stream_one_chunk}
break
else
len = @size - @position
len = ChunkSize if (len > ChunkSize)
@connection.send_data( "#{len.to_s(16)}\r\n" ) if @http_chunks
@connection.send_data( @mapping.get_chunk( @position, len ))
@connection.send_data("\r\n") if @http_chunks
@position += len
end
else
@connection.send_data "0\r\n\r\n" if @http_chunks
@mapping.close
succeed
break
end
}
end
#
# We use an outboard extension class to get memory-mapped files.
# It's outboard to avoid polluting the core distro, but that means
# there's a "hidden" dependency on it. The first time we get here in
# any run, try to load up the dependency extension. User code will see
# a LoadError if it's not available, but code that doesn't require
# mapped files will work fine without it. This is a somewhat difficult
# compromise between usability and proper modularization.
#
# @private
def ensure_mapping_extension_is_present
@@fastfilereader ||= (require 'fastfilereaderext')
end
private :ensure_mapping_extension_is_present
end # FileStreamer
end # EventMachine
@@ -0,0 +1,90 @@
module EventMachine
# = EventMachine::ThreadedResource
#
# A threaded resource is a "quick and dirty" wrapper around the concept of
# wiring up synchronous code into a standard EM::Pool. This is useful to keep
# interfaces coherent and provide a simple approach at "making an interface
# async-ish".
#
# General usage is to wrap libraries that do not support EventMachine, or to
# have a specific number of dedicated high-cpu worker resources.
#
# == Basic Usage example
#
# This example requires the cassandra gem. The cassandra gem contains an
# EventMachine interface, but it's sadly Fiber based and thus only works on
# 1.9. It also requires (potentially) complex stack switching logic to reach
# completion of nested operations. By contrast this approach provides a block
# in which normal synchronous code can occur, but makes no attempt to wire the
# IO into EventMachines C++ IO implementations, instead relying on the reactor
# pattern in rb_thread_select.
#
# cassandra_dispatcher = ThreadedResource.new do
# Cassandra.new('allthethings', '127.0.0.1:9160')
# end
#
# pool = EM::Pool.new
#
# pool.add cassandra_dispatcher
#
# # If we don't care about the result:
# pool.perform do |dispatcher|
# # The following block executes inside a dedicated thread, and should not
# # access EventMachine things:
# dispatcher.dispatch do |cassandra|
# cassandra.insert(:Things, '10', 'stuff' => 'things')
# end
# end
#
# # Example where we care about the result:
# pool.perform do |dispatcher|
# # The dispatch block is executed in the resources thread.
# completion = dispatcher.dispatch do |cassandra|
# cassandra.get(:Things, '10', 'stuff')
# end
#
# # This block will be yielded on the EM thread:
# completion.callback do |result|
# EM.do_something_with(result)
# end
#
# completion
# end
class ThreadedResource
# The block should return the resource that will be yielded in a dispatch.
def initialize
@resource = yield
@running = true
@queue = ::Queue.new
@thread = Thread.new do
@queue.pop.call while @running
end
end
# Called on the EM thread, generally in a perform block to return a
# completion for the work.
def dispatch
completion = EM::Completion.new
@queue << lambda do
begin
result = yield @resource
completion.succeed result
rescue => e
completion.fail e
end
end
completion
end
# Kill the internal thread. should only be used to cleanup - generally
# only required for tests.
def shutdown
@running = false
@queue << lambda {}
@thread.join
end
end
end
@@ -0,0 +1,85 @@
module EventMachine
# Creates and immediately starts an EventMachine::TickLoop
def self.tick_loop(*a, &b)
TickLoop.new(*a, &b).start
end
# A TickLoop is useful when one needs to distribute amounts of work
# throughout ticks in order to maintain response times. It is also useful for
# simple repeated checks and metrics.
# @example
# # Here we run through an array one item per tick until it is empty,
# # printing each element.
# # When the array is empty, we return :stop from the callback, and the
# # loop will terminate.
# # When the loop terminates, the on_stop callbacks will be called.
# EM.run do
# array = (1..100).to_a
#
# tickloop = EM.tick_loop do
# if array.empty?
# :stop
# else
# puts array.shift
# end
# end
#
# tickloop.on_stop { EM.stop }
# end
#
class TickLoop
# Arguments: A callback (EM::Callback) to call each tick. If the call
# returns +:stop+ then the loop will be stopped. Any other value is
# ignored.
def initialize(*a, &b)
@work = EM::Callback(*a, &b)
@stops = []
@stopped = true
end
# Arguments: A callback (EM::Callback) to call once on the next stop (or
# immediately if already stopped).
def on_stop(*a, &b)
if @stopped
EM::Callback(*a, &b).call
else
@stops << EM::Callback(*a, &b)
end
end
# Stop the tick loop immediately, and call it's on_stop callbacks.
def stop
@stopped = true
until @stops.empty?
@stops.shift.call
end
end
# Query if the loop is stopped.
def stopped?
@stopped
end
# Start the tick loop, will raise argument error if the loop is already
# running.
def start
raise ArgumentError, "double start" unless @stopped
@stopped = false
schedule
end
private
def schedule
EM.next_tick do
next if @stopped
if @work.call == :stop
stop
else
schedule
end
end
self
end
end
end
@@ -0,0 +1,61 @@
module EventMachine
# Creates a one-time timer
#
# timer = EventMachine::Timer.new(5) do
# # this will never fire because we cancel it
# end
# timer.cancel
#
class Timer
# Create a new timer that fires after a given number of seconds
def initialize interval, callback=nil, &block
@signature = EventMachine::add_timer(interval, callback || block)
end
# Cancel the timer
def cancel
EventMachine.send :cancel_timer, @signature
end
end
# Creates a periodic timer
#
# @example
# n = 0
# timer = EventMachine::PeriodicTimer.new(5) do
# puts "the time is #{Time.now}"
# timer.cancel if (n+=1) > 5
# end
#
class PeriodicTimer
# Create a new periodic timer that executes every interval seconds
def initialize interval, callback=nil, &block
@interval = interval
@code = callback || block
@cancelled = false
@work = method(:fire)
schedule
end
# Cancel the periodic timer
def cancel
@cancelled = true
end
# Fire the timer every interval seconds
attr_accessor :interval
# @private
def schedule
EventMachine::add_timer @interval, @work
end
# @private
def fire
unless @cancelled
@code.call
schedule
end
end
end
end
@@ -0,0 +1,3 @@
module EventMachine
VERSION = "1.2.7"
end
File diff suppressed because it is too large Load Diff
Binary file not shown.
@@ -0,0 +1,316 @@
#--
#
# Author:: Francis Cianfrocca (gmail: blackhedd)
# Homepage:: http://rubyeventmachine.com
# Date:: 8 Apr 2006
#
# See EventMachine and EventMachine::Connection for documentation and
# usage examples.
#
#----------------------------------------------------------------------------
#
# Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
# Gmail: blackhedd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of either: 1) 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; or 2) Ruby's License.
#
# See the file COPYING for complete licensing information.
#
#---------------------------------------------------------------------------
#
#
# This module provides "glue" for the Java version of the EventMachine reactor core.
# For C++ EventMachines, the analogous functionality is found in ext/rubymain.cpp,
# which is a garden-variety Ruby-extension glue module.
require 'java'
require 'rubyeventmachine'
require 'socket'
java_import java.io.FileDescriptor
java_import java.nio.channels.SocketChannel
java_import java.lang.reflect.Field
module JavaFields
def set_field(key, value)
field = getClass.getDeclaredField(key)
field.setAccessible(true)
if field.getType.toString == 'int'
field.setInt(self, value)
else
field.set(self, value)
end
end
def get_field(key)
field = getClass.getDeclaredField(key)
field.setAccessible(true)
field.get(self)
end
end
FileDescriptor.send :include, JavaFields
SocketChannel.send :include, JavaFields
module EventMachine
# TODO: These event numbers are defined in way too many places.
# DRY them up.
# @private
TimerFired = 100
# @private
ConnectionData = 101
# @private
ConnectionUnbound = 102
# @private
ConnectionAccepted = 103
# @private
ConnectionCompleted = 104
# @private
LoopbreakSignalled = 105
# @private
ConnectionNotifyReadable = 106
# @private
ConnectionNotifyWritable = 107
# @private
SslHandshakeCompleted = 108
# @private
SslVerify = 109
# @private
EM_PROTO_SSLv2 = 2
# @private
EM_PROTO_SSLv3 = 4
# @private
EM_PROTO_TLSv1 = 8
# @private
EM_PROTO_TLSv1_1 = 16
# @private
EM_PROTO_TLSv1_2 = 32
# Exceptions that are defined in rubymain.cpp
class ConnectionError < RuntimeError; end
class ConnectionNotBound < RuntimeError; end
class UnknownTimerFired < RuntimeError; end
class Unsupported < RuntimeError; end
# This thunk class used to be called EM, but that caused conflicts with
# the alias "EM" for module EventMachine. (FC, 20Jun08)
class JEM < com.rubyeventmachine.EmReactor
def eventCallback a1, a2, a3, a4
s = String.from_java_bytes(a3.array[a3.position...a3.limit]) if a3
EventMachine::event_callback a1, a2, s || a4
nil
end
end
# class Connection < com.rubyeventmachine.Connection
# def associate_callback_target sig
# # No-op for the time being.
# end
# end
def self.initialize_event_machine
@em = JEM.new
end
def self.release_machine
@em = nil
end
def self.add_oneshot_timer interval
@em.installOneshotTimer interval
end
def self.run_machine
@em.run
end
def self.stop
@em.stop
end
def self.start_tcp_server server, port
@em.startTcpServer server, port
end
def self.stop_tcp_server sig
@em.stopTcpServer sig
end
def self.start_unix_server filename
# TEMPORARILY unsupported until someone figures out how to do it.
raise "unsupported on this platform"
end
def self.send_data sig, data, length
@em.sendData sig, data.to_java_bytes
rescue java.lang.NullPointerException
0
end
def self.send_datagram sig, data, length, address, port
@em.sendDatagram sig, data.to_java_bytes, length, address, port
end
def self.connect_server server, port
bind_connect_server nil, nil, server, port
end
def self.bind_connect_server bind_addr, bind_port, server, port
@em.connectTcpServer bind_addr, bind_port.to_i, server, port
end
def self.close_connection sig, after_writing
@em.closeConnection sig, after_writing
end
def self.set_comm_inactivity_timeout sig, interval
@em.setCommInactivityTimeout sig, interval
end
def self.set_pending_connect_timeout sig, val
end
def self.set_heartbeat_interval val
end
def self.start_tls sig
@em.startTls sig
end
def self.ssl?
false
end
def self.signal_loopbreak
@em.signalLoopbreak
end
def self.set_timer_quantum q
@em.setTimerQuantum q
end
def self.epoll
# Epoll is a no-op for Java.
# The latest Java versions run epoll when possible in NIO.
end
def self.epoll= val
end
def self.kqueue
end
def self.kqueue= val
end
def self.epoll?
false
end
def self.kqueue?
false
end
def self.set_rlimit_nofile n_descriptors
# Currently a no-op for Java.
end
def self.open_udp_socket server, port
@em.openUdpSocket server, port
end
def self.invoke_popen cmd
# TEMPORARILY unsupported until someone figures out how to do it.
raise "unsupported on this platform"
end
def self.read_keyboard
# TEMPORARILY unsupported until someone figures out how to do it.
raise "temporarily unsupported on this platform"
end
def self.set_max_timer_count num
# harmless no-op in Java. There's no built-in timer limit.
@max_timer_count = num
end
def self.get_max_timer_count
# harmless no-op in Java. There's no built-in timer limit.
@max_timer_count || 100_000
end
def self.library_type
:java
end
def self.get_peername sig
if peer = @em.getPeerName(sig)
Socket.pack_sockaddr_in(*peer)
end
end
def self.get_sockname sig
if sockName = @em.getSockName(sig)
Socket.pack_sockaddr_in(*sockName)
end
end
# @private
def self.attach_fd fileno, watch_mode
# 3Aug09: We could pass in the actual SocketChannel, but then it would be modified (set as non-blocking), and
# we would need some logic to make sure detach_fd below didn't clobber it. For now, we just always make a new
# SocketChannel for the underlying file descriptor
# if fileno.java_kind_of? SocketChannel
# ch = fileno
# ch.configureBlocking(false)
# fileno = nil
# elsif fileno.java_kind_of? java.nio.channels.Channel
if fileno.java_kind_of? java.nio.channels.Channel
field = fileno.getClass.getDeclaredField('fdVal')
field.setAccessible(true)
fileno = field.get(fileno)
else
raise ArgumentError, 'attach_fd requires Java Channel or POSIX fileno' unless fileno.is_a? Integer
end
if fileno == 0
raise "can't open STDIN as selectable in Java =("
elsif fileno.is_a? Integer
# 8Aug09: The following code is specific to the sun jvm's SocketChannelImpl. Is there a cross-platform
# way of implementing this? If so, also remember to update EventableSocketChannel#close and #cleanup
fd = FileDescriptor.new
fd.set_field 'fd', fileno
ch = SocketChannel.open
ch.configureBlocking(false)
ch.kill
ch.set_field 'fd', fd
ch.set_field 'fdVal', fileno
ch.set_field 'state', ch.get_field('ST_CONNECTED')
end
@em.attachChannel(ch,watch_mode)
end
def self.detach_fd sig
if ch = @em.detachChannel(sig)
ch.get_field 'fdVal'
end
end
def self.set_notify_readable sig, mode
@em.setNotifyReadable(sig, mode)
end
def self.set_notify_writable sig, mode
@em.setNotifyWritable(sig, mode)
end
def self.is_notify_readable sig
@em.isNotifyReadable(sig)
end
def self.is_notify_writable sig
@em.isNotifyWritable(sig)
end
def self.get_connection_count
@em.getConnectionCount
end
def self.pause_connection(sig)
@em.pauseConnection(sig)
end
def self.resume_connection(sig)
@em.resumeConnection(sig)
end
def self.connection_paused?(sig)
@em.isConnectionPaused(sig)
end
def self._get_outbound_data_size(sig)
@em.getOutboundDataSize(sig)
end
def self.set_tls_parms(sig, params)
end
def self.start_tls(sig)
end
def self.send_file_data(sig, filename)
end
class Connection
def associate_callback_target sig
# No-op for the time being
end
def get_outbound_data_size
EM._get_outbound_data_size @signature
end
end
end
Binary file not shown.