The Mongrel2
Request
base class. Derivatives of this class represent a request from a Mongrel2
server.
The request body data, if there is any, as an IO(ish) object
The listener ID on the server
The Mongrel2::Table
object that contains the request headers
The Mongrel2::Table
object that contains the request headers
The path component of the requested URL in HTTP, or the equivalent for other request types
The raw request content, if the request was parsed from mongrel2
The UUID of the requesting mongrel server
Create a new Request
object with the given sender_id
, conn_id
, path
, headers
, and body
. The optional nil
is for the raw request content, which can be useful later for debugging.
# File lib/mongrel2/request.rb, line 113
def initialize( sender_id, conn_id, path, headers, body='', raw=nil )
@sender_id = sender_id
@conn_id = Integer( conn_id )
@path = path
@headers = Mongrel2::Table.new( headers )
@raw = raw
@body = self.make_entity_body( body )
@response = nil
end
Parse the given raw_request
from a Mongrel2
server and return an appropriate request object.
# File lib/mongrel2/request.rb, line 28
def self::parse( raw_request )
sender, conn_id, path, rest = raw_request.split( ' ', 4 )
self.log.debug "Parsing request for %p from %s:%s (rest: %p...)" %
[ path, sender, conn_id, rest[0,20] ]
# Extract the headers and the body, ignore the rest
headers, rest = TNetstring.parse( rest )
body, _ = TNetstring.parse( rest )
# Headers will be a JSON String when not using the TNetString protocol
if headers.is_a?( String )
self.log.debug " parsing old-style headers"
headers = Yajl::Parser.parse( headers )
end
# This isn't supposed to happen, but guard against it anyway
headers['METHOD'] =~ /^(\w+)$/ or
raise Mongrel2::UnhandledMethodError, headers['METHOD']
req_method = $1.untaint.to_sym
self.log.debug "Request method is: %p" % [ req_method ]
concrete_class = self.subclass_for_method( req_method )
return concrete_class.new( sender, conn_id, path, headers, body, raw_request )
end
Register the specified subclass
as the class to instantiate when the METHOD
header is one of the specified req_methods
. This method exists for frameworks which wish to provide their own Request
types.
For example, if your framework has a JSONRequest class that inherits from Mongrel2::JSONRequest
, and you want it to be returned from Mongrel2::Request.parse
for METHOD=JSON requests:
class MyFramework::JSONRequest < Mongrel2::JSONRequest register_request_type self, 'JSON' # Override #initialize to do any stuff specific to your # request type, but you'll likely want to super() to # Mongrel2::JSONRequest. def initialize( * ) super # Do some other stuff end end # class MyFramework::JSONRequest
If you wish one of your subclasses to be used instead of Mongrel2::Request
for the default request class, register it with a METHOD of :__default.
# File lib/mongrel2/request.rb, line 77
def self::register_request_type( subclass, *req_methods )
self.log.debug "Registering %p for %p requests" % [ subclass, req_methods ]
req_methods.each do |methname|
if methname == :__default
# Clear cached lookups
self.log.info "Registering %p as the default request type." % [ subclass ]
Mongrel2::Request.request_types.delete_if {|_, klass| klass == Mongrel2::Request }
Mongrel2::Request.request_types.default_proc = lambda {|h,k| h[k] = subclass }
else
self.log.info "Registering %p for the %p method." % [ subclass, methname ]
Mongrel2::Request.request_types[ methname.to_sym ] = subclass
end
end
end
Return the Mongrel2::Response
class that corresponds with the receiver.
# File lib/mongrel2/request.rb, line 100
def self::response_class
return Mongrel2::Response
end
Return the Mongrel2::Request
class registered for the request method methname
.
# File lib/mongrel2/request.rb, line 94
def self::subclass_for_method( methname )
return Mongrel2::Request.request_types[ methname.to_sym ]
end
Set the request's entity body to newbody
. If newbody
is a String-ish object (i.e., it responds to to_str), it will be wrapped in a StringIO in 'r+' mode).
# File lib/mongrel2/request.rb, line 153
def body=( newbody )
newbody = StringIO.new( newbody, 'a+' ) if newbody.respond_to?( :to_str )
@body = newbody
end
Return true
if the request is a special 'disconnect' notification from Mongrel2
.
# File lib/mongrel2/request.rb, line 169
def is_disconnect?
return false
end
Fetch the original requestor IP address.
# File lib/mongrel2/request.rb, line 175
def remote_ip
ips = [ self.headers.x_forwarded_for ]
return IPAddr.new( ips.flatten.first )
end
Create a Mongrel2::Response
that will respond to the same server/connection as the receiver. If you wish your specialized Request
class to have a corresponding response type, you can override the Mongrel2::Request.response_class
method to achieve that.
# File lib/mongrel2/request.rb, line 163
def response
return @response ||= self.class.response_class.from_request( self )
end
Indicate that a request is never an extended reply.
# File lib/mongrel2/request.rb, line 251
def extended_reply?
return false
end
Return the chroot directory of the mongrel2 daemon that received this request as a Pathname.
# File lib/mongrel2/request.rb, line 209
def server_chroot
route = Mongrel2::Config::Route.for_request( self ) or
raise Mongrel2::UploadError, "couldn't find the route config for %s" % [ self ]
server = route.host.server
path = server.chroot
path = '/' if path.empty?
return Pathname( path )
end
Returns true
if this request is an 'asynchronous upload done' notification.
# File lib/mongrel2/request.rb, line 229
def upload_done?
return self.headers.member?( :x_mongrel2_upload_start ) &&
self.headers.member?( :x_mongrel2_upload_done )
end
Returns true
if this request is an 'asynchronous upload done' notification and the two headers match (trivial guard against forgery)
# File lib/mongrel2/request.rb, line 237
def upload_headers_match?
return self.upload_done? &&
self.headers.x_mongrel2_upload_start == self.headers.x_mongrel2_upload_done
end
Returns true
if this request is an 'asynchronous upload started' notification.
# File lib/mongrel2/request.rb, line 222
def upload_started?
return self.headers.member?( :x_mongrel2_upload_start ) &&
!self.headers.member?( :x_mongrel2_upload_done )
end
The Pathname, relative to Mongrel2's chroot path, of the uploaded entity body.
# File lib/mongrel2/request.rb, line 187
def uploaded_file
raise Mongrel2::UploadError, "invalid upload: upload headers don't match" unless
self.upload_headers_match?
relpath = Pathname( self.headers.x_mongrel2_upload_done )
chrooted = self.server_chroot + relpath
if chrooted.exist?
return chrooted
elsif relpath.exist?
return relpath
else
self.log.error "uploaded body %s not found: tried relative to cwd and server chroot (%s)" %
[ relpath, chrooted ]
raise Mongrel2::UploadError,
"couldn't find the path to uploaded body %p." % [ chrooted.to_s ]
end
end
Returns true if this request is an asynchronous upload, and the filename of the finished request matches the one from the starting notification.
# File lib/mongrel2/request.rb, line 245
def valid_upload?
return self.upload_done? && self.upload_headers_match?
end
Returns a string containing a human-readable representation of the Request
, suitable for debugging.
# File lib/mongrel2/request.rb, line 262
def inspect
return "#<%p:0x%016x %s (%s/%d)>" % [
self.class,
self.object_id * 2,
self.inspect_details,
self.sender_id,
self.conn_id
]
end
Return the details to include in the contents of the inspected object. This method allows other request types to provide their own details while keeping the form somewhat consistent.
# File lib/mongrel2/request.rb, line 310
def inspect_details
return "%s -- %d headers, %p body" % [
self.path,
self.headers.length,
self.body.class,
]
end
Convert the entity body
into an IOish object, wrapping it in a StringIO if it doesn't already respond to :read, :pos, and :seek. If the request has valid 'X-Mongrel2-Upload-*' headers (the async upload API), a File object opened to the spool file will be returned instead.
# File lib/mongrel2/request.rb, line 281
def make_entity_body( body )
# :TODO: Handle Content-Encoding, too.
enc = self.headers.content_type[ /\bcharset=(\S+)/, 1 ] if self.headers.content_type
if self.valid_upload?
enc ||= Encoding::ASCII_8BIT
spoolfile = self.uploaded_file
self.log.info "Using async %s spool file %s as request entity body." % [ enc, spoolfile ]
return spoolfile.open( 'r', encoding: enc )
elsif !( body.respond_to?(:read) && body.respond_to?(:pos) && body.respond_to?(:seek) )
self.log.info "Wrapping non-IO (%p) body in a StringIO" % [ body.class ] unless
body.is_a?( String )
# Get the object as a String, set the encoding
str = body.to_s
str.force_encoding( enc ) if enc && str.encoding == Encoding::ASCII_8BIT
return StringIO.new( str, 'r+' )
else
return body
end
end