Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge commit 'official/master'

  • Loading branch information...
commit 503130e322cb9390cba028a0ef58c1e24848ff21 2 parents 7b6046b + ca6f9f9
Christian Neukirchen chneukirchen authored
239 lib/rack/auth/openid.rb
View
@@ -1,13 +1,14 @@
-# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
+# AUTHOR: Scytrin dai Kinthra <scytrin@gmail.com>; blink#ruby-lang@irc.freenode.net
gem 'ruby-openid', '~> 2' if defined? Gem
require 'rack/request'
require 'rack/utils'
require 'rack/auth/abstract/handler'
+
require 'uri'
-require 'openid' #gem
-require 'openid/extension' #gem
-require 'openid/store/memory' #gem
+require 'openid'
+require 'openid/extension'
+require 'openid/store/memory'
module Rack
class Request
@@ -45,108 +46,111 @@ module Auth
#
# NOTE: Due to the amount of data that this library stores in the
# session, Rack::Session::Cookie may fault.
+ #
+ # == Examples
+ #
+ # simple_oid = OpenID.new('http://mysite.com/')
+ #
+ # return_oid = OpenID.new('http://mysite.com/', {
+ # :return_to => 'http://mysite.com/openid'
+ # })
+ #
+ # complex_oid = OpenID.new('http://mysite.com/',
+ # :immediate => true,
+ # :extensions => {
+ # ::OpenID::SReg => [['email'],['nickname']]
+ # }
+ # )
+ #
+ # = Advanced
+ #
+ # Most of the functionality of this library is encapsulated such that
+ # expansion and overriding functions isn't difficult nor tricky.
+ # Alternately, to avoid opening up singleton objects or subclassing, a
+ # wrapper rack middleware can be composed to act upon Auth::OpenID's
+ # responses. See #check and #finish for locations of pertinent data.
+ #
+ # == Responses
+ #
+ # To change the responses that Auth::OpenID returns, override the methods
+ # #redirect, #bad_request, #unauthorized, #access_denied, and
+ # #foreign_server_failure.
+ #
+ # Additionally #confirm_post_params is used when the URI would exceed
+ # length limits on a GET request when doing the initial verification
+ # request.
+ #
+ # == Processing
+ #
+ # To change methods of processing completed transactions, override the
+ # methods #success, #setup_needed, #cancel, and #failure. Please ensure
+ # the returned object is a rack compatible response.
+ #
+ # The first argument is an OpenID::Response, the second is a
+ # Rack::Request of the current request, the last is the hash used in
+ # ruby-openid handling, which can be found manually at
+ # env['rack.session'][:openid].
+ #
+ # This is useful if you wanted to expand the processing done, such as
+ # setting up user accounts.
+ #
+ # oid_app = Rack::Auth::OpenID.new realm, :return_to => return_to
+ # def oid_app.success oid, request, session
+ # user = Models::User[oid.identity_url]
+ # user ||= Models::User.create_from_openid oid
+ # request['rack.session'][:user] = user.id
+ # redirect MyApp.site_home
+ # end
+ #
+ # site_map['/openid'] = oid_app
+ # map = Rack::URLMap.new site_map
+ # ...
class OpenID
-
+ # Raised if an incompatible session is being used.
class NoSession < RuntimeError; end
+ # Raised if an extension not matching specifications is provided.
class BadExtension < RuntimeError; end
- # Required for ruby-openid
- ValidStatus = [:success, :setup_needed, :cancel, :failure]
+ # Possible statuses returned from consumer responses. See definitions
+ # in the ruby-openid library.
+ ValidStatus = [
+ ::OpenID::Consumer::SUCCESS,
+ ::OpenID::Consumer::FAILURE,
+ ::OpenID::Consumer::CANCEL,
+ ::OpenID::Consumer::SETUP_NEEDED
+ ]
- # = Arguments
- #
# The first argument is the realm, identifying the site they are trusting
# with their identity. This is required, also treated as the trust_root
# in OpenID 1.x exchanges.
#
- # The optional second argument is a hash of options.
- #
- # == Options
+ # The lits of acceptable options include :return_to, :session_key,
+ # :openid_param, :store, :immediate, :extensions.
#
# <tt>:return_to</tt> defines the url to return to after the client
# authenticates with the openid service provider. This url should point
- # to where Rack::Auth::OpenID is mounted. If <tt>:return_to</tt> is not
- # provided, return_to will be the current url which allows flexibility
- # with caveats.
+ # to where Rack::Auth::OpenID is mounted. If unprovided, the url of
+ # the current request is used.
#
# <tt>:session_key</tt> defines the key to the session hash in the env.
- # It defaults to 'rack.session'.
+ # The default is 'rack.session'.
#
# <tt>:openid_param</tt> defines at what key in the request parameters to
# find the identifier to resolve. As per the 2.0 spec, the default is
# 'openid_identifier'.
#
# <tt>:store</tt> defined what OpenID Store to use for persistant
- # information. By default a Store::Memory will be used.
+ # information. By default a Store::Memory is used.
#
# <tt>:immediate</tt> as true will make initial requests to be of an
# immediate type. This is false by default. See OpenID specification
# documentation.
#
# <tt>:extensions</tt> should be a hash of openid extension
- # implementations. The key should be the extension main module, the value
- # should be an array of arguments for extension::Request.new.
+ # implementations. The key should be the extension module, the value
+ # should be an array of arguments for extension::Request.new().
# The hash is iterated over and passed to #add_extension for processing.
# Please see #add_extension for further documentation.
- #
- # == Examples
- #
- # simple_oid = OpenID.new('http://mysite.com/')
- #
- # return_oid = OpenID.new('http://mysite.com/', {
- # :return_to => 'http://mysite.com/openid'
- # })
- #
- # complex_oid = OpenID.new('http://mysite.com/',
- # :immediate => true,
- # :extensions => {
- # ::OpenID::SReg => [['email'],['nickname']]
- # }
- # )
- #
- # = Advanced
- #
- # Most of the functionality of this library is encapsulated such that
- # expansion and overriding functions isn't difficult nor tricky.
- # Alternately, to avoid opening up singleton objects or subclassing, a
- # wrapper rack middleware can be composed to act upon Auth::OpenID's
- # responses. See #check and #finish for locations of pertinent data.
- #
- # == Responses
- #
- # To change the responses that Auth::OpenID returns, override the methods
- # #redirect, #bad_request, #unauthorized, #access_denied, and
- # #foreign_server_failure.
- #
- # Additionally #confirm_post_params is used when the URI would exceed
- # length limits on a GET request when doing the initial verification
- # request.
- #
- # == Processing
- #
- # To change methods of processing completed transactions, override the
- # methods #success, #setup_needed, #cancel, and #failure. Please ensure
- # the returned object is a rack compatible response.
- #
- # The first argument is an OpenID::Response, the second is a
- # Rack::Request of the current request, the last is the hash used in
- # ruby-openid handling, which can be found manually at
- # env['rack.session'][:openid].
- #
- # This is useful if you wanted to expand the processing done, such as
- # setting up user accounts.
- #
- # oid_app = Rack::Auth::OpenID.new realm, :return_to => return_to
- # def oid_app.success oid, request, session
- # user = Models::User[oid.identity_url]
- # user ||= Models::User.create_from_openid oid
- # request['rack.session'][:user] = user.id
- # redirect MyApp.site_home
- # end
- #
- # site_map['/openid'] = oid_app
- # map = Rack::URLMap.new site_map
- # ...
def initialize(realm, options={})
realm = URI(realm)
@@ -162,7 +166,7 @@ def initialize(realm, options={})
ruri = URI(ruri)
raise ArgumentError, "Invalid return_to: #{ruri}" \
unless ruri.absolute? \
- and ruri.scheme =~ /^https?$/ \
+ and ruri.scheme =~ /^https?$/ \
and ruri.fragment.nil?
raise ArgumentError, "return_to #{ruri} not within realm #{realm}" \
unless self.within_realm?(ruri)
@@ -174,10 +178,10 @@ def initialize(realm, options={})
@store = options[:store] || ::OpenID::Store::Memory.new
@immediate = !!options[:immediate]
- @extensions = {}
- if extensions = options.delete(:extensions)
+ @extensions = {}
+ if extensions = options[:extensions]
extensions.each do |ext, args|
- add_extension ext, *args
+ add_extension(ext, *args)
end
end
@@ -199,33 +203,29 @@ def initialize(realm, options={})
# If the parameter specified by <tt>options[:openid_param]</tt> is
# present, processing is passed to #check and the result is returned.
#
- # If neither of these conditions are met, #unauthorized is called.
+ # If neither of these conditions are met, #bad_request is called.
def call(env)
env['rack.auth.openid'] = self
env_session = env[@session_key]
unless env_session and env_session.is_a?(Hash)
- raise NoSession, 'No compatible session'
+ raise NoSession, 'No compatible session.'
end
# let us work in our own namespace...
session = (env_session[:openid] ||= {})
unless session and session.is_a?(Hash)
- raise NoSession, 'Incompatible openid session'
+ raise NoSession, 'Incompatible openid session.'
end
request = Rack::Request.new(env)
consumer = ::OpenID::Consumer.new(session, @store)
if mode = request.GET['openid.mode']
- if session.key?(:openid_param)
- finish(consumer, session, request)
- else
- bad_request
- end
+ finish(consumer, session, request)
elsif request.GET[@openid_param]
check(consumer, session, request)
else
- unauthorized
+ bad_request
end
end
@@ -263,14 +263,13 @@ def check(consumer, session, req)
immediate = session.key?(:setup_needed) ? false : immediate
if oid.send_redirect?(realm, return_to_uri, immediate)
- uri = oid.redirect_url(realm, return_to_uri, immediate)
- redirect(uri)
+ redirect(oid.redirect_url(realm, return_to_uri, immediate))
else
confirm_post_params(oid, realm, return_to_uri, immediate)
end
rescue ::OpenID::DiscoveryFailure => e
# thrown from inside OpenID::Consumer#begin by yadis stuff
- req.env['rack.errors'].puts([e.message, *e.backtrace]*"\n")
+ req.env['rack.errors'].puts( [e.message, *e.backtrace]*"\n" )
return foreign_server_failure
end
@@ -290,21 +289,24 @@ def finish(consumer, session, req)
req.env['rack.errors'].puts(oid.message)
p oid if $DEBUG
- raise unless ValidStatus.include?(oid.status)
- __send__(oid.status, oid, req, session)
+ if ValidStatus.include?(oid.status)
+ __send__(oid.status, oid, req, session)
+ else
+ invalid_status(oid, req, session)
+ end
end
# The first argument should be the main extension module.
# The extension module should contain the constants:
- # * class Request, should have OpenID::Extension as an ancestor
- # * class Response, should have OpenID::Extension as an ancestor
- # * string NS_URI, which defining the namespace of the extension
+ # * class Request, should have OpenID::Extension as an ancestor
+ # * class Response, should have OpenID::Extension as an ancestor
+ # * string NS_URI, which defining the namespace of the extension
#
# All trailing arguments will be passed to extension::Request.new in
# #check.
# The openid response will be passed to
- # extension::Response#from_success_response, #get_extension_args will be
- # called on the result to attain the gathered data.
+ # extension::Response#from_success_response, oid#get_extension_args will
+ # be called on the result to attain the gathered data.
#
# This method returns the key at which the response data will be found in
# the session, which is the namespace uri by default.
@@ -344,28 +346,27 @@ def within_realm? uri
return false unless uri.host.match(realm_match)
return true
end
+
alias_method :include?, :within_realm?
protected
- ### These methods define some of the boilerplate responses.
-
# Returns an html form page for posting to an Identity Provider if the
# GET request would exceed the upper URI length limit.
def confirm_post_params(oid, realm, return_to, immediate)
- Rack::Response.new.finish do |r|
- r.write '<html><head><title>Confirm...</title></head><body>'
- r.write oid.form_markup(realm, return_to, immediate)
- r.write '</body></html>'
- end
+ response = Rack::Response.new '<html>'+
+ '<head><title>Confirm...</title></head>'+
+ '<body>'+oid.form_markup(realm, return_to, immediate)+'</body>'+
+ '</html>'
+ response.finish
end
# Returns a 303 redirect with the destination of that provided by the
# argument.
def redirect(uri)
- [ 303, {'Content-Length'=>'0', 'Content-Type'=>'text/plain',
+ [ 303, {'Content-Type'=>'text/plain', 'Content-Length'=>'0',
'Location' => uri},
[] ]
end
@@ -401,10 +402,6 @@ def foreign_server_failure
private
- ### These methods are called after a transaction is completed, depending
- # on its outcome. These should all return a rack compatible response.
- # You'd want to override these to provide additional functionality.
-
# Called to complete processing on a successful transaction.
# Within the openid session, :openid_identity and :openid_identifier are
# set to the user friendly and the standard representation of the
@@ -430,7 +427,7 @@ def success(oid, request, session)
def setup_needed(oid, request, session)
identifier = session[:openid_param]
session[:setup_needed] = true
- redirect req.script_name + '?' + openid_param + '=' + identifier
+ redirect(req.script_name + '?' + openid_param + '=' + identifier)
end
# Called if the user indicates they wish to cancel identification.
@@ -448,6 +445,16 @@ def cancel(oid, request, session)
def failure(oid, request, session)
unauthorized
end
+
+ # To be called if there is no method for handling the OpenID response
+ # status.
+
+ def invalid_status(oid, request, session)
+ msg = 'Invalid status returned by the OpenID authorization reponse.'
+ [ 500,
+ {'Content-Type'=>'text/plain','Content-Length'=>msg.length.to_s},
+ [msg] ]
+ end
end
# A class developed out of the request to use OpenID as an authentication
@@ -472,8 +479,8 @@ def initialize(app, realm, options={}, &auth)
end
def call(env)
- to = auth.call(env) ? @app : @oid
- to.call env
+ to = @authenticator.call(env) ? @app : @oid
+ to.call(env)
end
end
end
2  lib/rack/deflater.rb
View
@@ -60,7 +60,7 @@ def each(&block)
@writer = block
gzip =::Zlib::GzipWriter.new(self)
gzip.mtime = @mtime
- @body.each { |part| gzip << part }
+ @body.each { |part| gzip.write(part) }
@body.close if @body.respond_to?(:close)
gzip.close
@writer = nil
21 lib/rack/handler/lsws.rb
View
@@ -15,14 +15,19 @@ def self.serve(app)
env = ENV.to_hash
env.delete "HTTP_CONTENT_LENGTH"
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
- env.update({"rack.version" => [1,0],
- "rack.input" => StringIO.new($stdin.read.to_s),
- "rack.errors" => $stderr,
- "rack.multithread" => false,
- "rack.multiprocess" => true,
- "rack.run_once" => false,
- "rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http"
- })
+
+ rack_input = RewindableInput.new($stdin.read.to_s)
+
+ env.update(
+ "rack.version" => [1,0],
+ "rack.input" => rack_input,
+ "rack.errors" => $stderr,
+ "rack.multithread" => false,
+ "rack.multiprocess" => true,
+ "rack.run_once" => false,
+ "rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http"
+ )
+
env["QUERY_STRING"] ||= ""
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
env["REQUEST_PATH"] ||= "/"
5 lib/rack/handler/mongrel.rb
View
@@ -45,8 +45,11 @@ def process(request, response)
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
+ rack_input = request.body || StringIO.new('')
+ rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
+
env.update({"rack.version" => [1,0],
- "rack.input" => request.body || StringIO.new(""),
+ "rack.input" => rack_input,
"rack.errors" => $stderr,
"rack.multithread" => true,
7 lib/rack/handler/scgi.rb
View
@@ -32,10 +32,13 @@ def process_request(request, input_body, socket)
env["PATH_INFO"] = env["REQUEST_PATH"]
env["QUERY_STRING"] ||= ""
env["SCRIPT_NAME"] = ""
+
+ rack_input = StringIO.new(input_body)
+ rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
+
env.update({"rack.version" => [1,0],
- "rack.input" => StringIO.new(input_body),
+ "rack.input" => rack_input,
"rack.errors" => $stderr,
-
"rack.multithread" => true,
"rack.multiprocess" => true,
"rack.run_once" => false,
6 lib/rack/handler/webrick.rb
View
@@ -6,6 +6,7 @@ module Rack
module Handler
class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet
def self.run(app, options={})
+ options[:BindAddress] = options.delete(:Host) if options[:Host]
server = ::WEBrick::HTTPServer.new(options)
server.mount "/", Rack::Handler::WEBrick, app
trap(:INT) { server.shutdown }
@@ -22,8 +23,11 @@ def service(req, res)
env = req.meta_vars
env.delete_if { |k, v| v.nil? }
+ rack_input = StringIO.new(req.body.to_s)
+ rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
+
env.update({"rack.version" => [1,0],
- "rack.input" => StringIO.new(req.body.to_s),
+ "rack.input" => rack_input,
"rack.errors" => $stderr,
"rack.multithread" => true,
11 lib/rack/lint.rb
View
@@ -233,8 +233,17 @@ def check_env(env)
## === The Input Stream
##
## The input stream is an IO-like object which contains the raw HTTP
- ## POST data. If it is a file then it must be opened in binary mode.
+ ## POST data.
def check_input(input)
+ ## When applicable, its external encoding must be "ASCII-8BIT" and it
+ ## must be opened in binary mode, for Ruby 1.9 compatibility.
+ assert("rack.input #{input} does not have ASCII-8BIT as its external encoding") {
+ input.external_encoding.name == "ASCII-8BIT"
+ } if input.respond_to?(:external_encoding)
+ assert("rack.input #{input} is not opened in binary mode") {
+ input.binmode?
+ } if input.respond_to?(:binmode?)
+
## The input stream must respond to +gets+, +each+, +read+ and +rewind+.
[:gets, :each, :read, :rewind].each { |method|
assert("rack.input #{input} does not respond to ##{method}") {
3  lib/rack/mime.rb
View
@@ -14,7 +14,7 @@ module Mime
# Rack::Mime::MIME_TYPES.fetch('.foo', 'application/octet-stream')
def mime_type(ext, fallback='application/octet-stream')
- MIME_TYPES.fetch(ext, fallback)
+ MIME_TYPES.fetch(ext.to_s.downcase, fallback)
end
module_function :mime_type
@@ -126,6 +126,7 @@ def mime_type(ext, fallback='application/octet-stream')
".ods" => "application/vnd.oasis.opendocument.spreadsheet",
".odt" => "application/vnd.oasis.opendocument.text",
".ogg" => "application/ogg",
+ ".ogv" => "video/ogg",
".p" => "text/x-pascal",
".pas" => "text/x-pascal",
".pbm" => "image/x-portable-bitmap",
11 lib/rack/mock.rb
View
@@ -114,13 +114,18 @@ def self.env_for(uri="", opts={})
end
end
- opts[:input] ||= ""
+ empty_str = ""
+ empty_str.force_encoding("ASCII-8BIT") if empty_str.respond_to? :force_encoding
+ opts[:input] ||= empty_str
if String === opts[:input]
- env["rack.input"] = StringIO.new(opts[:input])
+ rack_input = StringIO.new(opts[:input])
else
- env["rack.input"] = opts[:input]
+ rack_input = opts[:input]
end
+ rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
+ env['rack.input'] = rack_input
+
env["CONTENT_LENGTH"] ||= env["rack.input"].length.to_s
opts.each { |field, value|
9 lib/rack/reloader.rb
View
@@ -1,5 +1,6 @@
# Copyright (c) 2009 Michael Fellinger m.fellinger@gmail.com
-# All files in this distribution are subject to the terms of the Ruby license.
+# Rack::Reloader is subject to the terms of an MIT-style license.
+# See COPYING or http://www.opensource.org/licenses/mit-license.php.
require 'pathname'
@@ -70,7 +71,7 @@ def rotation
next if file =~ /\.(so|bundle)$/ # cannot reload compiled files
found, stat = figure_path(file, paths)
- next unless found and stat and mtime = stat.mtime
+ next unless found && stat && mtime = stat.mtime
@cache[file] = found
@@ -87,11 +88,13 @@ def figure_path(file, paths)
found, stat = safe_stat(found)
return found, stat if found
- paths.each do |possible_path|
+ paths.find do |possible_path|
path = ::File.join(possible_path, file)
found, stat = safe_stat(path)
return ::File.expand_path(found), stat if found
end
+
+ return false, false
end
def safe_stat(file)
2  lib/rack/request.rb
View
@@ -65,7 +65,7 @@ def content_charset
def host
# Remove port number.
- (@env["HTTP_HOST"] || @env["SERVER_NAME"]).gsub(/:\d+\z/, '')
+ (@env["HTTP_HOST"] || @env["SERVER_NAME"]).to_s.gsub(/:\d+\z/, '')
end
def script_name=(s); @env["SCRIPT_NAME"] = s.to_s end
38 lib/rack/response.rb
View
@@ -54,45 +54,11 @@ def []=(key, value)
end
def set_cookie(key, value)
- case value
- when Hash
- domain = "; domain=" + value[:domain] if value[:domain]
- path = "; path=" + value[:path] if value[:path]
- # According to RFC 2109, we need dashes here.
- # N.B.: cgi.rb uses spaces...
- expires = "; expires=" + value[:expires].clone.gmtime.
- strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
- secure = "; secure" if value[:secure]
- httponly = "; HttpOnly" if value[:httponly]
- value = value[:value]
- end
- value = [value] unless Array === value
- cookie = Utils.escape(key) + "=" +
- value.map { |v| Utils.escape v }.join("&") +
- "#{domain}#{path}#{expires}#{secure}#{httponly}"
-
- case self["Set-Cookie"]
- when Array
- self["Set-Cookie"] << cookie
- when String
- self["Set-Cookie"] = [self["Set-Cookie"], cookie]
- when nil
- self["Set-Cookie"] = cookie
- end
+ Utils.set_cookie_header!(header, key, value)
end
def delete_cookie(key, value={})
- unless Array === self["Set-Cookie"]
- self["Set-Cookie"] = [self["Set-Cookie"]].compact
- end
-
- self["Set-Cookie"].reject! { |cookie|
- cookie =~ /\A#{Utils.escape(key)}=/
- }
-
- set_cookie(key,
- {:value => '', :path => nil, :domain => nil,
- :expires => Time.at(0) }.merge(value))
+ Utils.delete_cookie_header!(header, key, value)
end
def redirect(target, status=302)
2  lib/rack/rewindable_input.rb
View
@@ -72,6 +72,8 @@ def make_rewindable
# access it because we have the file handle open.
@rewindable_io = Tempfile.new('RackRewindableInput')
@rewindable_io.chmod(0000)
+ @rewindable_io.set_encoding(Encoding::BINARY) if @rewindable_io.respond_to?(:set_encoding)
+ @rewindable_io.binmode
if filesystem_has_posix_semantics?
@rewindable_io.unlink
@unlinked = true
8 lib/rack/session/abstract/id.rb
View
@@ -107,18 +107,16 @@ def commit_session(env, status, headers, body)
if not session_id = set_session(env, session_id, session, options)
env["rack.errors"].puts("Warning! #{self.class.name} failed to save session. Content dropped.")
- [status, headers, body]
elsif options[:defer] and not options[:renew]
env["rack.errors"].puts("Defering cookie for #{session_id}") if $VERBOSE
- [status, headers, body]
else
cookie = Hash.new
cookie[:value] = session_id
cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil?
- response = Rack::Response.new(body, status, headers)
- response.set_cookie(@key, cookie.merge(options))
- response.to_a
+ Utils.set_cookie_header!(headers, @key, cookie.merge(options))
end
+
+ [status, headers, body]
end
# All thread safety and session retrival proceedures should occur here.
7 lib/rack/session/cookie.rb
View
@@ -70,16 +70,15 @@ def commit_session(env, status, headers, body)
if session_data.size > (4096 - @key.size)
env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K. Content dropped.")
- [status, headers, body]
else
options = env["rack.session.options"]
cookie = Hash.new
cookie[:value] = session_data
cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil?
- response = Rack::Response.new(body, status, headers)
- response.set_cookie(@key, cookie.merge(options))
- response.to_a
+ Utils.set_cookie_header!(headers, @key, cookie.merge(options))
end
+
+ [status, headers, body]
end
def generate_hmac(data)
80 lib/rack/utils.rb
View
@@ -13,7 +13,7 @@ module Utils
# 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
+ '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
}.tr(' ', '+')
end
module_function :escape
@@ -168,6 +168,54 @@ def select_best_encoding(available_encodings, accept_encoding)
end
module_function :select_best_encoding
+ def set_cookie_header!(header, key, value)
+ case value
+ when Hash
+ domain = "; domain=" + value[:domain] if value[:domain]
+ path = "; path=" + value[:path] if value[:path]
+ # According to RFC 2109, we need dashes here.
+ # N.B.: cgi.rb uses spaces...
+ expires = "; expires=" + value[:expires].clone.gmtime.
+ strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
+ secure = "; secure" if value[:secure]
+ httponly = "; HttpOnly" if value[:httponly]
+ value = value[:value]
+ end
+ value = [value] unless Array === value
+ cookie = escape(key) + "=" +
+ value.map { |v| escape v }.join("&") +
+ "#{domain}#{path}#{expires}#{secure}#{httponly}"
+
+ case header["Set-Cookie"]
+ when Array
+ header["Set-Cookie"] << cookie
+ when String
+ header["Set-Cookie"] = [header["Set-Cookie"], cookie]
+ when nil
+ header["Set-Cookie"] = cookie
+ end
+
+ nil
+ end
+ module_function :set_cookie_header!
+
+ def delete_cookie_header!(header, key, value = {})
+ unless Array === header["Set-Cookie"]
+ header["Set-Cookie"] = [header["Set-Cookie"]].compact
+ end
+
+ header["Set-Cookie"].reject! { |cookie|
+ cookie =~ /\A#{escape(key)}=/
+ }
+
+ set_cookie_header!(header, key,
+ {:value => '', :path => nil, :domain => nil,
+ :expires => Time.at(0) }.merge(value))
+
+ nil
+ end
+ module_function :delete_cookie_header!
+
# Return the bytesize of String; uses String#length under Ruby 1.8 and
# String#bytesize under 1.9.
if ''.respond_to?(:bytesize)
@@ -211,6 +259,7 @@ def context(env, app=@app)
# header when set.
class HeaderHash < Hash
def initialize(hash={})
+ super()
@names = {}
hash.each { |k, v| self[k] = v }
end
@@ -238,8 +287,9 @@ def []=(k, v)
def delete(k)
canonical = k.downcase
- super @names.delete(canonical)
+ result = super @names.delete(canonical)
@names.delete_if { |name,| name.downcase == canonical }
+ result
end
def include?(k)
@@ -259,13 +309,23 @@ def merge(other)
hash = dup
hash.merge! other
end
+
+ def replace(other)
+ clear
+ other.each { |k, v| self[k] = v }
+ self
+ end
end
# Every standard HTTP code mapped to the appropriate message.
- # Stolen from Mongrel.
+ # Generated with:
+ # curl -s http://www.iana.org/assignments/http-status-codes | \
+ # ruby -ane 'm = /^(\d{3}) +(\S[^\[(]+)/.match($_) and
+ # puts " #{m[1]} => \x27#{m[2].strip}x27,"'
HTTP_STATUS_CODES = {
100 => 'Continue',
101 => 'Switching Protocols',
+ 102 => 'Processing',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
@@ -273,12 +333,15 @@ def merge(other)
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
+ 207 => 'Multi-Status',
+ 226 => 'IM Used',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
+ 306 => 'Reserved',
307 => 'Temporary Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
@@ -294,16 +357,23 @@ def merge(other)
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
- 414 => 'Request-URI Too Large',
+ 414 => 'Request-URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Requested Range Not Satisfiable',
417 => 'Expectation Failed',
+ 422 => 'Unprocessable Entity',
+ 423 => 'Locked',
+ 424 => 'Failed Dependency',
+ 426 => 'Upgrade Required',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
- 505 => 'HTTP Version Not Supported'
+ 505 => 'HTTP Version Not Supported',
+ 506 => 'Variant Also Negotiates',
+ 507 => 'Insufficient Storage',
+ 510 => 'Not Extended',
}
# Responses with HTTP status codes that should not have an entity body
12 test/spec_rack_commonlogger.rb
View
@@ -46,4 +46,16 @@
res.errors.should.not.be.empty
res.errors.should =~ /"GET \/ " 200 - /
end
+
+ def length
+ self.class.length
+ end
+
+ def self.length
+ 123
+ end
+
+ def self.obj
+ "hello world"
+ end
end
36 test/spec_rack_lint.rb
View
@@ -110,6 +110,28 @@ def env(*args)
Rack::Lint.new(nil).call(env("rack.input" => ""))
}.should.raise(Rack::Lint::LintError).
message.should.match(/does not respond to #gets/)
+
+ lambda {
+ input = Object.new
+ def input.binmode?
+ false
+ end
+ Rack::Lint.new(nil).call(env("rack.input" => input))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/is not opened in binary mode/)
+
+ lambda {
+ input = Object.new
+ def input.external_encoding
+ result = Object.new
+ def result.name
+ "US-ASCII"
+ end
+ result
+ end
+ Rack::Lint.new(nil).call(env("rack.input" => input))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/does not have ASCII-8BIT as its external encoding/)
end
specify "notices error errors" do
@@ -432,46 +454,48 @@ def rewind
end
specify "passes valid read calls" do
+ hello_str = "hello world"
+ hello_str.force_encoding("ASCII-8BIT") if hello_str.respond_to? :force_encoding
lambda {
Rack::Lint.new(lambda { |env|
env["rack.input"].read
[201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
- }).call(env({"rack.input" => StringIO.new("hello world")}))
+ }).call(env({"rack.input" => StringIO.new(hello_str)}))
}.should.not.raise(Rack::Lint::LintError)
lambda {
Rack::Lint.new(lambda { |env|
env["rack.input"].read(0)
[201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
- }).call(env({"rack.input" => StringIO.new("hello world")}))
+ }).call(env({"rack.input" => StringIO.new(hello_str)}))
}.should.not.raise(Rack::Lint::LintError)
lambda {
Rack::Lint.new(lambda { |env|
env["rack.input"].read(1)
[201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
- }).call(env({"rack.input" => StringIO.new("hello world")}))
+ }).call(env({"rack.input" => StringIO.new(hello_str)}))
}.should.not.raise(Rack::Lint::LintError)
lambda {
Rack::Lint.new(lambda { |env|
env["rack.input"].read(nil)
[201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
- }).call(env({"rack.input" => StringIO.new("hello world")}))
+ }).call(env({"rack.input" => StringIO.new(hello_str)}))
}.should.not.raise(Rack::Lint::LintError)
lambda {
Rack::Lint.new(lambda { |env|
env["rack.input"].read(nil, '')
[201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
- }).call(env({"rack.input" => StringIO.new("hello world")}))
+ }).call(env({"rack.input" => StringIO.new(hello_str)}))
}.should.not.raise(Rack::Lint::LintError)
lambda {
Rack::Lint.new(lambda { |env|
env["rack.input"].read(1, '')
[201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
- }).call(env({"rack.input" => StringIO.new("hello world")}))
+ }).call(env({"rack.input" => StringIO.new(hello_str)}))
}.should.not.raise(Rack::Lint::LintError)
end
end
6 test/spec_rack_request.rb
View
@@ -37,6 +37,11 @@
req = Rack::Request.new \
Rack::MockRequest.env_for("/", "SERVER_NAME" => "example.org:9292")
req.host.should.equal "example.org"
+
+ env = Rack::MockRequest.env_for("/")
+ env.delete("SERVER_NAME")
+ req = Rack::Request.new(env)
+ req.host.should.equal ""
end
specify "can parse the query string" do
@@ -424,6 +429,7 @@
/9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\r
--AaB03x--\r
EOF
+ input.force_encoding("ASCII-8BIT") if input.respond_to? :force_encoding
res = Rack::MockRequest.new(Rack::Lint.new(app)).get "/",
"CONTENT_TYPE" => "multipart/form-data, boundary=AaB03x",
"CONTENT_LENGTH" => input.size.to_s, "rack.input" => StringIO.new(input)
40 test/spec_rack_utils.rb
View
@@ -12,6 +12,15 @@
should.equal "q1%212%22%27w%245%267%2Fz8%29%3F%5C"
end
+ specify "should escape correctly for multibyte characters" do
+ matz_name = "\xE3\x81\xBE\xE3\x81\xA4\xE3\x82\x82\xE3\x81\xA8".unpack("a*")[0] # Matsumoto
+ matz_name.force_encoding("UTF-8") if matz_name.respond_to? :force_encoding
+ Rack::Utils.escape(matz_name).should.equal '%E3%81%BE%E3%81%A4%E3%82%82%E3%81%A8'
+ matz_name_sep = "\xE3\x81\xBE\xE3\x81\xA4 \xE3\x82\x82\xE3\x81\xA8".unpack("a*")[0] # Matsu moto
+ matz_name_sep.force_encoding("UTF-8") if matz_name_sep.respond_to? :force_encoding
+ Rack::Utils.escape(matz_name_sep).should.equal '%E3%81%BE%E3%81%A4+%E3%82%82%E3%81%A8'
+ end
+
specify "should unescape correctly" do
Rack::Utils.unescape("fo%3Co%3Ebar").should.equal "fo<o>bar"
Rack::Utils.unescape("a+space").should.equal "a space"
@@ -228,6 +237,37 @@
h = Rack::Utils::HeaderHash.new("foo" => ["bar", "baz"])
h.to_hash.should.equal({ "foo" => "bar\nbaz" })
end
+
+ specify "should replace hashes correctly" do
+ h = Rack::Utils::HeaderHash.new("Foo-Bar" => "baz")
+ j = {"foo" => "bar"}
+ h.replace(j)
+ h["foo"].should.equal "bar"
+ end
+
+ specify "should be able to delete the given key case-sensitively" do
+ h = Rack::Utils::HeaderHash.new("foo" => "bar")
+ h.delete("foo")
+ h["foo"].should.be.nil
+ h["FOO"].should.be.nil
+ end
+
+ specify "should be able to delete the given key case-insensitively" do
+ h = Rack::Utils::HeaderHash.new("foo" => "bar")
+ h.delete("FOO")
+ h["foo"].should.be.nil
+ h["FOO"].should.be.nil
+ end
+
+ specify "should return the deleted value when #delete is called on an existing key" do
+ h = Rack::Utils::HeaderHash.new("foo" => "bar")
+ h.delete("Foo").should.equal("bar")
+ end
+
+ specify "should return nil when #delete is called on a non-existant key" do
+ h = Rack::Utils::HeaderHash.new("foo" => "bar")
+ h.delete("Hello").should.be.nil
+ end
end
context "Rack::Utils::Context" do
Please sign in to comment.
Something went wrong with that request. Please try again.