2009-08-04 17:16:03 +02:00
|
|
|
# -*- encoding: binary -*-
|
|
|
|
|
2009-02-04 21:26:08 +01:00
|
|
|
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) {
|
2009-12-19 03:16:58 +01:00
|
|
|
'%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
|
2009-02-04 21:26:08 +01:00
|
|
|
}.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
|
|
|
|
|
2009-12-19 03:16:58 +01:00
|
|
|
DEFAULT_SEP = /[&;] */n
|
|
|
|
|
2009-02-04 21:26:08 +01:00
|
|
|
# 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 '&;').
|
2009-12-19 03:16:58 +01:00
|
|
|
def parse_query(qs, d = nil)
|
2009-02-04 21:26:08 +01:00
|
|
|
params = {}
|
|
|
|
|
2009-12-19 03:16:58 +01:00
|
|
|
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
|
|
|
|
k, v = p.split('=', 2).map { |x| unescape(x) }
|
2009-02-04 21:26:08 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2009-12-19 03:16:58 +01:00
|
|
|
def parse_nested_query(qs, d = nil)
|
2009-02-28 02:23:00 +01:00
|
|
|
params = {}
|
|
|
|
|
2009-12-19 03:16:58 +01:00
|
|
|
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
|
2009-02-28 02:23:00 +01:00
|
|
|
k, v = unescape(p).split('=', 2)
|
|
|
|
normalize_params(params, k, v)
|
|
|
|
end
|
|
|
|
|
|
|
|
return params
|
|
|
|
end
|
|
|
|
module_function :parse_nested_query
|
|
|
|
|
|
|
|
def normalize_params(params, name, v = nil)
|
2009-08-04 17:16:03 +02:00
|
|
|
name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
|
2009-02-28 02:23:00 +01:00
|
|
|
k = $1 || ''
|
|
|
|
after = $' || ''
|
|
|
|
|
|
|
|
return if k.empty?
|
|
|
|
|
|
|
|
if after == ""
|
|
|
|
params[k] = v
|
|
|
|
elsif after == "[]"
|
|
|
|
params[k] ||= []
|
2009-08-04 17:16:03 +02:00
|
|
|
raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
|
2009-02-28 02:23:00 +01:00
|
|
|
params[k] << v
|
|
|
|
elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
|
|
|
|
child_key = $1
|
|
|
|
params[k] ||= []
|
2009-08-04 17:16:03 +02:00
|
|
|
raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
|
2009-02-28 02:23:00 +01:00
|
|
|
if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)
|
|
|
|
normalize_params(params[k].last, child_key, v)
|
|
|
|
else
|
|
|
|
params[k] << normalize_params({}, child_key, v)
|
|
|
|
end
|
|
|
|
else
|
|
|
|
params[k] ||= {}
|
2009-08-04 17:16:03 +02:00
|
|
|
raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Hash)
|
2009-02-28 02:23:00 +01:00
|
|
|
params[k] = normalize_params(params[k], after, v)
|
|
|
|
end
|
|
|
|
|
|
|
|
return params
|
|
|
|
end
|
|
|
|
module_function :normalize_params
|
|
|
|
|
2009-02-04 21:26:08 +01:00
|
|
|
def build_query(params)
|
|
|
|
params.map { |k, v|
|
|
|
|
if v.class == Array
|
|
|
|
build_query(v.map { |x| [k, x] })
|
|
|
|
else
|
2009-12-19 03:16:58 +01:00
|
|
|
"#{escape(k)}=#{escape(v)}"
|
2009-02-04 21:26:08 +01:00
|
|
|
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
|
|
|
|
|
2009-03-16 15:55:30 +01:00
|
|
|
# Return the bytesize of String; uses String#length under Ruby 1.8 and
|
|
|
|
# String#bytesize under 1.9.
|
|
|
|
if ''.respond_to?(:bytesize)
|
|
|
|
def bytesize(string)
|
|
|
|
string.bytesize
|
|
|
|
end
|
|
|
|
else
|
|
|
|
def bytesize(string)
|
|
|
|
string.size
|
|
|
|
end
|
|
|
|
end
|
|
|
|
module_function :bytesize
|
|
|
|
|
2009-02-28 02:23:00 +01:00
|
|
|
# Context allows the use of a compatible middleware at different points
|
|
|
|
# in a request handling stack. A compatible middleware must define
|
|
|
|
# #context which should take the arguments env and app. The first of which
|
|
|
|
# would be the request environment. The second of which would be the rack
|
|
|
|
# application that the request would be forwarded to.
|
|
|
|
class Context
|
2009-02-04 21:26:08 +01:00
|
|
|
attr_reader :for, :app
|
2009-02-28 02:23:00 +01:00
|
|
|
|
|
|
|
def initialize(app_f, app_r)
|
2009-02-04 21:26:08 +01:00
|
|
|
raise 'running context does not respond to #context' unless app_f.respond_to? :context
|
2009-02-28 02:23:00 +01:00
|
|
|
@for, @app = app_f, app_r
|
2009-02-04 21:26:08 +01:00
|
|
|
end
|
2009-02-28 02:23:00 +01:00
|
|
|
|
|
|
|
def call(env)
|
|
|
|
@for.context(env, @app)
|
2009-02-04 21:26:08 +01:00
|
|
|
end
|
2009-02-28 02:23:00 +01:00
|
|
|
|
|
|
|
def recontext(app)
|
|
|
|
self.class.new(@for, app)
|
2009-02-04 21:26:08 +01:00
|
|
|
end
|
2009-02-28 02:23:00 +01:00
|
|
|
|
|
|
|
def context(env, app=@app)
|
|
|
|
recontext(app).call(env)
|
2009-02-04 21:26:08 +01:00
|
|
|
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
|
2009-02-28 02:23:00 +01:00
|
|
|
inject({}) do |hash, (k,v)|
|
|
|
|
if v.respond_to? :to_ary
|
|
|
|
hash[k] = v.to_ary.join("\n")
|
|
|
|
else
|
|
|
|
hash[k] = v
|
|
|
|
end
|
|
|
|
hash
|
|
|
|
end
|
2009-02-04 21:26:08 +01:00
|
|
|
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'] =~
|
2009-08-04 17:16:03 +02:00
|
|
|
%r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n
|
2009-02-04 21:26:08 +01:00
|
|
|
nil
|
|
|
|
else
|
|
|
|
boundary = "--#{$1}"
|
|
|
|
|
|
|
|
params = {}
|
|
|
|
buf = ""
|
|
|
|
content_length = env['CONTENT_LENGTH'].to_i
|
|
|
|
input = env['rack.input']
|
2009-08-04 17:16:03 +02:00
|
|
|
input.rewind
|
2009-02-04 21:26:08 +01:00
|
|
|
|
2009-08-04 17:16:03 +02:00
|
|
|
boundary_size = Utils.bytesize(boundary) + EOL.size
|
2009-02-04 21:26:08 +01:00
|
|
|
bufsize = 16384
|
|
|
|
|
|
|
|
content_length -= boundary_size
|
|
|
|
|
2009-08-04 17:16:03 +02:00
|
|
|
read_buffer = ''
|
|
|
|
|
|
|
|
status = input.read(boundary_size, read_buffer)
|
2009-02-04 21:26:08 +01:00
|
|
|
raise EOFError, "bad content body" unless status == boundary + EOL
|
|
|
|
|
2009-02-28 02:23:00 +01:00
|
|
|
rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/n
|
2009-02-04 21:26:08 +01:00
|
|
|
|
|
|
|
loop {
|
|
|
|
head = nil
|
|
|
|
body = ''
|
|
|
|
filename = content_type = name = nil
|
|
|
|
|
|
|
|
until head && buf =~ rx
|
2009-08-04 17:16:03 +02:00
|
|
|
if !head && i = buf.index(EOL+EOL)
|
2009-02-04 21:26:08 +01:00
|
|
|
head = buf.slice!(0, i+2) # First \r\n
|
|
|
|
buf.slice!(0, 2) # Second \r\n
|
|
|
|
|
2009-12-19 03:16:58 +01:00
|
|
|
filename = head[/Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;\s]*))/ni, 1]
|
2009-08-04 17:16:03 +02:00
|
|
|
content_type = head[/Content-Type: (.*)#{EOL}/ni, 1]
|
|
|
|
name = head[/Content-Disposition:.*\s+name="?([^\";]*)"?/ni, 1] || head[/Content-ID:\s*([^#{EOL}]*)/ni, 1]
|
2009-02-04 21:26:08 +01:00
|
|
|
|
2009-08-04 17:16:03 +02:00
|
|
|
if content_type || filename
|
2009-02-04 21:26:08 +01:00
|
|
|
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
|
|
|
|
|
2009-08-04 17:16:03 +02:00
|
|
|
c = input.read(bufsize < content_length ? bufsize : content_length, read_buffer)
|
2009-02-04 21:26:08 +01:00
|
|
|
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
|
|
|
|
|
2009-02-28 02:23:00 +01:00
|
|
|
if filename == ""
|
|
|
|
# filename is blank which means no file has been selected
|
|
|
|
data = nil
|
|
|
|
elsif filename
|
2009-02-04 21:26:08 +01:00
|
|
|
body.rewind
|
2009-02-28 02:23:00 +01:00
|
|
|
|
|
|
|
# Take the basename of the upload's original filename.
|
|
|
|
# This handles the full Windows paths given by Internet Explorer
|
|
|
|
# (and perhaps other broken user agents) without affecting
|
|
|
|
# those which give the lone filename.
|
|
|
|
filename =~ /^(?:.*[:\\\/])?(.*)/m
|
|
|
|
filename = $1
|
|
|
|
|
2009-02-04 21:26:08 +01:00
|
|
|
data = {:filename => filename, :type => content_type,
|
|
|
|
:name => name, :tempfile => body, :head => head}
|
2009-08-04 17:16:03 +02:00
|
|
|
elsif !filename && content_type
|
|
|
|
body.rewind
|
2009-12-19 03:16:58 +01:00
|
|
|
|
2009-08-04 17:16:03 +02:00
|
|
|
# Generic multipart cases, not coming from a form
|
|
|
|
data = {:type => content_type,
|
|
|
|
:name => name, :tempfile => body, :head => head}
|
2009-02-04 21:26:08 +01:00
|
|
|
else
|
|
|
|
data = body
|
|
|
|
end
|
|
|
|
|
2009-03-16 15:55:30 +01:00
|
|
|
Utils.normalize_params(params, name, data) unless data.nil?
|
2009-02-04 21:26:08 +01:00
|
|
|
|
|
|
|
break if buf.empty? || content_length == -1
|
|
|
|
}
|
|
|
|
|
2009-08-04 17:16:03 +02:00
|
|
|
input.rewind
|
2009-02-28 02:23:00 +01:00
|
|
|
|
2009-02-04 21:26:08 +01:00
|
|
|
params
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|