348 lines
9.7 KiB
Ruby
348 lines
9.7 KiB
Ruby
|
require 'set'
|
||
|
require 'tempfile'
|
||
|
|
||
|
module Rack
|
||
|
# Rack::Utils contains a grab-bag of useful methods for writing web
|
||
|
# applications adopted from all kinds of Ruby libraries.
|
||
|
|
||
|
module Utils
|
||
|
# Performs URI escaping so that you can construct proper
|
||
|
# query strings faster. Use this rather than the cgi.rb
|
||
|
# version since it's faster. (Stolen from Camping).
|
||
|
def escape(s)
|
||
|
s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
|
||
|
'%'+$1.unpack('H2'*$1.size).join('%').upcase
|
||
|
}.tr(' ', '+')
|
||
|
end
|
||
|
module_function :escape
|
||
|
|
||
|
# Unescapes a URI escaped string. (Stolen from Camping).
|
||
|
def unescape(s)
|
||
|
s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
|
||
|
[$1.delete('%')].pack('H*')
|
||
|
}
|
||
|
end
|
||
|
module_function :unescape
|
||
|
|
||
|
# Stolen from Mongrel, with some small modifications:
|
||
|
# Parses a query string by breaking it up at the '&'
|
||
|
# and ';' characters. You can also use this to parse
|
||
|
# cookies by changing the characters used in the second
|
||
|
# parameter (which defaults to '&;').
|
||
|
|
||
|
def parse_query(qs, d = '&;')
|
||
|
params = {}
|
||
|
|
||
|
(qs || '').split(/[#{d}] */n).each do |p|
|
||
|
k, v = unescape(p).split('=', 2)
|
||
|
|
||
|
if cur = params[k]
|
||
|
if cur.class == Array
|
||
|
params[k] << v
|
||
|
else
|
||
|
params[k] = [cur, v]
|
||
|
end
|
||
|
else
|
||
|
params[k] = v
|
||
|
end
|
||
|
end
|
||
|
|
||
|
return params
|
||
|
end
|
||
|
module_function :parse_query
|
||
|
|
||
|
def build_query(params)
|
||
|
params.map { |k, v|
|
||
|
if v.class == Array
|
||
|
build_query(v.map { |x| [k, x] })
|
||
|
else
|
||
|
escape(k) + "=" + escape(v)
|
||
|
end
|
||
|
}.join("&")
|
||
|
end
|
||
|
module_function :build_query
|
||
|
|
||
|
# Escape ampersands, brackets and quotes to their HTML/XML entities.
|
||
|
def escape_html(string)
|
||
|
string.to_s.gsub("&", "&").
|
||
|
gsub("<", "<").
|
||
|
gsub(">", ">").
|
||
|
gsub("'", "'").
|
||
|
gsub('"', """)
|
||
|
end
|
||
|
module_function :escape_html
|
||
|
|
||
|
def select_best_encoding(available_encodings, accept_encoding)
|
||
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
||
|
|
||
|
expanded_accept_encoding =
|
||
|
accept_encoding.map { |m, q|
|
||
|
if m == "*"
|
||
|
(available_encodings - accept_encoding.map { |m2, _| m2 }).map { |m2| [m2, q] }
|
||
|
else
|
||
|
[[m, q]]
|
||
|
end
|
||
|
}.inject([]) { |mem, list|
|
||
|
mem + list
|
||
|
}
|
||
|
|
||
|
encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map { |m, _| m }
|
||
|
|
||
|
unless encoding_candidates.include?("identity")
|
||
|
encoding_candidates.push("identity")
|
||
|
end
|
||
|
|
||
|
expanded_accept_encoding.find_all { |m, q|
|
||
|
q == 0.0
|
||
|
}.each { |m, _|
|
||
|
encoding_candidates.delete(m)
|
||
|
}
|
||
|
|
||
|
return (encoding_candidates & available_encodings)[0]
|
||
|
end
|
||
|
module_function :select_best_encoding
|
||
|
|
||
|
# The recommended manner in which to implement a contexting application
|
||
|
# is to define a method #context in which a new Context is instantiated.
|
||
|
#
|
||
|
# As a Context is a glorified block, it is highly recommended that you
|
||
|
# define the contextual block within the application's operational scope.
|
||
|
# This would typically the application as you're place into Rack's stack.
|
||
|
#
|
||
|
# class MyObject
|
||
|
# ...
|
||
|
# def context app
|
||
|
# Rack::Utils::Context.new app do |env|
|
||
|
# do_stuff
|
||
|
# response = app.call(env)
|
||
|
# do_more_stuff
|
||
|
# end
|
||
|
# end
|
||
|
# ...
|
||
|
# end
|
||
|
#
|
||
|
# mobj = MyObject.new
|
||
|
# app = mobj.context other_app
|
||
|
# Rack::Handler::Mongrel.new app
|
||
|
class Context < Proc
|
||
|
alias_method :old_inspect, :inspect
|
||
|
attr_reader :for, :app
|
||
|
def initialize app_f, app_r
|
||
|
raise 'running context not provided' unless app_f
|
||
|
raise 'running context does not respond to #context' unless app_f.respond_to? :context
|
||
|
raise 'application context not provided' unless app_r
|
||
|
raise 'application context does not respond to #call' unless app_r.respond_to? :call
|
||
|
@for = app_f
|
||
|
@app = app_r
|
||
|
end
|
||
|
def inspect
|
||
|
"#{old_inspect} ==> #{@for.inspect} ==> #{@app.inspect}"
|
||
|
end
|
||
|
def context app_r
|
||
|
raise 'new application context not provided' unless app_r
|
||
|
raise 'new application context does not respond to #call' unless app_r.respond_to? :call
|
||
|
@for.context app_r
|
||
|
end
|
||
|
def pretty_print pp
|
||
|
pp.text old_inspect
|
||
|
pp.nest 1 do
|
||
|
pp.breakable
|
||
|
pp.text '=for> '
|
||
|
pp.pp @for
|
||
|
pp.breakable
|
||
|
pp.text '=app> '
|
||
|
pp.pp @app
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# A case-insensitive Hash that preserves the original case of a
|
||
|
# header when set.
|
||
|
class HeaderHash < Hash
|
||
|
def initialize(hash={})
|
||
|
@names = {}
|
||
|
hash.each { |k, v| self[k] = v }
|
||
|
end
|
||
|
|
||
|
def to_hash
|
||
|
{}.replace(self)
|
||
|
end
|
||
|
|
||
|
def [](k)
|
||
|
super @names[k.downcase]
|
||
|
end
|
||
|
|
||
|
def []=(k, v)
|
||
|
delete k
|
||
|
@names[k.downcase] = k
|
||
|
super k, v
|
||
|
end
|
||
|
|
||
|
def delete(k)
|
||
|
super @names.delete(k.downcase)
|
||
|
end
|
||
|
|
||
|
def include?(k)
|
||
|
@names.has_key? k.downcase
|
||
|
end
|
||
|
|
||
|
alias_method :has_key?, :include?
|
||
|
alias_method :member?, :include?
|
||
|
alias_method :key?, :include?
|
||
|
|
||
|
def merge!(other)
|
||
|
other.each { |k, v| self[k] = v }
|
||
|
self
|
||
|
end
|
||
|
|
||
|
def merge(other)
|
||
|
hash = dup
|
||
|
hash.merge! other
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# Every standard HTTP code mapped to the appropriate message.
|
||
|
# Stolen from Mongrel.
|
||
|
HTTP_STATUS_CODES = {
|
||
|
100 => 'Continue',
|
||
|
101 => 'Switching Protocols',
|
||
|
200 => 'OK',
|
||
|
201 => 'Created',
|
||
|
202 => 'Accepted',
|
||
|
203 => 'Non-Authoritative Information',
|
||
|
204 => 'No Content',
|
||
|
205 => 'Reset Content',
|
||
|
206 => 'Partial Content',
|
||
|
300 => 'Multiple Choices',
|
||
|
301 => 'Moved Permanently',
|
||
|
302 => 'Found',
|
||
|
303 => 'See Other',
|
||
|
304 => 'Not Modified',
|
||
|
305 => 'Use Proxy',
|
||
|
307 => 'Temporary Redirect',
|
||
|
400 => 'Bad Request',
|
||
|
401 => 'Unauthorized',
|
||
|
402 => 'Payment Required',
|
||
|
403 => 'Forbidden',
|
||
|
404 => 'Not Found',
|
||
|
405 => 'Method Not Allowed',
|
||
|
406 => 'Not Acceptable',
|
||
|
407 => 'Proxy Authentication Required',
|
||
|
408 => 'Request Timeout',
|
||
|
409 => 'Conflict',
|
||
|
410 => 'Gone',
|
||
|
411 => 'Length Required',
|
||
|
412 => 'Precondition Failed',
|
||
|
413 => 'Request Entity Too Large',
|
||
|
414 => 'Request-URI Too Large',
|
||
|
415 => 'Unsupported Media Type',
|
||
|
416 => 'Requested Range Not Satisfiable',
|
||
|
417 => 'Expectation Failed',
|
||
|
500 => 'Internal Server Error',
|
||
|
501 => 'Not Implemented',
|
||
|
502 => 'Bad Gateway',
|
||
|
503 => 'Service Unavailable',
|
||
|
504 => 'Gateway Timeout',
|
||
|
505 => 'HTTP Version Not Supported'
|
||
|
}
|
||
|
|
||
|
# Responses with HTTP status codes that should not have an entity body
|
||
|
STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 304)
|
||
|
|
||
|
# A multipart form data parser, adapted from IOWA.
|
||
|
#
|
||
|
# Usually, Rack::Request#POST takes care of calling this.
|
||
|
|
||
|
module Multipart
|
||
|
EOL = "\r\n"
|
||
|
|
||
|
def self.parse_multipart(env)
|
||
|
unless env['CONTENT_TYPE'] =~
|
||
|
%r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n
|
||
|
nil
|
||
|
else
|
||
|
boundary = "--#{$1}"
|
||
|
|
||
|
params = {}
|
||
|
buf = ""
|
||
|
content_length = env['CONTENT_LENGTH'].to_i
|
||
|
input = env['rack.input']
|
||
|
|
||
|
boundary_size = boundary.size + EOL.size
|
||
|
bufsize = 16384
|
||
|
|
||
|
content_length -= boundary_size
|
||
|
|
||
|
status = input.read(boundary_size)
|
||
|
raise EOFError, "bad content body" unless status == boundary + EOL
|
||
|
|
||
|
rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/
|
||
|
|
||
|
loop {
|
||
|
head = nil
|
||
|
body = ''
|
||
|
filename = content_type = name = nil
|
||
|
|
||
|
until head && buf =~ rx
|
||
|
if !head && i = buf.index("\r\n\r\n")
|
||
|
head = buf.slice!(0, i+2) # First \r\n
|
||
|
buf.slice!(0, 2) # Second \r\n
|
||
|
|
||
|
filename = head[/Content-Disposition:.* filename="?([^\";]*)"?/ni, 1]
|
||
|
content_type = head[/Content-Type: (.*)\r\n/ni, 1]
|
||
|
name = head[/Content-Disposition:.* name="?([^\";]*)"?/ni, 1]
|
||
|
|
||
|
if filename
|
||
|
body = Tempfile.new("RackMultipart")
|
||
|
body.binmode if body.respond_to?(:binmode)
|
||
|
end
|
||
|
|
||
|
next
|
||
|
end
|
||
|
|
||
|
# Save the read body part.
|
||
|
if head && (boundary_size+4 < buf.size)
|
||
|
body << buf.slice!(0, buf.size - (boundary_size+4))
|
||
|
end
|
||
|
|
||
|
c = input.read(bufsize < content_length ? bufsize : content_length)
|
||
|
raise EOFError, "bad content body" if c.nil? || c.empty?
|
||
|
buf << c
|
||
|
content_length -= c.size
|
||
|
end
|
||
|
|
||
|
# Save the rest.
|
||
|
if i = buf.index(rx)
|
||
|
body << buf.slice!(0, i)
|
||
|
buf.slice!(0, boundary_size+2)
|
||
|
|
||
|
content_length = -1 if $1 == "--"
|
||
|
end
|
||
|
|
||
|
if filename
|
||
|
body.rewind
|
||
|
data = {:filename => filename, :type => content_type,
|
||
|
:name => name, :tempfile => body, :head => head}
|
||
|
else
|
||
|
data = body
|
||
|
end
|
||
|
|
||
|
if name
|
||
|
if name =~ /\[\]\z/
|
||
|
params[name] ||= []
|
||
|
params[name] << data
|
||
|
else
|
||
|
params[name] = data
|
||
|
end
|
||
|
end
|
||
|
|
||
|
break if buf.empty? || content_length == -1
|
||
|
}
|
||
|
|
||
|
params
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|