lib/rack/utils.rb in rack-1.2.8 vs lib/rack/utils.rb in rack-1.3.0.beta
- old
+ new
@@ -2,64 +2,55 @@
require 'fileutils'
require 'set'
require 'tempfile'
+require 'rack/multipart'
+
+if RUBY_VERSION[/^\d+\.\d+/] == '1.8'
+ # pull in backports
+ require 'rack/backports/uri/common'
+else
+ require 'uri/common'
+end
+
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).
+ # URI escapes a string. (CGI style space to +)
def escape(s)
- s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
- '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
- }.tr(' ', '+')
+ URI.encode_www_form_component(s)
end
module_function :escape
- # Unescapes a URI escaped string. (Stolen from Camping).
+ # Like URI escaping, but with %20 instead of +. Strictly speaking this is
+ # true URI escaping.
+ def escape_path(s)
+ escape(s).gsub('+', '%20')
+ end
+ module_function :escape_path
+
+ # Unescapes a URI escaped string.
def unescape(s)
- s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
- [$1.delete('%')].pack('H*')
- }
+ URI.decode_www_form_component(s)
end
module_function :unescape
DEFAULT_SEP = /[&;] */n
- class << self
- attr_accessor :key_space_limit
- end
-
- # The default number of bytes to allow parameter keys to take up.
- # This helps prevent a rogue client from flooding a Request.
- self.key_space_limit = 65536
-
# 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 = nil)
params = {}
- max_key_space = Utils.key_space_limit
- bytes = 0
-
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
k, v = p.split('=', 2).map { |x| unescape(x) }
-
- if k
- bytes += k.size
- if bytes > max_key_space
- raise RangeError, "exceeded available parameter key space"
- end
- end
-
if cur = params[k]
if cur.class == Array
params[k] << v
else
params[k] = [cur, v]
@@ -74,23 +65,12 @@
module_function :parse_query
def parse_nested_query(qs, d = nil)
params = {}
- max_key_space = Utils.key_space_limit
- bytes = 0
-
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
- k, v = unescape(p).split('=', 2)
-
- if k
- bytes += k.size
- if bytes > max_key_space
- raise RangeError, "exceeded available parameter key space"
- end
- end
-
+ k, v = p.split('=', 2).map { |s| unescape(s) }
normalize_params(params, k, v)
end
return params
end
@@ -160,21 +140,15 @@
ESCAPE_HTML = {
"&" => "&",
"<" => "<",
">" => ">",
- "'" => "'",
+ "'" => "'",
'"' => """,
- "/" => "/"
+ "/" => "/"
}
- if //.respond_to?(:encoding)
- ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
- else
- # On 1.8, there is a kcode = 'u' bug that allows for XSS otherwhise
- # TODO doesn't apply to jruby, so a better condition above might be preferable?
- ESCAPE_HTML_PATTERN = /#{Regexp.union(*ESCAPE_HTML.keys)}/n
- end
+ ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
# Escape ampersands, brackets and quotes to their HTML/XML entities.
def escape_html(string)
string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
end
@@ -267,11 +241,11 @@
nil
end
module_function :delete_cookie_header!
- # Return the bytesize of String; uses String#length under Ruby 1.8 and
+ # Return the bytesize of String; uses String#size under Ruby 1.8 and
# String#bytesize under 1.9.
if ''.respond_to?(:bytesize)
def bytesize(string)
string.bytesize
end
@@ -292,39 +266,47 @@
# weekday and month.
#
def rfc2822(time)
wday = Time::RFC2822_DAY_NAME[time.wday]
mon = Time::RFC2822_MONTH_NAME[time.mon - 1]
- time.strftime("#{wday}, %d-#{mon}-%Y %T GMT")
+ time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
end
module_function :rfc2822
- # 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
+ # Parses the "Range:" header, if present, into an array of Range objects.
+ # Returns nil if the header is missing or syntactically invalid.
+ # Returns an empty array if none of the ranges are satisfiable.
+ def byte_ranges(env, size)
+ # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
+ http_range = env['HTTP_RANGE']
+ return nil unless http_range
+ ranges = []
+ http_range.split(/,\s*/).each do |range_spec|
+ matches = range_spec.match(/bytes=(\d*)-(\d*)/)
+ return nil unless matches
+ r0,r1 = matches[1], matches[2]
+ if r0.empty?
+ return nil if r1.empty?
+ # suffix-byte-range-spec, represents trailing suffix of file
+ r0 = [size - r1.to_i, 0].max
+ r1 = size - 1
+ else
+ r0 = r0.to_i
+ if r1.empty?
+ r1 = size - 1
+ else
+ r1 = r1.to_i
+ return nil if r1 < r0 # backwards range is syntactically invalid
+ r1 = size-1 if r1 >= size
+ end
+ end
+ ranges << (r0..r1) if r0 <= r1
end
- else
- def bytesize(string)
- string.size
- end
+ ranges
end
- module_function :bytesize
+ module_function :byte_ranges
- # Constant time string comparison.
- def secure_compare(a, b)
- return false unless bytesize(a) == bytesize(b)
-
- l = a.unpack("C*")
-
- r, i = 0, -1
- b.each_byte { |v| r |= v ^ l[i+=1] }
- r == 0
- end
- module_function :secure_compare
-
# 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.
@@ -367,28 +349,23 @@
yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
end
end
def to_hash
- 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
+ Hash[*map do |k, v|
+ [k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v]
+ end.flatten]
end
def [](k)
- super(@names[k]) if @names[k]
- super(@names[k.downcase])
+ super(k) || super(@names[k.downcase])
end
def []=(k, v)
- delete k
- @names[k] = @names[k.downcase] = k
+ canonical = k.downcase
+ delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
+ @names[k] = @names[canonical] = k
super k, v
end
def delete(k)
canonical = k.downcase
@@ -482,258 +459,22 @@
}
# 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)
- SYMBOL_TO_STATUS_CODE = HTTP_STATUS_CODES.inject({}) { |hash, (code, message)|
- hash[message.downcase.gsub(/\s|-/, '_').to_sym] = code
- hash
- }
+ SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
+ [message.downcase.gsub(/\s|-/, '_').to_sym, code]
+ }.flatten]
def status_code(status)
if status.is_a?(Symbol)
SYMBOL_TO_STATUS_CODE[status] || 500
else
status.to_i
end
end
module_function :status_code
- # A multipart form data parser, adapted from IOWA.
- #
- # Usually, Rack::Request#POST takes care of calling this.
+ Multipart = Rack::Multipart
- module Multipart
- class UploadedFile
- # The filename, *not* including the path, of the "uploaded" file
- attr_reader :original_filename
-
- # The content type of the "uploaded" file
- attr_accessor :content_type
-
- def initialize(path, content_type = "text/plain", binary = false)
- raise "#{path} file does not exist" unless ::File.exist?(path)
- @content_type = content_type
- @original_filename = ::File.basename(path)
- @tempfile = Tempfile.new(@original_filename)
- @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
- @tempfile.binmode if binary
- FileUtils.copy_file(path, @tempfile.path)
- end
-
- def path
- @tempfile.path
- end
- alias_method :local_path, :path
-
- def method_missing(method_name, *args, &block) #:nodoc:
- @tempfile.__send__(method_name, *args, &block)
- end
- end
-
- EOL = "\r\n"
- MULTIPART_BOUNDARY = "AaB03x"
-
- def self.parse_multipart(env)
- unless env['CONTENT_TYPE'] =~
- %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n
- nil
- else
- boundary = "--#{$1}"
-
- params = {}
- buf = ""
- content_length = env['CONTENT_LENGTH'].to_i
- input = env['rack.input']
- input.rewind
-
- boundary_size = Utils.bytesize(boundary) + EOL.size
- bufsize = 16384
-
- content_length -= boundary_size
-
- read_buffer = ''
-
- status = input.read(boundary_size, read_buffer)
- raise EOFError, "bad content body" unless status == boundary + EOL
-
- rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/n
-
- max_key_space = Utils.key_space_limit
- bytes = 0
-
- loop {
- head = nil
- body = ''
- filename = content_type = name = nil
-
- until head && buf =~ rx
- if !head && i = buf.index(EOL+EOL)
- head = buf.slice!(0, i+2) # First \r\n
- buf.slice!(0, 2) # Second \r\n
-
- token = /[^\s()<>,;:\\"\/\[\]?=]+/
- condisp = /Content-Disposition:\s*#{token}\s*/i
- dispparm = /;\s*(#{token})=("(?:\\"|[^"])*"|#{token})/
-
- rfc2183 = /^#{condisp}(#{dispparm})+$/i
- broken_quoted = /^#{condisp}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{token}=)/i
- broken_unquoted = /^#{condisp}.*;\sfilename=(#{token})/i
-
- if head =~ rfc2183
- filename = Hash[head.scan(dispparm)]['filename']
- filename = $1 if filename and filename =~ /^"(.*)"$/
- elsif head =~ broken_quoted
- filename = $1
- elsif head =~ broken_unquoted
- filename = $1
- end
-
- if filename && filename !~ /\\[^\\"]/
- filename = Utils.unescape(filename).gsub(/\\(.)/, '\1')
- end
-
- content_type = head[/Content-Type: (.*)#{EOL}/ni, 1]
- name = head[/Content-Disposition:.*\s+name="?([^\";]*)"?/ni, 1] || head[/Content-ID:\s*([^#{EOL}]*)/ni, 1]
-
- if name
- bytes += name.size
- if bytes > max_key_space
- raise RangeError, "exceeded available parameter key space"
- end
- end
-
- if content_type || 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, read_buffer)
- 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 == ""
- # filename is blank which means no file has been selected
- data = nil
- elsif filename
- body.rewind
-
- # 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 = filename.split(/[\/\\]/).last
-
- data = {:filename => filename, :type => content_type,
- :name => name, :tempfile => body, :head => head}
- elsif !filename && content_type
- body.rewind
-
- # Generic multipart cases, not coming from a form
- data = {:type => content_type,
- :name => name, :tempfile => body, :head => head}
- else
- data = body
- end
-
- Utils.normalize_params(params, name, data) unless data.nil?
-
- # break if we're at the end of a buffer, but not if it is the end of a field
- break if (buf.empty? && $1 != EOL) || content_length == -1
- }
-
- input.rewind
-
- params
- end
- end
-
- def self.build_multipart(params, first = true)
- if first
- unless params.is_a?(Hash)
- raise ArgumentError, "value must be a Hash"
- end
-
- multipart = false
- query = lambda { |value|
- case value
- when Array
- value.each(&query)
- when Hash
- value.values.each(&query)
- when UploadedFile
- multipart = true
- end
- }
- params.values.each(&query)
- return nil unless multipart
- end
-
- flattened_params = Hash.new
-
- params.each do |key, value|
- k = first ? key.to_s : "[#{key}]"
-
- case value
- when Array
- value.map { |v|
- build_multipart(v, false).each { |subkey, subvalue|
- flattened_params["#{k}[]#{subkey}"] = subvalue
- }
- }
- when Hash
- build_multipart(value, false).each { |subkey, subvalue|
- flattened_params[k + subkey] = subvalue
- }
- else
- flattened_params[k] = value
- end
- end
-
- if first
- flattened_params.map { |name, file|
- if file.respond_to?(:original_filename)
- ::File.open(file.path, "rb") do |f|
- f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
-<<-EOF
---#{MULTIPART_BOUNDARY}\r
-Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
-Content-Type: #{file.content_type}\r
-Content-Length: #{::File.stat(file.path).size}\r
-\r
-#{f.read}\r
-EOF
- end
- else
-<<-EOF
---#{MULTIPART_BOUNDARY}\r
-Content-Disposition: form-data; name="#{name}"\r
-\r
-#{file}\r
-EOF
- end
- }.join + "--#{MULTIPART_BOUNDARY}--\r"
- else
- flattened_params
- end
- end
- end
end
end