Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Merge commit 'official/master'

  • Loading branch information...
commit 503130e322cb9390cba028a0ef58c1e24848ff21 2 parents 7b6046b + ca6f9f9
Christian Neukirchen chneukirchen authored
239 lib/rack/auth/openid.rb
... ... @@ -1,13 +1,14 @@
1   -# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
  1 +# AUTHOR: Scytrin dai Kinthra <scytrin@gmail.com>; blink#ruby-lang@irc.freenode.net
2 2
3 3 gem 'ruby-openid', '~> 2' if defined? Gem
4 4 require 'rack/request'
5 5 require 'rack/utils'
6 6 require 'rack/auth/abstract/handler'
  7 +
7 8 require 'uri'
8   -require 'openid' #gem
9   -require 'openid/extension' #gem
10   -require 'openid/store/memory' #gem
  9 +require 'openid'
  10 +require 'openid/extension'
  11 +require 'openid/store/memory'
11 12
12 13 module Rack
13 14 class Request
@@ -45,108 +46,111 @@ module Auth
45 46 #
46 47 # NOTE: Due to the amount of data that this library stores in the
47 48 # session, Rack::Session::Cookie may fault.
  49 + #
  50 + # == Examples
  51 + #
  52 + # simple_oid = OpenID.new('http://mysite.com/')
  53 + #
  54 + # return_oid = OpenID.new('http://mysite.com/', {
  55 + # :return_to => 'http://mysite.com/openid'
  56 + # })
  57 + #
  58 + # complex_oid = OpenID.new('http://mysite.com/',
  59 + # :immediate => true,
  60 + # :extensions => {
  61 + # ::OpenID::SReg => [['email'],['nickname']]
  62 + # }
  63 + # )
  64 + #
  65 + # = Advanced
  66 + #
  67 + # Most of the functionality of this library is encapsulated such that
  68 + # expansion and overriding functions isn't difficult nor tricky.
  69 + # Alternately, to avoid opening up singleton objects or subclassing, a
  70 + # wrapper rack middleware can be composed to act upon Auth::OpenID's
  71 + # responses. See #check and #finish for locations of pertinent data.
  72 + #
  73 + # == Responses
  74 + #
  75 + # To change the responses that Auth::OpenID returns, override the methods
  76 + # #redirect, #bad_request, #unauthorized, #access_denied, and
  77 + # #foreign_server_failure.
  78 + #
  79 + # Additionally #confirm_post_params is used when the URI would exceed
  80 + # length limits on a GET request when doing the initial verification
  81 + # request.
  82 + #
  83 + # == Processing
  84 + #
  85 + # To change methods of processing completed transactions, override the
  86 + # methods #success, #setup_needed, #cancel, and #failure. Please ensure
  87 + # the returned object is a rack compatible response.
  88 + #
  89 + # The first argument is an OpenID::Response, the second is a
  90 + # Rack::Request of the current request, the last is the hash used in
  91 + # ruby-openid handling, which can be found manually at
  92 + # env['rack.session'][:openid].
  93 + #
  94 + # This is useful if you wanted to expand the processing done, such as
  95 + # setting up user accounts.
  96 + #
  97 + # oid_app = Rack::Auth::OpenID.new realm, :return_to => return_to
  98 + # def oid_app.success oid, request, session
  99 + # user = Models::User[oid.identity_url]
  100 + # user ||= Models::User.create_from_openid oid
  101 + # request['rack.session'][:user] = user.id
  102 + # redirect MyApp.site_home
  103 + # end
  104 + #
  105 + # site_map['/openid'] = oid_app
  106 + # map = Rack::URLMap.new site_map
  107 + # ...
48 108
49 109 class OpenID
50   -
  110 + # Raised if an incompatible session is being used.
51 111 class NoSession < RuntimeError; end
  112 + # Raised if an extension not matching specifications is provided.
52 113 class BadExtension < RuntimeError; end
53   - # Required for ruby-openid
54   - ValidStatus = [:success, :setup_needed, :cancel, :failure]
  114 + # Possible statuses returned from consumer responses. See definitions
  115 + # in the ruby-openid library.
  116 + ValidStatus = [
  117 + ::OpenID::Consumer::SUCCESS,
  118 + ::OpenID::Consumer::FAILURE,
  119 + ::OpenID::Consumer::CANCEL,
  120 + ::OpenID::Consumer::SETUP_NEEDED
  121 + ]
55 122
56   - # = Arguments
57   - #
58 123 # The first argument is the realm, identifying the site they are trusting
59 124 # with their identity. This is required, also treated as the trust_root
60 125 # in OpenID 1.x exchanges.
61 126 #
62   - # The optional second argument is a hash of options.
63   - #
64   - # == Options
  127 + # The lits of acceptable options include :return_to, :session_key,
  128 + # :openid_param, :store, :immediate, :extensions.
65 129 #
66 130 # <tt>:return_to</tt> defines the url to return to after the client
67 131 # authenticates with the openid service provider. This url should point
68   - # to where Rack::Auth::OpenID is mounted. If <tt>:return_to</tt> is not
69   - # provided, return_to will be the current url which allows flexibility
70   - # with caveats.
  132 + # to where Rack::Auth::OpenID is mounted. If unprovided, the url of
  133 + # the current request is used.
71 134 #
72 135 # <tt>:session_key</tt> defines the key to the session hash in the env.
73   - # It defaults to 'rack.session'.
  136 + # The default is 'rack.session'.
74 137 #
75 138 # <tt>:openid_param</tt> defines at what key in the request parameters to
76 139 # find the identifier to resolve. As per the 2.0 spec, the default is
77 140 # 'openid_identifier'.
78 141 #
79 142 # <tt>:store</tt> defined what OpenID Store to use for persistant
80   - # information. By default a Store::Memory will be used.
  143 + # information. By default a Store::Memory is used.
81 144 #
82 145 # <tt>:immediate</tt> as true will make initial requests to be of an
83 146 # immediate type. This is false by default. See OpenID specification
84 147 # documentation.
85 148 #
86 149 # <tt>:extensions</tt> should be a hash of openid extension
87   - # implementations. The key should be the extension main module, the value
88   - # should be an array of arguments for extension::Request.new.
  150 + # implementations. The key should be the extension module, the value
  151 + # should be an array of arguments for extension::Request.new().
89 152 # The hash is iterated over and passed to #add_extension for processing.
90 153 # Please see #add_extension for further documentation.
91   - #
92   - # == Examples
93   - #
94   - # simple_oid = OpenID.new('http://mysite.com/')
95   - #
96   - # return_oid = OpenID.new('http://mysite.com/', {
97   - # :return_to => 'http://mysite.com/openid'
98   - # })
99   - #
100   - # complex_oid = OpenID.new('http://mysite.com/',
101   - # :immediate => true,
102   - # :extensions => {
103   - # ::OpenID::SReg => [['email'],['nickname']]
104   - # }
105   - # )
106   - #
107   - # = Advanced
108   - #
109   - # Most of the functionality of this library is encapsulated such that
110   - # expansion and overriding functions isn't difficult nor tricky.
111   - # Alternately, to avoid opening up singleton objects or subclassing, a
112   - # wrapper rack middleware can be composed to act upon Auth::OpenID's
113   - # responses. See #check and #finish for locations of pertinent data.
114   - #
115   - # == Responses
116   - #
117   - # To change the responses that Auth::OpenID returns, override the methods
118   - # #redirect, #bad_request, #unauthorized, #access_denied, and
119   - # #foreign_server_failure.
120   - #
121   - # Additionally #confirm_post_params is used when the URI would exceed
122   - # length limits on a GET request when doing the initial verification
123   - # request.
124   - #
125   - # == Processing
126   - #
127   - # To change methods of processing completed transactions, override the
128   - # methods #success, #setup_needed, #cancel, and #failure. Please ensure
129   - # the returned object is a rack compatible response.
130   - #
131   - # The first argument is an OpenID::Response, the second is a
132   - # Rack::Request of the current request, the last is the hash used in
133   - # ruby-openid handling, which can be found manually at
134   - # env['rack.session'][:openid].
135   - #
136   - # This is useful if you wanted to expand the processing done, such as
137   - # setting up user accounts.
138   - #
139   - # oid_app = Rack::Auth::OpenID.new realm, :return_to => return_to
140   - # def oid_app.success oid, request, session
141   - # user = Models::User[oid.identity_url]
142   - # user ||= Models::User.create_from_openid oid
143   - # request['rack.session'][:user] = user.id
144   - # redirect MyApp.site_home
145   - # end
146   - #
147   - # site_map['/openid'] = oid_app
148   - # map = Rack::URLMap.new site_map
149   - # ...
150 154
151 155 def initialize(realm, options={})
152 156 realm = URI(realm)
@@ -162,7 +166,7 @@ def initialize(realm, options={})
162 166 ruri = URI(ruri)
163 167 raise ArgumentError, "Invalid return_to: #{ruri}" \
164 168 unless ruri.absolute? \
165   - and ruri.scheme =~ /^https?$/ \
  169 + and ruri.scheme =~ /^https?$/ \
166 170 and ruri.fragment.nil?
167 171 raise ArgumentError, "return_to #{ruri} not within realm #{realm}" \
168 172 unless self.within_realm?(ruri)
@@ -174,10 +178,10 @@ def initialize(realm, options={})
174 178 @store = options[:store] || ::OpenID::Store::Memory.new
175 179 @immediate = !!options[:immediate]
176 180
177   - @extensions = {}
178   - if extensions = options.delete(:extensions)
  181 + @extensions = {}
  182 + if extensions = options[:extensions]
179 183 extensions.each do |ext, args|
180   - add_extension ext, *args
  184 + add_extension(ext, *args)
181 185 end
182 186 end
183 187
@@ -199,33 +203,29 @@ def initialize(realm, options={})
199 203 # If the parameter specified by <tt>options[:openid_param]</tt> is
200 204 # present, processing is passed to #check and the result is returned.
201 205 #
202   - # If neither of these conditions are met, #unauthorized is called.
  206 + # If neither of these conditions are met, #bad_request is called.
203 207
204 208 def call(env)
205 209 env['rack.auth.openid'] = self
206 210 env_session = env[@session_key]
207 211 unless env_session and env_session.is_a?(Hash)
208   - raise NoSession, 'No compatible session'
  212 + raise NoSession, 'No compatible session.'
209 213 end
210 214 # let us work in our own namespace...
211 215 session = (env_session[:openid] ||= {})
212 216 unless session and session.is_a?(Hash)
213   - raise NoSession, 'Incompatible openid session'
  217 + raise NoSession, 'Incompatible openid session.'
214 218 end
215 219
216 220 request = Rack::Request.new(env)
217 221 consumer = ::OpenID::Consumer.new(session, @store)
218 222
219 223 if mode = request.GET['openid.mode']
220   - if session.key?(:openid_param)
221   - finish(consumer, session, request)
222   - else
223   - bad_request
224   - end
  224 + finish(consumer, session, request)
225 225 elsif request.GET[@openid_param]
226 226 check(consumer, session, request)
227 227 else
228   - unauthorized
  228 + bad_request
229 229 end
230 230 end
231 231
@@ -263,14 +263,13 @@ def check(consumer, session, req)
263 263 immediate = session.key?(:setup_needed) ? false : immediate
264 264
265 265 if oid.send_redirect?(realm, return_to_uri, immediate)
266   - uri = oid.redirect_url(realm, return_to_uri, immediate)
267   - redirect(uri)
  266 + redirect(oid.redirect_url(realm, return_to_uri, immediate))
268 267 else
269 268 confirm_post_params(oid, realm, return_to_uri, immediate)
270 269 end
271 270 rescue ::OpenID::DiscoveryFailure => e
272 271 # thrown from inside OpenID::Consumer#begin by yadis stuff
273   - req.env['rack.errors'].puts([e.message, *e.backtrace]*"\n")
  272 + req.env['rack.errors'].puts( [e.message, *e.backtrace]*"\n" )
274 273 return foreign_server_failure
275 274 end
276 275
@@ -290,21 +289,24 @@ def finish(consumer, session, req)
290 289 req.env['rack.errors'].puts(oid.message)
291 290 p oid if $DEBUG
292 291
293   - raise unless ValidStatus.include?(oid.status)
294   - __send__(oid.status, oid, req, session)
  292 + if ValidStatus.include?(oid.status)
  293 + __send__(oid.status, oid, req, session)
  294 + else
  295 + invalid_status(oid, req, session)
  296 + end
295 297 end
296 298
297 299 # The first argument should be the main extension module.
298 300 # The extension module should contain the constants:
299   - # * class Request, should have OpenID::Extension as an ancestor
300   - # * class Response, should have OpenID::Extension as an ancestor
301   - # * string NS_URI, which defining the namespace of the extension
  301 + # * class Request, should have OpenID::Extension as an ancestor
  302 + # * class Response, should have OpenID::Extension as an ancestor
  303 + # * string NS_URI, which defining the namespace of the extension
302 304 #
303 305 # All trailing arguments will be passed to extension::Request.new in
304 306 # #check.
305 307 # The openid response will be passed to
306   - # extension::Response#from_success_response, #get_extension_args will be
307   - # called on the result to attain the gathered data.
  308 + # extension::Response#from_success_response, oid#get_extension_args will
  309 + # be called on the result to attain the gathered data.
308 310 #
309 311 # This method returns the key at which the response data will be found in
310 312 # the session, which is the namespace uri by default.
@@ -344,28 +346,27 @@ def within_realm? uri
344 346 return false unless uri.host.match(realm_match)
345 347 return true
346 348 end
  349 +
347 350 alias_method :include?, :within_realm?
348 351
349 352 protected
350 353
351   - ### These methods define some of the boilerplate responses.
352   -
353 354 # Returns an html form page for posting to an Identity Provider if the
354 355 # GET request would exceed the upper URI length limit.
355 356
356 357 def confirm_post_params(oid, realm, return_to, immediate)
357   - Rack::Response.new.finish do |r|
358   - r.write '<html><head><title>Confirm...</title></head><body>'
359   - r.write oid.form_markup(realm, return_to, immediate)
360   - r.write '</body></html>'
361   - end
  358 + response = Rack::Response.new '<html>'+
  359 + '<head><title>Confirm...</title></head>'+
  360 + '<body>'+oid.form_markup(realm, return_to, immediate)+'</body>'+
  361 + '</html>'
  362 + response.finish
362 363 end
363 364
364 365 # Returns a 303 redirect with the destination of that provided by the
365 366 # argument.
366 367
367 368 def redirect(uri)
368   - [ 303, {'Content-Length'=>'0', 'Content-Type'=>'text/plain',
  369 + [ 303, {'Content-Type'=>'text/plain', 'Content-Length'=>'0',
369 370 'Location' => uri},
370 371 [] ]
371 372 end
@@ -401,10 +402,6 @@ def foreign_server_failure
401 402
402 403 private
403 404
404   - ### These methods are called after a transaction is completed, depending
405   - # on its outcome. These should all return a rack compatible response.
406   - # You'd want to override these to provide additional functionality.
407   -
408 405 # Called to complete processing on a successful transaction.
409 406 # Within the openid session, :openid_identity and :openid_identifier are
410 407 # set to the user friendly and the standard representation of the
@@ -430,7 +427,7 @@ def success(oid, request, session)
430 427 def setup_needed(oid, request, session)
431 428 identifier = session[:openid_param]
432 429 session[:setup_needed] = true
433   - redirect req.script_name + '?' + openid_param + '=' + identifier
  430 + redirect(req.script_name + '?' + openid_param + '=' + identifier)
434 431 end
435 432
436 433 # Called if the user indicates they wish to cancel identification.
@@ -448,6 +445,16 @@ def cancel(oid, request, session)
448 445 def failure(oid, request, session)
449 446 unauthorized
450 447 end
  448 +
  449 + # To be called if there is no method for handling the OpenID response
  450 + # status.
  451 +
  452 + def invalid_status(oid, request, session)
  453 + msg = 'Invalid status returned by the OpenID authorization reponse.'
  454 + [ 500,
  455 + {'Content-Type'=>'text/plain','Content-Length'=>msg.length.to_s},
  456 + [msg] ]
  457 + end
451 458 end
452 459
453 460 # A class developed out of the request to use OpenID as an authentication
@@ -472,8 +479,8 @@ def initialize(app, realm, options={}, &auth)
472 479 end
473 480
474 481 def call(env)
475   - to = auth.call(env) ? @app : @oid
476   - to.call env
  482 + to = @authenticator.call(env) ? @app : @oid
  483 + to.call(env)
477 484 end
478 485 end
479 486 end
2  lib/rack/deflater.rb
@@ -60,7 +60,7 @@ def each(&block)
60 60 @writer = block
61 61 gzip =::Zlib::GzipWriter.new(self)
62 62 gzip.mtime = @mtime
63   - @body.each { |part| gzip << part }
  63 + @body.each { |part| gzip.write(part) }
64 64 @body.close if @body.respond_to?(:close)
65 65 gzip.close
66 66 @writer = nil
21 lib/rack/handler/lsws.rb
@@ -15,14 +15,19 @@ def self.serve(app)
15 15 env = ENV.to_hash
16 16 env.delete "HTTP_CONTENT_LENGTH"
17 17 env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
18   - env.update({"rack.version" => [1,0],
19   - "rack.input" => StringIO.new($stdin.read.to_s),
20   - "rack.errors" => $stderr,
21   - "rack.multithread" => false,
22   - "rack.multiprocess" => true,
23   - "rack.run_once" => false,
24   - "rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http"
25   - })
  18 +
  19 + rack_input = RewindableInput.new($stdin.read.to_s)
  20 +
  21 + env.update(
  22 + "rack.version" => [1,0],
  23 + "rack.input" => rack_input,
  24 + "rack.errors" => $stderr,
  25 + "rack.multithread" => false,
  26 + "rack.multiprocess" => true,
  27 + "rack.run_once" => false,
  28 + "rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http"
  29 + )
  30 +
26 31 env["QUERY_STRING"] ||= ""
27 32 env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
28 33 env["REQUEST_PATH"] ||= "/"
5 lib/rack/handler/mongrel.rb
@@ -45,8 +45,11 @@ def process(request, response)
45 45
46 46 env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
47 47
  48 + rack_input = request.body || StringIO.new('')
  49 + rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
  50 +
48 51 env.update({"rack.version" => [1,0],
49   - "rack.input" => request.body || StringIO.new(""),
  52 + "rack.input" => rack_input,
50 53 "rack.errors" => $stderr,
51 54
52 55 "rack.multithread" => true,
7 lib/rack/handler/scgi.rb
@@ -32,10 +32,13 @@ def process_request(request, input_body, socket)
32 32 env["PATH_INFO"] = env["REQUEST_PATH"]
33 33 env["QUERY_STRING"] ||= ""
34 34 env["SCRIPT_NAME"] = ""
  35 +
  36 + rack_input = StringIO.new(input_body)
  37 + rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
  38 +
35 39 env.update({"rack.version" => [1,0],
36   - "rack.input" => StringIO.new(input_body),
  40 + "rack.input" => rack_input,
37 41 "rack.errors" => $stderr,
38   -
39 42 "rack.multithread" => true,
40 43 "rack.multiprocess" => true,
41 44 "rack.run_once" => false,
6 lib/rack/handler/webrick.rb
@@ -6,6 +6,7 @@ module Rack
6 6 module Handler
7 7 class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet
8 8 def self.run(app, options={})
  9 + options[:BindAddress] = options.delete(:Host) if options[:Host]
9 10 server = ::WEBrick::HTTPServer.new(options)
10 11 server.mount "/", Rack::Handler::WEBrick, app
11 12 trap(:INT) { server.shutdown }
@@ -22,8 +23,11 @@ def service(req, res)
22 23 env = req.meta_vars
23 24 env.delete_if { |k, v| v.nil? }
24 25
  26 + rack_input = StringIO.new(req.body.to_s)
  27 + rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
  28 +
25 29 env.update({"rack.version" => [1,0],
26   - "rack.input" => StringIO.new(req.body.to_s),
  30 + "rack.input" => rack_input,
27 31 "rack.errors" => $stderr,
28 32
29 33 "rack.multithread" => true,
11 lib/rack/lint.rb
@@ -233,8 +233,17 @@ def check_env(env)
233 233 ## === The Input Stream
234 234 ##
235 235 ## The input stream is an IO-like object which contains the raw HTTP
236   - ## POST data. If it is a file then it must be opened in binary mode.
  236 + ## POST data.
237 237 def check_input(input)
  238 + ## When applicable, its external encoding must be "ASCII-8BIT" and it
  239 + ## must be opened in binary mode, for Ruby 1.9 compatibility.
  240 + assert("rack.input #{input} does not have ASCII-8BIT as its external encoding") {
  241 + input.external_encoding.name == "ASCII-8BIT"
  242 + } if input.respond_to?(:external_encoding)
  243 + assert("rack.input #{input} is not opened in binary mode") {
  244 + input.binmode?
  245 + } if input.respond_to?(:binmode?)
  246 +
238 247 ## The input stream must respond to +gets+, +each+, +read+ and +rewind+.
239 248 [:gets, :each, :read, :rewind].each { |method|
240 249 assert("rack.input #{input} does not respond to ##{method}") {
3  lib/rack/mime.rb
@@ -14,7 +14,7 @@ module Mime
14 14 # Rack::Mime::MIME_TYPES.fetch('.foo', 'application/octet-stream')
15 15
16 16 def mime_type(ext, fallback='application/octet-stream')
17   - MIME_TYPES.fetch(ext, fallback)
  17 + MIME_TYPES.fetch(ext.to_s.downcase, fallback)
18 18 end
19 19 module_function :mime_type
20 20
@@ -126,6 +126,7 @@ def mime_type(ext, fallback='application/octet-stream')
126 126 ".ods" => "application/vnd.oasis.opendocument.spreadsheet",
127 127 ".odt" => "application/vnd.oasis.opendocument.text",
128 128 ".ogg" => "application/ogg",
  129 + ".ogv" => "video/ogg",
129 130 ".p" => "text/x-pascal",
130 131 ".pas" => "text/x-pascal",
131 132 ".pbm" => "image/x-portable-bitmap",
11 lib/rack/mock.rb
@@ -114,13 +114,18 @@ def self.env_for(uri="", opts={})
114 114 end
115 115 end
116 116
117   - opts[:input] ||= ""
  117 + empty_str = ""
  118 + empty_str.force_encoding("ASCII-8BIT") if empty_str.respond_to? :force_encoding
  119 + opts[:input] ||= empty_str
118 120 if String === opts[:input]
119   - env["rack.input"] = StringIO.new(opts[:input])
  121 + rack_input = StringIO.new(opts[:input])
120 122 else
121   - env["rack.input"] = opts[:input]
  123 + rack_input = opts[:input]
122 124 end
123 125
  126 + rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
  127 + env['rack.input'] = rack_input
  128 +
124 129 env["CONTENT_LENGTH"] ||= env["rack.input"].length.to_s
125 130
126 131 opts.each { |field, value|
9 lib/rack/reloader.rb
... ... @@ -1,5 +1,6 @@
1 1 # Copyright (c) 2009 Michael Fellinger m.fellinger@gmail.com
2   -# All files in this distribution are subject to the terms of the Ruby license.
  2 +# Rack::Reloader is subject to the terms of an MIT-style license.
  3 +# See COPYING or http://www.opensource.org/licenses/mit-license.php.
3 4
4 5 require 'pathname'
5 6
@@ -70,7 +71,7 @@ def rotation
70 71 next if file =~ /\.(so|bundle)$/ # cannot reload compiled files
71 72
72 73 found, stat = figure_path(file, paths)
73   - next unless found and stat and mtime = stat.mtime
  74 + next unless found && stat && mtime = stat.mtime
74 75
75 76 @cache[file] = found
76 77
@@ -87,11 +88,13 @@ def figure_path(file, paths)
87 88 found, stat = safe_stat(found)
88 89 return found, stat if found
89 90
90   - paths.each do |possible_path|
  91 + paths.find do |possible_path|
91 92 path = ::File.join(possible_path, file)
92 93 found, stat = safe_stat(path)
93 94 return ::File.expand_path(found), stat if found
94 95 end
  96 +
  97 + return false, false
95 98 end
96 99
97 100 def safe_stat(file)
2  lib/rack/request.rb
@@ -65,7 +65,7 @@ def content_charset
65 65
66 66 def host
67 67 # Remove port number.
68   - (@env["HTTP_HOST"] || @env["SERVER_NAME"]).gsub(/:\d+\z/, '')
  68 + (@env["HTTP_HOST"] || @env["SERVER_NAME"]).to_s.gsub(/:\d+\z/, '')
69 69 end
70 70
71 71 def script_name=(s); @env["SCRIPT_NAME"] = s.to_s end
38 lib/rack/response.rb
@@ -54,45 +54,11 @@ def []=(key, value)
54 54 end
55 55
56 56 def set_cookie(key, value)
57   - case value
58   - when Hash
59   - domain = "; domain=" + value[:domain] if value[:domain]
60   - path = "; path=" + value[:path] if value[:path]
61   - # According to RFC 2109, we need dashes here.
62   - # N.B.: cgi.rb uses spaces...
63   - expires = "; expires=" + value[:expires].clone.gmtime.
64   - strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
65   - secure = "; secure" if value[:secure]
66   - httponly = "; HttpOnly" if value[:httponly]
67   - value = value[:value]
68   - end
69   - value = [value] unless Array === value
70   - cookie = Utils.escape(key) + "=" +
71   - value.map { |v| Utils.escape v }.join("&") +
72   - "#{domain}#{path}#{expires}#{secure}#{httponly}"
73   -
74   - case self["Set-Cookie"]
75   - when Array
76   - self["Set-Cookie"] << cookie
77   - when String
78   - self["Set-Cookie"] = [self["Set-Cookie"], cookie]
79   - when nil
80   - self["Set-Cookie"] = cookie
81   - end
  57 + Utils.set_cookie_header!(header, key, value)
82 58 end
83 59
84 60 def delete_cookie(key, value={})
85   - unless Array === self["Set-Cookie"]
86   - self["Set-Cookie"] = [self["Set-Cookie"]].compact
87   - end
88   -
89   - self["Set-Cookie"].reject! { |cookie|
90   - cookie =~ /\A#{Utils.escape(key)}=/
91   - }
92   -
93   - set_cookie(key,
94   - {:value => '', :path => nil, :domain => nil,
95   - :expires => Time.at(0) }.merge(value))
  61 + Utils.delete_cookie_header!(header, key, value)
96 62 end
97 63
98 64 def redirect(target, status=302)
2  lib/rack/rewindable_input.rb
@@ -72,6 +72,8 @@ def make_rewindable
72 72 # access it because we have the file handle open.
73 73 @rewindable_io = Tempfile.new('RackRewindableInput')
74 74 @rewindable_io.chmod(0000)
  75 + @rewindable_io.set_encoding(Encoding::BINARY) if @rewindable_io.respond_to?(:set_encoding)
  76 + @rewindable_io.binmode
75 77 if filesystem_has_posix_semantics?
76 78 @rewindable_io.unlink
77 79 @unlinked = true
8 lib/rack/session/abstract/id.rb
@@ -107,18 +107,16 @@ def commit_session(env, status, headers, body)
107 107
108 108 if not session_id = set_session(env, session_id, session, options)
109 109 env["rack.errors"].puts("Warning! #{self.class.name} failed to save session. Content dropped.")
110   - [status, headers, body]
111 110 elsif options[:defer] and not options[:renew]
112 111 env["rack.errors"].puts("Defering cookie for #{session_id}") if $VERBOSE
113   - [status, headers, body]
114 112 else
115 113 cookie = Hash.new
116 114 cookie[:value] = session_id
117 115 cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil?
118   - response = Rack::Response.new(body, status, headers)
119   - response.set_cookie(@key, cookie.merge(options))
120   - response.to_a
  116 + Utils.set_cookie_header!(headers, @key, cookie.merge(options))
121 117 end
  118 +
  119 + [status, headers, body]
122 120 end
123 121
124 122 # All thread safety and session retrival proceedures should occur here.
7 lib/rack/session/cookie.rb
@@ -70,16 +70,15 @@ def commit_session(env, status, headers, body)
80 lib/rack/utils.rb
@@ -13,7 +13,7 @@ module Utils
13 13 # version since it's faster. (Stolen from Camping).
14 14 def escape(s)
15 15 s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
16   - '%'+$1.unpack('H2'*$1.size).join('%').upcase
  16 + '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
17 17 }.tr(' ', '+')
18 18 end
19 19 module_function :escape
@@ -168,6 +168,54 @@ def select_best_encoding(available_encodings, accept_encoding)
168 168 end
169 169 module_function :select_best_encoding
170 170
  171 + def set_cookie_header!(header, key, value)
  172 + case value
  173 + when Hash
  174 + domain = "; domain=" + value[:domain] if value[:domain]
  175 + path = "; path=" + value[:path] if value[:path]
  176 + # According to RFC 2109, we need dashes here.
  177 + # N.B.: cgi.rb uses spaces...
  178 + expires = "; expires=" + value[:expires].clone.gmtime.
  179 + strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
  180 + secure = "; secure" if value[:secure]
  181 + httponly = "; HttpOnly" if value[:httponly]
  182 + value = value[:value]
  183 + end
  184 + value = [value] unless Array === value
  185 + cookie = escape(key) + "=" +
  186 + value.map { |v| escape v }.join("&") +
  187 + "#{domain}#{path}#{expires}#{secure}#{httponly}"
  188 +
  189 + case header["Set-Cookie"]
  190 + when Array
  191 + header["Set-Cookie"] << cookie
  192 + when String
  193 + header["Set-Cookie"] = [header["Set-Cookie"], cookie]
  194 + when nil
  195 + header["Set-Cookie"] = cookie
  196 + end
  197 +
  198 + nil
  199 + end
  200 + module_function :set_cookie_header!
  201 +
  202 + def delete_cookie_header!(header, key, value = {})
  203 + unless Array === header["Set-Cookie"]
  204 + header["Set-Cookie"] = [header["Set-Cookie"]].compact
  205 + end
  206 +
  207 + header["Set-Cookie"].reject! { |cookie|
  208 + cookie =~ /\A#{escape(key)}=/
  209 + }
  210 +
  211 + set_cookie_header!(header, key,
  212 + {:value => '', :path => nil, :domain => nil,
  213 + :expires => Time.at(0) }.merge(value))
  214 +
  215 + nil
  216 + end
  217 + module_function :delete_cookie_header!
  218 +
171 219 # Return the bytesize of String; uses String#length under Ruby 1.8 and
172 220 # String#bytesize under 1.9.
173 221 if ''.respond_to?(:bytesize)
@@ -211,6 +259,7 @@ def context(env, app=@app)
211 259 # header when set.
212 260 class HeaderHash < Hash
213 261 def initialize(hash={})
  262 + super()
214 263 @names = {}
215 264 hash.each { |k, v| self[k] = v }
216 265 end
@@ -238,8 +287,9 @@ def []=(k, v)
238 287
239 288 def delete(k)
240 289 canonical = k.downcase
241   - super @names.delete(canonical)
  290 + result = super @names.delete(canonical)
242 291 @names.delete_if { |name,| name.downcase == canonical }
  292 + result
243 293 end
244 294
245 295 def include?(k)
@@ -259,13 +309,23 @@ def merge(other)
259 309 hash = dup
260 310 hash.merge! other
261 311 end
  312 +
  313 + def replace(other)
  314 + clear
  315 + other.each { |k, v| self[k] = v }
  316 + self
  317 + end
262 318 end
263 319
264 320 # Every standard HTTP code mapped to the appropriate message.
265   - # Stolen from Mongrel.
  321 + # Generated with:
  322 + # curl -s http://www.iana.org/assignments/http-status-codes | \
  323 + # ruby -ane 'm = /^(\d{3}) +(\S[^\[(]+)/.match($_) and
  324 + # puts " #{m[1]} => \x27#{m[2].strip}x27,"'
266 325 HTTP_STATUS_CODES = {
267 326 100 => 'Continue',
268 327 101 => 'Switching Protocols',
  328 + 102 => 'Processing',
269 329 200 => 'OK',
270 330 201 => 'Created',
271 331 202 => 'Accepted',
@@ -273,12 +333,15 @@ def merge(other)
273 333 204 => 'No Content',
274 334 205 => 'Reset Content',
275 335 206 => 'Partial Content',
  336 + 207 => 'Multi-Status',
  337 + 226 => 'IM Used',
276 338 300 => 'Multiple Choices',
277 339 301 => 'Moved Permanently',
278 340 302 => 'Found',
279 341 303 => 'See Other',
280 342 304 => 'Not Modified',
281 343 305 => 'Use Proxy',
  344 + 306 => 'Reserved',
282 345 307 => 'Temporary Redirect',
283 346 400 => 'Bad Request',
284 347 401 => 'Unauthorized',
@@ -294,16 +357,23 @@ def merge(other)
294 357 411 => 'Length Required',
295 358 412 => 'Precondition Failed',
296 359 413 => 'Request Entity Too Large',
297   - 414 => 'Request-URI Too Large',
  360 + 414 => 'Request-URI Too Long',
298 361 415 => 'Unsupported Media Type',
299 362 416 => 'Requested Range Not Satisfiable',
300 363 417 => 'Expectation Failed',
  364 + 422 => 'Unprocessable Entity',
  365 + 423 => 'Locked',
  366 + 424 => 'Failed Dependency',
  367 + 426 => 'Upgrade Required',
301 368 500 => 'Internal Server Error',
302 369 501 => 'Not Implemented',
303 370 502 => 'Bad Gateway',
304 371 503 => 'Service Unavailable',
305 372 504 => 'Gateway Timeout',
306   - 505 => 'HTTP Version Not Supported'
  373 + 505 => 'HTTP Version Not Supported',
  374 + 506 => 'Variant Also Negotiates',
  375 + 507 => 'Insufficient Storage',
  376 + 510 => 'Not Extended',
307 377 }
308 378
309 379 # Responses with HTTP status codes that should not have an entity body
12 test/spec_rack_commonlogger.rb
@@ -46,4 +46,16 @@
46 46 res.errors.should.not.be.empty
47 47 res.errors.should =~ /"GET \/ " 200 - /
48 48 end
  49 +
  50 + def length
  51 + self.class.length
  52 + end
  53 +
  54 + def self.length
  55 + 123
  56 + end
  57 +
  58 + def self.obj
  59 + "hello world"
  60 + end
49 61 end
36 test/spec_rack_lint.rb
@@ -110,6 +110,28 @@ def env(*args)
110 110 Rack::Lint.new(nil).call(env("rack.input" => ""))
111 111 }.should.raise(Rack::Lint::LintError).
112 112 message.should.match(/does not respond to #gets/)
  113 +
  114 + lambda {
  115 + input = Object.new
  116 + def input.binmode?
  117 + false
  118 + end
  119 + Rack::Lint.new(nil).call(env("rack.input" => input))
  120 + }.should.raise(Rack::Lint::LintError).
  121 + message.should.match(/is not opened in binary mode/)
  122 +
  123 + lambda {
  124 + input = Object.new
  125 + def input.external_encoding
  126 + result = Object.new
  127 + def result.name
  128 + "US-ASCII"
  129 + end
  130 + result
  131 + end
  132 + Rack::Lint.new(nil).call(env("rack.input" => input))
  133 + }.should.raise(Rack::Lint::LintError).
  134 + message.should.match(/does not have ASCII-8BIT as its external encoding/)
113 135 end
114 136
115 137 specify "notices error errors" do
@@ -432,46 +454,48 @@ def rewind
432 454 end
433 455
434 456 specify "passes valid read calls" do
  457 + hello_str = "hello world"
  458 + hello_str.force_encoding("ASCII-8BIT") if hello_str.respond_to? :force_encoding
435 459 lambda {
436 460 Rack::Lint.new(lambda { |env|
437 461 env["rack.input"].read
438 462 [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
439   - }).call(env({"rack.input" => StringIO.new("hello world")}))
  463 + }).call(env({"rack.input" => StringIO.new(hello_str)}))
440 464 }.should.not.raise(Rack::Lint::LintError)
441 465
442 466 lambda {
443 467 Rack::Lint.new(lambda { |env|
444 468 env["rack.input"].read(0)
445 469 [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
446   - }).call(env({"rack.input" => StringIO.new("hello world")}))
  470 + }).call(env({"rack.input" => StringIO.new(hello_str)}))
447 471 }.should.not.raise(Rack::Lint::LintError)
448 472
449 473 lambda {
450 474 Rack::Lint.new(lambda { |env|
451 475 env["rack.input"].read(1)
452 476 [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
453   - }).call(env({"rack.input" => StringIO.new("hello world")}))
  477 + }).call(env({"rack.input" => StringIO.new(hello_str)}))
454 478 }.should.not.raise(Rack::Lint::LintError)
455 479
456 480 lambda {
457 481 Rack::Lint.new(lambda { |env|
458 482 env["rack.input"].read(nil)
459 483 [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
460   - }).call(env({"rack.input" => StringIO.new("hello world")}))
  484 + }).call(env({"rack.input" => StringIO.new(hello_str)}))
461 485 }.should.not.raise(Rack::Lint::LintError)
462 486
463 487 lambda {
464 488 Rack::Lint.new(lambda { |env|
465 489 env["rack.input"].read(nil, '')
466 490 [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
467   - }).call(env({"rack.input" => StringIO.new("hello world")}))
  491 + }).call(env({"rack.input" => StringIO.new(hello_str)}))
468 492 }.should.not.raise(Rack::Lint::LintError)
469 493
470 494 lambda {
471 495 Rack::Lint.new(lambda { |env|
472 496 env["rack.input"].read(1, '')
473 497 [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
474   - }).call(env({"rack.input" => StringIO.new("hello world")}))
  498 + }).call(env({"rack.input" => StringIO.new(hello_str)}))
475 499 }.should.not.raise(Rack::Lint::LintError)
476 500 end
477 501 end
6 test/spec_rack_request.rb
@@ -37,6 +37,11 @@
37 37 req = Rack::Request.new \
38 38 Rack::MockRequest.env_for("/", "SERVER_NAME" => "example.org:9292")
39 39 req.host.should.equal "example.org"
  40 +
  41 + env = Rack::MockRequest.env_for("/")
  42 + env.delete("SERVER_NAME")
  43 + req = Rack::Request.new(env)
  44 + req.host.should.equal ""
40 45 end
41 46
42 47 specify "can parse the query string" do
@@ -424,6 +429,7 @@
424 429 /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\r
425 430 --AaB03x--\r
426 431 EOF
  432 + input.force_encoding("ASCII-8BIT") if input.respond_to? :force_encoding
427 433 res = Rack::MockRequest.new(Rack::Lint.new(app)).get "/",
428 434 "CONTENT_TYPE" => "multipart/form-data, boundary=AaB03x",
429 435 "CONTENT_LENGTH" => input.size.to_s, "rack.input" => StringIO.new(input)
40 test/spec_rack_utils.rb
@@ -12,6 +12,15 @@
12 12 should.equal "q1%212%22%27w%245%267%2Fz8%29%3F%5C"
13 13 end
14 14
  15 + specify "should escape correctly for multibyte characters" do
  16 + matz_name = "\xE3\x81\xBE\xE3\x81\xA4\xE3\x82\x82\xE3\x81\xA8".unpack("a*")[0] # Matsumoto
  17 + matz_name.force_encoding("UTF-8") if matz_name.respond_to? :force_encoding
  18 + Rack::Utils.escape(matz_name).should.equal '%E3%81%BE%E3%81%A4%E3%82%82%E3%81%A8'
  19 + matz_name_sep = "\xE3\x81\xBE\xE3\x81\xA4 \xE3\x82\x82\xE3\x81\xA8".unpack("a*")[0] # Matsu moto
  20 + matz_name_sep.force_encoding("UTF-8") if matz_name_sep.respond_to? :force_encoding
  21 + Rack::Utils.escape(matz_name_sep).should.equal '%E3%81%BE%E3%81%A4+%E3%82%82%E3%81%A8'
  22 + end
  23 +
15 24 specify "should unescape correctly" do
16 25 Rack::Utils.unescape("fo%3Co%3Ebar").should.equal "fo<o>bar"
17 26 Rack::Utils.unescape("a+space").should.equal "a space"
@@ -228,6 +237,37 @@
228 237 h = Rack::Utils::HeaderHash.new("foo" => ["bar", "baz"])
229 238 h.to_hash.should.equal({ "foo" => "bar\nbaz" })
230 239 end
  240 +
  241 + specify "should replace hashes correctly" do
  242 + h = Rack::Utils::HeaderHash.new("Foo-Bar" => "baz")
  243 + j = {"foo" => "bar"}
  244 + h.replace(j)
  245 + h["foo"].should.equal "bar"
  246 + end
  247 +
  248 + specify "should be able to delete the given key case-sensitively" do
  249 + h = Rack::Utils::HeaderHash.new("foo" => "bar")
  250 + h.delete("foo")
  251 + h["foo"].should.be.nil
  252 + h["FOO"].should.be.nil
  253 + end
  254 +
  255 + specify "should be able to delete the given key case-insensitively" do
  256 + h = Rack::Utils::HeaderHash.new("foo" => "bar")
  257 + h.delete("FOO")
  258 + h["foo"].should.be.nil
  259 + h["FOO"].should.be.nil
  260 + end
  261 +
  262 + specify "should return the deleted value when #delete is called on an existing key" do
  263 + h = Rack::Utils::HeaderHash.new("foo" => "bar")
  264 + h.delete("Foo").should.equal("bar")
  265 + end
  266 +
  267 + specify "should return nil when #delete is called on a non-existant key" do
  268 + h = Rack::Utils::HeaderHash.new("foo" => "bar")
  269 + h.delete("Hello").should.be.nil
  270 + end
231 271 end
232 272
233 273 context "Rack::Utils::Context" do

0 comments on commit 503130e

Please sign in to comment.
Something went wrong with that request. Please try again.