eac-rack 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. data/COPYING +18 -0
  2. data/KNOWN-ISSUES +21 -0
  3. data/README +399 -0
  4. data/bin/rackup +4 -0
  5. data/contrib/rack_logo.svg +111 -0
  6. data/example/lobster.ru +4 -0
  7. data/example/protectedlobster.rb +14 -0
  8. data/example/protectedlobster.ru +8 -0
  9. data/lib/rack.rb +92 -0
  10. data/lib/rack/adapter/camping.rb +22 -0
  11. data/lib/rack/auth/abstract/handler.rb +37 -0
  12. data/lib/rack/auth/abstract/request.rb +37 -0
  13. data/lib/rack/auth/basic.rb +58 -0
  14. data/lib/rack/auth/digest/md5.rb +124 -0
  15. data/lib/rack/auth/digest/nonce.rb +51 -0
  16. data/lib/rack/auth/digest/params.rb +55 -0
  17. data/lib/rack/auth/digest/request.rb +40 -0
  18. data/lib/rack/builder.rb +80 -0
  19. data/lib/rack/cascade.rb +41 -0
  20. data/lib/rack/chunked.rb +49 -0
  21. data/lib/rack/commonlogger.rb +49 -0
  22. data/lib/rack/conditionalget.rb +47 -0
  23. data/lib/rack/config.rb +15 -0
  24. data/lib/rack/content_length.rb +29 -0
  25. data/lib/rack/content_type.rb +23 -0
  26. data/lib/rack/deflater.rb +96 -0
  27. data/lib/rack/directory.rb +157 -0
  28. data/lib/rack/etag.rb +23 -0
  29. data/lib/rack/file.rb +90 -0
  30. data/lib/rack/handler.rb +88 -0
  31. data/lib/rack/handler/cgi.rb +61 -0
  32. data/lib/rack/handler/evented_mongrel.rb +8 -0
  33. data/lib/rack/handler/fastcgi.rb +89 -0
  34. data/lib/rack/handler/lsws.rb +63 -0
  35. data/lib/rack/handler/mongrel.rb +90 -0
  36. data/lib/rack/handler/scgi.rb +62 -0
  37. data/lib/rack/handler/swiftiplied_mongrel.rb +8 -0
  38. data/lib/rack/handler/thin.rb +18 -0
  39. data/lib/rack/handler/webrick.rb +69 -0
  40. data/lib/rack/head.rb +19 -0
  41. data/lib/rack/lint.rb +575 -0
  42. data/lib/rack/lobster.rb +65 -0
  43. data/lib/rack/lock.rb +16 -0
  44. data/lib/rack/logger.rb +20 -0
  45. data/lib/rack/methodoverride.rb +27 -0
  46. data/lib/rack/mime.rb +206 -0
  47. data/lib/rack/mock.rb +189 -0
  48. data/lib/rack/nulllogger.rb +18 -0
  49. data/lib/rack/recursive.rb +57 -0
  50. data/lib/rack/reloader.rb +109 -0
  51. data/lib/rack/request.rb +271 -0
  52. data/lib/rack/response.rb +149 -0
  53. data/lib/rack/rewindable_input.rb +100 -0
  54. data/lib/rack/runtime.rb +27 -0
  55. data/lib/rack/sendfile.rb +142 -0
  56. data/lib/rack/server.rb +212 -0
  57. data/lib/rack/session/abstract/id.rb +140 -0
  58. data/lib/rack/session/cookie.rb +90 -0
  59. data/lib/rack/session/memcache.rb +119 -0
  60. data/lib/rack/session/pool.rb +100 -0
  61. data/lib/rack/showexceptions.rb +349 -0
  62. data/lib/rack/showstatus.rb +106 -0
  63. data/lib/rack/static.rb +38 -0
  64. data/lib/rack/urlmap.rb +56 -0
  65. data/lib/rack/utils.rb +614 -0
  66. data/rack.gemspec +38 -0
  67. data/test/spec_rack_auth_basic.rb +73 -0
  68. data/test/spec_rack_auth_digest.rb +226 -0
  69. data/test/spec_rack_builder.rb +84 -0
  70. data/test/spec_rack_camping.rb +51 -0
  71. data/test/spec_rack_cascade.rb +48 -0
  72. data/test/spec_rack_cgi.rb +89 -0
  73. data/test/spec_rack_chunked.rb +62 -0
  74. data/test/spec_rack_commonlogger.rb +61 -0
  75. data/test/spec_rack_conditionalget.rb +41 -0
  76. data/test/spec_rack_config.rb +24 -0
  77. data/test/spec_rack_content_length.rb +43 -0
  78. data/test/spec_rack_content_type.rb +30 -0
  79. data/test/spec_rack_deflater.rb +127 -0
  80. data/test/spec_rack_directory.rb +61 -0
  81. data/test/spec_rack_etag.rb +17 -0
  82. data/test/spec_rack_fastcgi.rb +89 -0
  83. data/test/spec_rack_file.rb +75 -0
  84. data/test/spec_rack_handler.rb +43 -0
  85. data/test/spec_rack_head.rb +30 -0
  86. data/test/spec_rack_lint.rb +528 -0
  87. data/test/spec_rack_lobster.rb +45 -0
  88. data/test/spec_rack_lock.rb +38 -0
  89. data/test/spec_rack_logger.rb +21 -0
  90. data/test/spec_rack_methodoverride.rb +60 -0
  91. data/test/spec_rack_mock.rb +243 -0
  92. data/test/spec_rack_mongrel.rb +189 -0
  93. data/test/spec_rack_nulllogger.rb +13 -0
  94. data/test/spec_rack_recursive.rb +77 -0
  95. data/test/spec_rack_request.rb +545 -0
  96. data/test/spec_rack_response.rb +221 -0
  97. data/test/spec_rack_rewindable_input.rb +118 -0
  98. data/test/spec_rack_runtime.rb +35 -0
  99. data/test/spec_rack_sendfile.rb +86 -0
  100. data/test/spec_rack_session_cookie.rb +73 -0
  101. data/test/spec_rack_session_memcache.rb +273 -0
  102. data/test/spec_rack_session_pool.rb +172 -0
  103. data/test/spec_rack_showexceptions.rb +21 -0
  104. data/test/spec_rack_showstatus.rb +72 -0
  105. data/test/spec_rack_static.rb +37 -0
  106. data/test/spec_rack_thin.rb +91 -0
  107. data/test/spec_rack_urlmap.rb +215 -0
  108. data/test/spec_rack_utils.rb +554 -0
  109. data/test/spec_rack_webrick.rb +130 -0
  110. data/test/spec_rackup.rb +154 -0
  111. metadata +311 -0
@@ -0,0 +1,38 @@
1
+ module Rack
2
+
3
+ # The Rack::Static middleware intercepts requests for static files
4
+ # (javascript files, images, stylesheets, etc) based on the url prefixes
5
+ # passed in the options, and serves them using a Rack::File object. This
6
+ # allows a Rack stack to serve both static and dynamic content.
7
+ #
8
+ # Examples:
9
+ # use Rack::Static, :urls => ["/media"]
10
+ # will serve all requests beginning with /media from the "media" folder
11
+ # located in the current directory (ie media/*).
12
+ #
13
+ # use Rack::Static, :urls => ["/css", "/images"], :root => "public"
14
+ # will serve all requests beginning with /css or /images from the folder
15
+ # "public" in the current directory (ie public/css/* and public/images/*)
16
+
17
+ class Static
18
+
19
+ def initialize(app, options={})
20
+ @app = app
21
+ @urls = options[:urls] || ["/favicon.ico"]
22
+ root = options[:root] || Dir.pwd
23
+ @file_server = Rack::File.new(root)
24
+ end
25
+
26
+ def call(env)
27
+ path = env["PATH_INFO"]
28
+ can_serve = @urls.any? { |url| path.index(url) == 0 }
29
+
30
+ if can_serve
31
+ @file_server.call(env)
32
+ else
33
+ @app.call(env)
34
+ end
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,56 @@
1
+ module Rack
2
+ # Rack::URLMap takes a hash mapping urls or paths to apps, and
3
+ # dispatches accordingly. Support for HTTP/1.1 host names exists if
4
+ # the URLs start with <tt>http://</tt> or <tt>https://</tt>.
5
+ #
6
+ # URLMap modifies the SCRIPT_NAME and PATH_INFO such that the part
7
+ # relevant for dispatch is in the SCRIPT_NAME, and the rest in the
8
+ # PATH_INFO. This should be taken care of when you need to
9
+ # reconstruct the URL in order to create links.
10
+ #
11
+ # URLMap dispatches in such a way that the longest paths are tried
12
+ # first, since they are most specific.
13
+
14
+ class URLMap
15
+ def initialize(map = {})
16
+ remap(map)
17
+ end
18
+
19
+ def remap(map)
20
+ @mapping = map.map { |location, app|
21
+ if location =~ %r{\Ahttps?://(.*?)(/.*)}
22
+ host, location = $1, $2
23
+ else
24
+ host = nil
25
+ end
26
+
27
+ unless location[0] == ?/
28
+ raise ArgumentError, "paths need to start with /"
29
+ end
30
+ location = location.chomp('/')
31
+ match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", nil, 'n')
32
+
33
+ [host, location, match, app]
34
+ }.sort_by { |(h, l, m, a)| [h ? -h.size : (-1.0 / 0.0), -l.size] } # Longest path first
35
+ end
36
+
37
+ def call(env)
38
+ path = env["PATH_INFO"].to_s
39
+ script_name = env['SCRIPT_NAME']
40
+ hHost, sName, sPort = env.values_at('HTTP_HOST','SERVER_NAME','SERVER_PORT')
41
+ @mapping.each { |host, location, match, app|
42
+ next unless (hHost == host || sName == host \
43
+ || (host.nil? && (hHost == sName || hHost == sName+':'+sPort)))
44
+ next unless path =~ match && rest = $1
45
+ next unless rest.empty? || rest[0] == ?/
46
+
47
+ return app.call(
48
+ env.merge(
49
+ 'SCRIPT_NAME' => (script_name + location),
50
+ 'PATH_INFO' => rest))
51
+ }
52
+ [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass"}, ["Not Found: #{path}"]]
53
+ end
54
+ end
55
+ end
56
+
@@ -0,0 +1,614 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ require 'set'
4
+ require 'tempfile'
5
+
6
+ module Rack
7
+ # Rack::Utils contains a grab-bag of useful methods for writing web
8
+ # applications adopted from all kinds of Ruby libraries.
9
+
10
+ module Utils
11
+ # Performs URI escaping so that you can construct proper
12
+ # query strings faster. Use this rather than the cgi.rb
13
+ # version since it's faster. (Stolen from Camping).
14
+ def escape(s)
15
+ s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
16
+ '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
17
+ }.tr(' ', '+')
18
+ end
19
+ module_function :escape
20
+
21
+ # Unescapes a URI escaped string. (Stolen from Camping).
22
+ def unescape(s)
23
+ s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
24
+ [$1.delete('%')].pack('H*')
25
+ }
26
+ end
27
+ module_function :unescape
28
+
29
+ DEFAULT_SEP = /[&;] */n
30
+
31
+ # Stolen from Mongrel, with some small modifications:
32
+ # Parses a query string by breaking it up at the '&'
33
+ # and ';' characters. You can also use this to parse
34
+ # cookies by changing the characters used in the second
35
+ # parameter (which defaults to '&;').
36
+ def parse_query(qs, d = nil)
37
+ params = {}
38
+
39
+ (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
40
+ k, v = p.split('=', 2).map { |x| unescape(x) }
41
+ if cur = params[k]
42
+ if cur.class == Array
43
+ params[k] << v
44
+ else
45
+ params[k] = [cur, v]
46
+ end
47
+ else
48
+ params[k] = v
49
+ end
50
+ end
51
+
52
+ return params
53
+ end
54
+ module_function :parse_query
55
+
56
+ def parse_nested_query(qs, d = nil)
57
+ params = {}
58
+
59
+ (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
60
+ k, v = unescape(p).split('=', 2)
61
+ normalize_params(params, k, v)
62
+ end
63
+
64
+ return params
65
+ end
66
+ module_function :parse_nested_query
67
+
68
+ def normalize_params(params, name, v = nil)
69
+ name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
70
+ k = $1 || ''
71
+ after = $' || ''
72
+
73
+ return if k.empty?
74
+
75
+ if after == ""
76
+ params[k] = v
77
+ elsif after == "[]"
78
+ params[k] ||= []
79
+ raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
80
+ params[k] << v
81
+ elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
82
+ child_key = $1
83
+ params[k] ||= []
84
+ raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
85
+ if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)
86
+ normalize_params(params[k].last, child_key, v)
87
+ else
88
+ params[k] << normalize_params({}, child_key, v)
89
+ end
90
+ else
91
+ params[k] ||= {}
92
+ raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Hash)
93
+ params[k] = normalize_params(params[k], after, v)
94
+ end
95
+
96
+ return params
97
+ end
98
+ module_function :normalize_params
99
+
100
+ def build_query(params)
101
+ params.map { |k, v|
102
+ if v.class == Array
103
+ build_query(v.map { |x| [k, x] })
104
+ else
105
+ "#{escape(k)}=#{escape(v)}"
106
+ end
107
+ }.join("&")
108
+ end
109
+ module_function :build_query
110
+
111
+ def build_nested_query(value, prefix = nil)
112
+ case value
113
+ when Array
114
+ value.map { |v|
115
+ build_nested_query(v, "#{prefix}[]")
116
+ }.join("&")
117
+ when Hash
118
+ value.map { |k, v|
119
+ build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
120
+ }.join("&")
121
+ when String
122
+ raise ArgumentError, "value must be a Hash" if prefix.nil?
123
+ "#{prefix}=#{escape(value)}"
124
+ else
125
+ prefix
126
+ end
127
+ end
128
+ module_function :build_nested_query
129
+
130
+ # Escape ampersands, brackets and quotes to their HTML/XML entities.
131
+ def escape_html(string)
132
+ string.to_s.gsub("&", "&amp;").
133
+ gsub("<", "&lt;").
134
+ gsub(">", "&gt;").
135
+ gsub("'", "&#39;").
136
+ gsub('"', "&quot;")
137
+ end
138
+ module_function :escape_html
139
+
140
+ def select_best_encoding(available_encodings, accept_encoding)
141
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
142
+
143
+ expanded_accept_encoding =
144
+ accept_encoding.map { |m, q|
145
+ if m == "*"
146
+ (available_encodings - accept_encoding.map { |m2, _| m2 }).map { |m2| [m2, q] }
147
+ else
148
+ [[m, q]]
149
+ end
150
+ }.inject([]) { |mem, list|
151
+ mem + list
152
+ }
153
+
154
+ encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map { |m, _| m }
155
+
156
+ unless encoding_candidates.include?("identity")
157
+ encoding_candidates.push("identity")
158
+ end
159
+
160
+ expanded_accept_encoding.find_all { |m, q|
161
+ q == 0.0
162
+ }.each { |m, _|
163
+ encoding_candidates.delete(m)
164
+ }
165
+
166
+ return (encoding_candidates & available_encodings)[0]
167
+ end
168
+ module_function :select_best_encoding
169
+
170
+ def set_cookie_header!(header, key, value)
171
+ case value
172
+ when Hash
173
+ domain = "; domain=" + value[:domain] if value[:domain]
174
+ path = "; path=" + value[:path] if value[:path]
175
+ # According to RFC 2109, we need dashes here.
176
+ # N.B.: cgi.rb uses spaces...
177
+ expires = "; expires=" + value[:expires].clone.gmtime.
178
+ strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
179
+ secure = "; secure" if value[:secure]
180
+ httponly = "; HttpOnly" if value[:httponly]
181
+ value = value[:value]
182
+ end
183
+ value = [value] unless Array === value
184
+ cookie = escape(key) + "=" +
185
+ value.map { |v| escape v }.join("&") +
186
+ "#{domain}#{path}#{expires}#{secure}#{httponly}"
187
+
188
+ case header["Set-Cookie"]
189
+ when Array
190
+ header["Set-Cookie"] << cookie
191
+ when String
192
+ header["Set-Cookie"] = [header["Set-Cookie"], cookie]
193
+ when nil
194
+ header["Set-Cookie"] = cookie
195
+ end
196
+
197
+ nil
198
+ end
199
+ module_function :set_cookie_header!
200
+
201
+ def delete_cookie_header!(header, key, value = {})
202
+ unless Array === header["Set-Cookie"]
203
+ header["Set-Cookie"] = [header["Set-Cookie"]].compact
204
+ end
205
+
206
+ header["Set-Cookie"].reject! { |cookie|
207
+ cookie =~ /\A#{escape(key)}=/
208
+ }
209
+
210
+ set_cookie_header!(header, key,
211
+ {:value => '', :path => nil, :domain => nil,
212
+ :expires => Time.at(0) }.merge(value))
213
+
214
+ nil
215
+ end
216
+ module_function :delete_cookie_header!
217
+
218
+ # Return the bytesize of String; uses String#length under Ruby 1.8 and
219
+ # String#bytesize under 1.9.
220
+ if ''.respond_to?(:bytesize)
221
+ def bytesize(string)
222
+ string.bytesize
223
+ end
224
+ else
225
+ def bytesize(string)
226
+ string.size
227
+ end
228
+ end
229
+ module_function :bytesize
230
+
231
+ # Context allows the use of a compatible middleware at different points
232
+ # in a request handling stack. A compatible middleware must define
233
+ # #context which should take the arguments env and app. The first of which
234
+ # would be the request environment. The second of which would be the rack
235
+ # application that the request would be forwarded to.
236
+ class Context
237
+ attr_reader :for, :app
238
+
239
+ def initialize(app_f, app_r)
240
+ raise 'running context does not respond to #context' unless app_f.respond_to? :context
241
+ @for, @app = app_f, app_r
242
+ end
243
+
244
+ def call(env)
245
+ @for.context(env, @app)
246
+ end
247
+
248
+ def recontext(app)
249
+ self.class.new(@for, app)
250
+ end
251
+
252
+ def context(env, app=@app)
253
+ recontext(app).call(env)
254
+ end
255
+ end
256
+
257
+ # A case-insensitive Hash that preserves the original case of a
258
+ # header when set.
259
+ class HeaderHash < Hash
260
+ def self.new(hash={})
261
+ HeaderHash === hash ? hash : super(hash)
262
+ end
263
+
264
+ def initialize(hash={})
265
+ super()
266
+ @names = {}
267
+ hash.each { |k, v| self[k] = v }
268
+ end
269
+
270
+ def each
271
+ super do |k, v|
272
+ yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
273
+ end
274
+ end
275
+
276
+ def to_hash
277
+ inject({}) do |hash, (k,v)|
278
+ if v.respond_to? :to_ary
279
+ hash[k] = v.to_ary.join("\n")
280
+ else
281
+ hash[k] = v
282
+ end
283
+ hash
284
+ end
285
+ end
286
+
287
+ def [](k)
288
+ super(@names[k] ||= @names[k.downcase])
289
+ end
290
+
291
+ def []=(k, v)
292
+ delete k
293
+ @names[k] = @names[k.downcase] = k
294
+ super k, v
295
+ end
296
+
297
+ def delete(k)
298
+ canonical = k.downcase
299
+ result = super @names.delete(canonical)
300
+ @names.delete_if { |name,| name.downcase == canonical }
301
+ result
302
+ end
303
+
304
+ def include?(k)
305
+ @names.include?(k) || @names.include?(k.downcase)
306
+ end
307
+
308
+ alias_method :has_key?, :include?
309
+ alias_method :member?, :include?
310
+ alias_method :key?, :include?
311
+
312
+ def merge!(other)
313
+ other.each { |k, v| self[k] = v }
314
+ self
315
+ end
316
+
317
+ def merge(other)
318
+ hash = dup
319
+ hash.merge! other
320
+ end
321
+
322
+ def replace(other)
323
+ clear
324
+ other.each { |k, v| self[k] = v }
325
+ self
326
+ end
327
+ end
328
+
329
+ # Every standard HTTP code mapped to the appropriate message.
330
+ # Generated with:
331
+ # curl -s http://www.iana.org/assignments/http-status-codes | \
332
+ # ruby -ane 'm = /^(\d{3}) +(\S[^\[(]+)/.match($_) and
333
+ # puts " #{m[1]} => \x27#{m[2].strip}x27,"'
334
+ HTTP_STATUS_CODES = {
335
+ 100 => 'Continue',
336
+ 101 => 'Switching Protocols',
337
+ 102 => 'Processing',
338
+ 200 => 'OK',
339
+ 201 => 'Created',
340
+ 202 => 'Accepted',
341
+ 203 => 'Non-Authoritative Information',
342
+ 204 => 'No Content',
343
+ 205 => 'Reset Content',
344
+ 206 => 'Partial Content',
345
+ 207 => 'Multi-Status',
346
+ 226 => 'IM Used',
347
+ 300 => 'Multiple Choices',
348
+ 301 => 'Moved Permanently',
349
+ 302 => 'Found',
350
+ 303 => 'See Other',
351
+ 304 => 'Not Modified',
352
+ 305 => 'Use Proxy',
353
+ 306 => 'Reserved',
354
+ 307 => 'Temporary Redirect',
355
+ 400 => 'Bad Request',
356
+ 401 => 'Unauthorized',
357
+ 402 => 'Payment Required',
358
+ 403 => 'Forbidden',
359
+ 404 => 'Not Found',
360
+ 405 => 'Method Not Allowed',
361
+ 406 => 'Not Acceptable',
362
+ 407 => 'Proxy Authentication Required',
363
+ 408 => 'Request Timeout',
364
+ 409 => 'Conflict',
365
+ 410 => 'Gone',
366
+ 411 => 'Length Required',
367
+ 412 => 'Precondition Failed',
368
+ 413 => 'Request Entity Too Large',
369
+ 414 => 'Request-URI Too Long',
370
+ 415 => 'Unsupported Media Type',
371
+ 416 => 'Requested Range Not Satisfiable',
372
+ 417 => 'Expectation Failed',
373
+ 422 => 'Unprocessable Entity',
374
+ 423 => 'Locked',
375
+ 424 => 'Failed Dependency',
376
+ 426 => 'Upgrade Required',
377
+ 500 => 'Internal Server Error',
378
+ 501 => 'Not Implemented',
379
+ 502 => 'Bad Gateway',
380
+ 503 => 'Service Unavailable',
381
+ 504 => 'Gateway Timeout',
382
+ 505 => 'HTTP Version Not Supported',
383
+ 506 => 'Variant Also Negotiates',
384
+ 507 => 'Insufficient Storage',
385
+ 510 => 'Not Extended',
386
+ }
387
+
388
+ # Responses with HTTP status codes that should not have an entity body
389
+ STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 304)
390
+
391
+ SYMBOL_TO_STATUS_CODE = HTTP_STATUS_CODES.inject({}) { |hash, (code, message)|
392
+ hash[message.downcase.gsub(/\s|-/, '_').to_sym] = code
393
+ hash
394
+ }
395
+
396
+ def status_code(status)
397
+ if status.is_a?(Symbol)
398
+ SYMBOL_TO_STATUS_CODE[status] || 500
399
+ else
400
+ status.to_i
401
+ end
402
+ end
403
+ module_function :status_code
404
+
405
+ # A multipart form data parser, adapted from IOWA.
406
+ #
407
+ # Usually, Rack::Request#POST takes care of calling this.
408
+
409
+ module Multipart
410
+ class UploadedFile
411
+ # The filename, *not* including the path, of the "uploaded" file
412
+ attr_reader :original_filename
413
+
414
+ # The content type of the "uploaded" file
415
+ attr_accessor :content_type
416
+
417
+ def initialize(path, content_type = "text/plain", binary = false)
418
+ raise "#{path} file does not exist" unless ::File.exist?(path)
419
+ @content_type = content_type
420
+ @original_filename = ::File.basename(path)
421
+ @tempfile = Tempfile.new(@original_filename)
422
+ @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
423
+ @tempfile.binmode if binary
424
+ FileUtils.copy_file(path, @tempfile.path)
425
+ end
426
+
427
+ def path
428
+ @tempfile.path
429
+ end
430
+ alias_method :local_path, :path
431
+
432
+ def method_missing(method_name, *args, &block) #:nodoc:
433
+ @tempfile.__send__(method_name, *args, &block)
434
+ end
435
+ end
436
+
437
+ EOL = "\r\n"
438
+ MULTIPART_BOUNDARY = "AaB03x"
439
+
440
+ def self.parse_multipart(env)
441
+ unless env['CONTENT_TYPE'] =~
442
+ %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n
443
+ nil
444
+ else
445
+ boundary = "--#{$1}"
446
+
447
+ params = {}
448
+ buf = ""
449
+ content_length = env['CONTENT_LENGTH'].to_i
450
+ input = env['rack.input']
451
+ input.rewind
452
+
453
+ boundary_size = Utils.bytesize(boundary) + EOL.size
454
+ bufsize = 16384
455
+
456
+ content_length -= boundary_size
457
+
458
+ read_buffer = ''
459
+
460
+ status = input.read(boundary_size, read_buffer)
461
+ raise EOFError, "bad content body" unless status == boundary + EOL
462
+
463
+ rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/n
464
+
465
+ loop {
466
+ head = nil
467
+ body = ''
468
+ filename = content_type = name = nil
469
+
470
+ until head && buf =~ rx
471
+ if !head && i = buf.index(EOL+EOL)
472
+ head = buf.slice!(0, i+2) # First \r\n
473
+ buf.slice!(0, 2) # Second \r\n
474
+
475
+ filename = head[/Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;\s]*))/ni, 1]
476
+ content_type = head[/Content-Type: (.*)#{EOL}/ni, 1]
477
+ name = head[/Content-Disposition:.*\s+name="?([^\";]*)"?/ni, 1] || head[/Content-ID:\s*([^#{EOL}]*)/ni, 1]
478
+
479
+ if content_type || filename
480
+ body = Tempfile.new("RackMultipart")
481
+ body.binmode if body.respond_to?(:binmode)
482
+ end
483
+
484
+ next
485
+ end
486
+
487
+ # Save the read body part.
488
+ if head && (boundary_size+4 < buf.size)
489
+ body << buf.slice!(0, buf.size - (boundary_size+4))
490
+ end
491
+
492
+ c = input.read(bufsize < content_length ? bufsize : content_length, read_buffer)
493
+ raise EOFError, "bad content body" if c.nil? || c.empty?
494
+ buf << c
495
+ content_length -= c.size
496
+ end
497
+
498
+ # Save the rest.
499
+ if i = buf.index(rx)
500
+ body << buf.slice!(0, i)
501
+ buf.slice!(0, boundary_size+2)
502
+
503
+ content_length = -1 if $1 == "--"
504
+ end
505
+
506
+ if filename == ""
507
+ # filename is blank which means no file has been selected
508
+ data = nil
509
+ elsif filename
510
+ body.rewind
511
+
512
+ # Take the basename of the upload's original filename.
513
+ # This handles the full Windows paths given by Internet Explorer
514
+ # (and perhaps other broken user agents) without affecting
515
+ # those which give the lone filename.
516
+ filename =~ /^(?:.*[:\\\/])?(.*)/m
517
+ filename = $1
518
+
519
+ data = {:filename => filename, :type => content_type,
520
+ :name => name, :tempfile => body, :head => head}
521
+ elsif !filename && content_type
522
+ body.rewind
523
+
524
+ # Generic multipart cases, not coming from a form
525
+ data = {:type => content_type,
526
+ :name => name, :tempfile => body, :head => head}
527
+ else
528
+ data = body
529
+ end
530
+
531
+ Utils.normalize_params(params, name, data) unless data.nil?
532
+
533
+ # break if we're at the end of a buffer, but not if it is the end of a field
534
+ break if (buf.empty? && $1 != EOL) || content_length == -1
535
+ }
536
+
537
+ input.rewind
538
+
539
+ params
540
+ end
541
+ end
542
+
543
+ def self.build_multipart(params, first = true)
544
+ if first
545
+ unless params.is_a?(Hash)
546
+ raise ArgumentError, "value must be a Hash"
547
+ end
548
+
549
+ multipart = false
550
+ query = lambda { |value|
551
+ case value
552
+ when Array
553
+ value.each(&query)
554
+ when Hash
555
+ value.values.each(&query)
556
+ when UploadedFile
557
+ multipart = true
558
+ end
559
+ }
560
+ params.values.each(&query)
561
+ return nil unless multipart
562
+ end
563
+
564
+ flattened_params = Hash.new
565
+
566
+ params.each do |key, value|
567
+ k = first ? key.to_s : "[#{key}]"
568
+
569
+ case value
570
+ when Array
571
+ value.map { |v|
572
+ build_multipart(v, false).each { |subkey, subvalue|
573
+ flattened_params["#{k}[]#{subkey}"] = subvalue
574
+ }
575
+ }
576
+ when Hash
577
+ build_multipart(value, false).each { |subkey, subvalue|
578
+ flattened_params[k + subkey] = subvalue
579
+ }
580
+ else
581
+ flattened_params[k] = value
582
+ end
583
+ end
584
+
585
+ if first
586
+ flattened_params.map { |name, file|
587
+ if file.respond_to?(:original_filename)
588
+ ::File.open(file.path, "rb") do |f|
589
+ f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
590
+ <<-EOF
591
+ --#{MULTIPART_BOUNDARY}\r
592
+ Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
593
+ Content-Type: #{file.content_type}\r
594
+ Content-Length: #{::File.stat(file.path).size}\r
595
+ \r
596
+ #{f.read}\r
597
+ EOF
598
+ end
599
+ else
600
+ <<-EOF
601
+ --#{MULTIPART_BOUNDARY}\r
602
+ Content-Disposition: form-data; name="#{name}"\r
603
+ \r
604
+ #{file}\r
605
+ EOF
606
+ end
607
+ }.join + "--#{MULTIPART_BOUNDARY}--\r"
608
+ else
609
+ flattened_params
610
+ end
611
+ end
612
+ end
613
+ end
614
+ end