This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user