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,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