eac-rack 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
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