edgar-rack 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (138) hide show
  1. data/COPYING +18 -0
  2. data/KNOWN-ISSUES +21 -0
  3. data/README +401 -0
  4. data/Rakefile +101 -0
  5. data/SPEC +171 -0
  6. data/bin/rackup +4 -0
  7. data/contrib/rack_logo.svg +111 -0
  8. data/example/lobster.ru +4 -0
  9. data/example/protectedlobster.rb +14 -0
  10. data/example/protectedlobster.ru +8 -0
  11. data/lib/rack.rb +81 -0
  12. data/lib/rack/auth/abstract/handler.rb +37 -0
  13. data/lib/rack/auth/abstract/request.rb +43 -0
  14. data/lib/rack/auth/basic.rb +58 -0
  15. data/lib/rack/auth/digest/md5.rb +124 -0
  16. data/lib/rack/auth/digest/nonce.rb +51 -0
  17. data/lib/rack/auth/digest/params.rb +53 -0
  18. data/lib/rack/auth/digest/request.rb +40 -0
  19. data/lib/rack/builder.rb +80 -0
  20. data/lib/rack/cascade.rb +41 -0
  21. data/lib/rack/chunked.rb +52 -0
  22. data/lib/rack/commonlogger.rb +49 -0
  23. data/lib/rack/conditionalget.rb +63 -0
  24. data/lib/rack/config.rb +15 -0
  25. data/lib/rack/content_length.rb +29 -0
  26. data/lib/rack/content_type.rb +23 -0
  27. data/lib/rack/deflater.rb +96 -0
  28. data/lib/rack/directory.rb +157 -0
  29. data/lib/rack/etag.rb +59 -0
  30. data/lib/rack/file.rb +118 -0
  31. data/lib/rack/handler.rb +88 -0
  32. data/lib/rack/handler/cgi.rb +61 -0
  33. data/lib/rack/handler/evented_mongrel.rb +8 -0
  34. data/lib/rack/handler/fastcgi.rb +90 -0
  35. data/lib/rack/handler/lsws.rb +61 -0
  36. data/lib/rack/handler/mongrel.rb +90 -0
  37. data/lib/rack/handler/scgi.rb +59 -0
  38. data/lib/rack/handler/swiftiplied_mongrel.rb +8 -0
  39. data/lib/rack/handler/thin.rb +17 -0
  40. data/lib/rack/handler/webrick.rb +73 -0
  41. data/lib/rack/head.rb +19 -0
  42. data/lib/rack/lint.rb +567 -0
  43. data/lib/rack/lobster.rb +65 -0
  44. data/lib/rack/lock.rb +44 -0
  45. data/lib/rack/logger.rb +18 -0
  46. data/lib/rack/methodoverride.rb +27 -0
  47. data/lib/rack/mime.rb +210 -0
  48. data/lib/rack/mock.rb +185 -0
  49. data/lib/rack/nulllogger.rb +18 -0
  50. data/lib/rack/recursive.rb +61 -0
  51. data/lib/rack/reloader.rb +109 -0
  52. data/lib/rack/request.rb +307 -0
  53. data/lib/rack/response.rb +151 -0
  54. data/lib/rack/rewindable_input.rb +104 -0
  55. data/lib/rack/runtime.rb +27 -0
  56. data/lib/rack/sendfile.rb +139 -0
  57. data/lib/rack/server.rb +289 -0
  58. data/lib/rack/session/abstract/id.rb +348 -0
  59. data/lib/rack/session/cookie.rb +152 -0
  60. data/lib/rack/session/memcache.rb +93 -0
  61. data/lib/rack/session/pool.rb +79 -0
  62. data/lib/rack/showexceptions.rb +378 -0
  63. data/lib/rack/showstatus.rb +113 -0
  64. data/lib/rack/static.rb +53 -0
  65. data/lib/rack/urlmap.rb +55 -0
  66. data/lib/rack/utils.rb +698 -0
  67. data/rack.gemspec +39 -0
  68. data/test/cgi/lighttpd.conf +25 -0
  69. data/test/cgi/rackup_stub.rb +6 -0
  70. data/test/cgi/sample_rackup.ru +5 -0
  71. data/test/cgi/test +9 -0
  72. data/test/cgi/test.fcgi +8 -0
  73. data/test/cgi/test.ru +5 -0
  74. data/test/gemloader.rb +6 -0
  75. data/test/multipart/bad_robots +259 -0
  76. data/test/multipart/binary +0 -0
  77. data/test/multipart/empty +10 -0
  78. data/test/multipart/fail_16384_nofile +814 -0
  79. data/test/multipart/file1.txt +1 -0
  80. data/test/multipart/filename_and_modification_param +7 -0
  81. data/test/multipart/filename_with_escaped_quotes +6 -0
  82. data/test/multipart/filename_with_escaped_quotes_and_modification_param +7 -0
  83. data/test/multipart/filename_with_percent_escaped_quotes +6 -0
  84. data/test/multipart/filename_with_unescaped_quotes +6 -0
  85. data/test/multipart/ie +6 -0
  86. data/test/multipart/nested +10 -0
  87. data/test/multipart/none +9 -0
  88. data/test/multipart/semicolon +6 -0
  89. data/test/multipart/text +15 -0
  90. data/test/rackup/config.ru +31 -0
  91. data/test/spec_auth_basic.rb +70 -0
  92. data/test/spec_auth_digest.rb +241 -0
  93. data/test/spec_builder.rb +123 -0
  94. data/test/spec_cascade.rb +45 -0
  95. data/test/spec_cgi.rb +102 -0
  96. data/test/spec_chunked.rb +60 -0
  97. data/test/spec_commonlogger.rb +56 -0
  98. data/test/spec_conditionalget.rb +86 -0
  99. data/test/spec_config.rb +23 -0
  100. data/test/spec_content_length.rb +36 -0
  101. data/test/spec_content_type.rb +29 -0
  102. data/test/spec_deflater.rb +125 -0
  103. data/test/spec_directory.rb +57 -0
  104. data/test/spec_etag.rb +75 -0
  105. data/test/spec_fastcgi.rb +107 -0
  106. data/test/spec_file.rb +92 -0
  107. data/test/spec_handler.rb +49 -0
  108. data/test/spec_head.rb +30 -0
  109. data/test/spec_lint.rb +515 -0
  110. data/test/spec_lobster.rb +43 -0
  111. data/test/spec_lock.rb +142 -0
  112. data/test/spec_logger.rb +28 -0
  113. data/test/spec_methodoverride.rb +58 -0
  114. data/test/spec_mock.rb +241 -0
  115. data/test/spec_mongrel.rb +182 -0
  116. data/test/spec_nulllogger.rb +12 -0
  117. data/test/spec_recursive.rb +69 -0
  118. data/test/spec_request.rb +774 -0
  119. data/test/spec_response.rb +245 -0
  120. data/test/spec_rewindable_input.rb +118 -0
  121. data/test/spec_runtime.rb +39 -0
  122. data/test/spec_sendfile.rb +83 -0
  123. data/test/spec_server.rb +8 -0
  124. data/test/spec_session_abstract_id.rb +43 -0
  125. data/test/spec_session_cookie.rb +171 -0
  126. data/test/spec_session_memcache.rb +289 -0
  127. data/test/spec_session_pool.rb +200 -0
  128. data/test/spec_showexceptions.rb +87 -0
  129. data/test/spec_showstatus.rb +79 -0
  130. data/test/spec_static.rb +48 -0
  131. data/test/spec_thin.rb +86 -0
  132. data/test/spec_urlmap.rb +213 -0
  133. data/test/spec_utils.rb +678 -0
  134. data/test/spec_webrick.rb +141 -0
  135. data/test/testrequest.rb +78 -0
  136. data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
  137. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
  138. metadata +329 -0
@@ -0,0 +1,113 @@
1
+ require 'erb'
2
+ require 'rack/request'
3
+ require 'rack/utils'
4
+
5
+ module Rack
6
+ # Rack::ShowStatus catches all empty responses the app it wraps and
7
+ # replaces them with a site explaining the error.
8
+ #
9
+ # Additional details can be put into <tt>rack.showstatus.detail</tt>
10
+ # and will be shown as HTML. If such details exist, the error page
11
+ # is always rendered, even if the reply was not empty.
12
+
13
+ class ShowStatus
14
+ def initialize(app)
15
+ @app = app
16
+ @template = ERB.new(TEMPLATE)
17
+ end
18
+
19
+ def call(env)
20
+ status, headers, body = @app.call(env)
21
+ headers = Utils::HeaderHash.new(headers)
22
+ empty = headers['Content-Length'].to_i <= 0
23
+
24
+ # client or server error, or explicit message
25
+ if (status.to_i >= 400 && empty) || env["rack.showstatus.detail"]
26
+ # This double assignment is to prevent an "unused variable" warning on
27
+ # Ruby 1.9.3. Yes, it is dumb, but I don't like Ruby yelling at me.
28
+ req = req = Rack::Request.new(env)
29
+
30
+ message = Rack::Utils::HTTP_STATUS_CODES[status.to_i] || status.to_s
31
+
32
+ # This double assignment is to prevent an "unused variable" warning on
33
+ # Ruby 1.9.3. Yes, it is dumb, but I don't like Ruby yelling at me.
34
+ detail = detail = env["rack.showstatus.detail"] || message
35
+
36
+ body = @template.result(binding)
37
+ size = Rack::Utils.bytesize(body)
38
+ [status, headers.merge("Content-Type" => "text/html", "Content-Length" => size.to_s), [body]]
39
+ else
40
+ [status, headers, body]
41
+ end
42
+ end
43
+
44
+ def h(obj) # :nodoc:
45
+ case obj
46
+ when String
47
+ Utils.escape_html(obj)
48
+ else
49
+ Utils.escape_html(obj.inspect)
50
+ end
51
+ end
52
+
53
+ # :stopdoc:
54
+
55
+ # adapted from Django <djangoproject.com>
56
+ # Copyright (c) 2005, the Lawrence Journal-World
57
+ # Used under the modified BSD license:
58
+ # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
59
+ TEMPLATE = <<'HTML'
60
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
61
+ <html lang="en">
62
+ <head>
63
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
64
+ <title><%=h message %> at <%=h req.script_name + req.path_info %></title>
65
+ <meta name="robots" content="NONE,NOARCHIVE" />
66
+ <style type="text/css">
67
+ html * { padding:0; margin:0; }
68
+ body * { padding:10px 20px; }
69
+ body * * { padding:0; }
70
+ body { font:small sans-serif; background:#eee; }
71
+ body>div { border-bottom:1px solid #ddd; }
72
+ h1 { font-weight:normal; margin-bottom:.4em; }
73
+ h1 span { font-size:60%; color:#666; font-weight:normal; }
74
+ table { border:none; border-collapse: collapse; width:100%; }
75
+ td, th { vertical-align:top; padding:2px 3px; }
76
+ th { width:12em; text-align:right; color:#666; padding-right:.5em; }
77
+ #info { background:#f6f6f6; }
78
+ #info ol { margin: 0.5em 4em; }
79
+ #info ol li { font-family: monospace; }
80
+ #summary { background: #ffc; }
81
+ #explanation { background:#eee; border-bottom: 0px none; }
82
+ </style>
83
+ </head>
84
+ <body>
85
+ <div id="summary">
86
+ <h1><%=h message %> <span>(<%= status.to_i %>)</span></h1>
87
+ <table class="meta">
88
+ <tr>
89
+ <th>Request Method:</th>
90
+ <td><%=h req.request_method %></td>
91
+ </tr>
92
+ <tr>
93
+ <th>Request URL:</th>
94
+ <td><%=h req.url %></td>
95
+ </tr>
96
+ </table>
97
+ </div>
98
+ <div id="info">
99
+ <p><%= detail %></p>
100
+ </div>
101
+
102
+ <div id="explanation">
103
+ <p>
104
+ You're seeing this error because you use <code>Rack::ShowStatus</code>.
105
+ </p>
106
+ </div>
107
+ </body>
108
+ </html>
109
+ HTML
110
+
111
+ # :startdoc:
112
+ end
113
+ end
@@ -0,0 +1,53 @@
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 or
5
+ # route mappings passed in the options, and serves them using a Rack::File
6
+ # object. This allows a Rack stack to serve both static and dynamic content.
7
+ #
8
+ # Examples:
9
+ #
10
+ # Serve all requests beginning with /media from the "media" folder located
11
+ # in the current directory (ie media/*):
12
+ #
13
+ # use Rack::Static, :urls => ["/media"]
14
+ #
15
+ # Serve all requests beginning with /css or /images from the folder "public"
16
+ # in the current directory (ie public/css/* and public/images/*):
17
+ #
18
+ # use Rack::Static, :urls => ["/css", "/images"], :root => "public"
19
+ #
20
+ # Serve all requests to / with "index.html" from the folder "public" in the
21
+ # current directory (ie public/index.html):
22
+ #
23
+ # use Rack::Static, :urls => {"/" => 'index.html'}, :root => 'public'
24
+ #
25
+
26
+ class Static
27
+
28
+ def initialize(app, options={})
29
+ @app = app
30
+ @urls = options[:urls] || ["/favicon.ico"]
31
+ root = options[:root] || Dir.pwd
32
+ @file_server = Rack::File.new(root)
33
+ end
34
+
35
+ def call(env)
36
+ path = env["PATH_INFO"]
37
+
38
+ unless @urls.kind_of? Hash
39
+ can_serve = @urls.any? { |url| path.index(url) == 0 }
40
+ else
41
+ can_serve = @urls.key? path
42
+ end
43
+
44
+ if can_serve
45
+ env["PATH_INFO"] = @urls[path] if @urls.kind_of? Hash
46
+ @file_server.call(env)
47
+ else
48
+ @app.call(env)
49
+ end
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,55 @@
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, _, _)| [h ? -h.size : (-1.0 / 0.0), -l.size] } # Longest path first
35
+ end
36
+
37
+ def call(env)
38
+ path = env["PATH_INFO"]
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.to_s =~ match && rest = $1
45
+ next unless rest.empty? || rest[0] == ?/
46
+ env.merge!('SCRIPT_NAME' => (script_name + location), 'PATH_INFO' => rest)
47
+ return app.call(env)
48
+ }
49
+ [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass"}, ["Not Found: #{path}"]]
50
+ ensure
51
+ env.merge! 'PATH_INFO' => path, 'SCRIPT_NAME' => script_name
52
+ end
53
+ end
54
+ end
55
+
@@ -0,0 +1,698 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ require 'fileutils'
4
+ require 'set'
5
+ require 'tempfile'
6
+
7
+ module Rack
8
+ # Rack::Utils contains a grab-bag of useful methods for writing web
9
+ # applications adopted from all kinds of Ruby libraries.
10
+
11
+ module Utils
12
+ # Performs URI escaping so that you can construct proper
13
+ # query strings faster. Use this rather than the cgi.rb
14
+ # version since it's faster. (Stolen from Camping).
15
+ def escape(s)
16
+ s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/u) {
17
+ '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
18
+ }.tr(' ', '+')
19
+ end
20
+ module_function :escape
21
+
22
+ # Unescapes a URI escaped string. (Stolen from Camping).
23
+ def unescape(s)
24
+ s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
25
+ [$1.delete('%')].pack('H*')
26
+ }
27
+ end
28
+ module_function :unescape
29
+
30
+ DEFAULT_SEP = /[&;] */n
31
+
32
+ # Stolen from Mongrel, with some small modifications:
33
+ # Parses a query string by breaking it up at the '&'
34
+ # and ';' characters. You can also use this to parse
35
+ # cookies by changing the characters used in the second
36
+ # parameter (which defaults to '&;').
37
+ def parse_query(qs, d = nil)
38
+ params = {}
39
+
40
+ (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
41
+ k, v = p.split('=', 2).map { |x| unescape(x) }
42
+ if cur = params[k]
43
+ if cur.class == Array
44
+ params[k] << v
45
+ else
46
+ params[k] = [cur, v]
47
+ end
48
+ else
49
+ params[k] = v
50
+ end
51
+ end
52
+
53
+ return params
54
+ end
55
+ module_function :parse_query
56
+
57
+ def parse_nested_query(qs, d = nil)
58
+ params = {}
59
+
60
+ (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
61
+ k, v = unescape(p).split('=', 2)
62
+ normalize_params(params, k, v)
63
+ end
64
+
65
+ return params
66
+ end
67
+ module_function :parse_nested_query
68
+
69
+ def normalize_params(params, name, v = nil)
70
+ name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
71
+ k = $1 || ''
72
+ after = $' || ''
73
+
74
+ return if k.empty?
75
+
76
+ if after == ""
77
+ params[k] = v
78
+ elsif after == "[]"
79
+ params[k] ||= []
80
+ raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
81
+ params[k] << v
82
+ elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
83
+ child_key = $1
84
+ params[k] ||= []
85
+ raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
86
+ if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)
87
+ normalize_params(params[k].last, child_key, v)
88
+ else
89
+ params[k] << normalize_params({}, child_key, v)
90
+ end
91
+ else
92
+ params[k] ||= {}
93
+ raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Hash)
94
+ params[k] = normalize_params(params[k], after, v)
95
+ end
96
+
97
+ return params
98
+ end
99
+ module_function :normalize_params
100
+
101
+ def build_query(params)
102
+ params.map { |k, v|
103
+ if v.class == Array
104
+ build_query(v.map { |x| [k, x] })
105
+ else
106
+ "#{escape(k)}=#{escape(v)}"
107
+ end
108
+ }.join("&")
109
+ end
110
+ module_function :build_query
111
+
112
+ def build_nested_query(value, prefix = nil)
113
+ case value
114
+ when Array
115
+ value.map { |v|
116
+ build_nested_query(v, "#{prefix}[]")
117
+ }.join("&")
118
+ when Hash
119
+ value.map { |k, v|
120
+ build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
121
+ }.join("&")
122
+ when String
123
+ raise ArgumentError, "value must be a Hash" if prefix.nil?
124
+ "#{prefix}=#{escape(value)}"
125
+ else
126
+ prefix
127
+ end
128
+ end
129
+ module_function :build_nested_query
130
+
131
+ ESCAPE_HTML = {
132
+ "&" => "&amp;",
133
+ "<" => "&lt;",
134
+ ">" => "&gt;",
135
+ "'" => "&#x27;",
136
+ '"' => "&quot;",
137
+ "/" => "&#x2F;"
138
+ }
139
+ ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
140
+
141
+ # Escape ampersands, brackets and quotes to their HTML/XML entities.
142
+ def escape_html(string)
143
+ string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
144
+ end
145
+ module_function :escape_html
146
+
147
+ def select_best_encoding(available_encodings, accept_encoding)
148
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
149
+
150
+ expanded_accept_encoding =
151
+ accept_encoding.map { |m, q|
152
+ if m == "*"
153
+ (available_encodings - accept_encoding.map { |m2, _| m2 }).map { |m2| [m2, q] }
154
+ else
155
+ [[m, q]]
156
+ end
157
+ }.inject([]) { |mem, list|
158
+ mem + list
159
+ }
160
+
161
+ encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map { |m, _| m }
162
+
163
+ unless encoding_candidates.include?("identity")
164
+ encoding_candidates.push("identity")
165
+ end
166
+
167
+ expanded_accept_encoding.find_all { |m, q|
168
+ q == 0.0
169
+ }.each { |m, _|
170
+ encoding_candidates.delete(m)
171
+ }
172
+
173
+ return (encoding_candidates & available_encodings)[0]
174
+ end
175
+ module_function :select_best_encoding
176
+
177
+ def set_cookie_header!(header, key, value)
178
+ case value
179
+ when Hash
180
+ domain = "; domain=" + value[:domain] if value[:domain]
181
+ path = "; path=" + value[:path] if value[:path]
182
+ # According to RFC 2109, we need dashes here.
183
+ # N.B.: cgi.rb uses spaces...
184
+ expires = "; expires=" +
185
+ rfc2822(value[:expires].clone.gmtime) if value[:expires]
186
+ secure = "; secure" if value[:secure]
187
+ httponly = "; HttpOnly" if value[:httponly]
188
+ value = value[:value]
189
+ end
190
+ value = [value] unless Array === value
191
+ cookie = escape(key) + "=" +
192
+ value.map { |v| escape v }.join("&") +
193
+ "#{domain}#{path}#{expires}#{secure}#{httponly}"
194
+
195
+ case header["Set-Cookie"]
196
+ when nil, ''
197
+ header["Set-Cookie"] = cookie
198
+ when String
199
+ header["Set-Cookie"] = [header["Set-Cookie"], cookie].join("\n")
200
+ when Array
201
+ header["Set-Cookie"] = (header["Set-Cookie"] + [cookie]).join("\n")
202
+ end
203
+
204
+ nil
205
+ end
206
+ module_function :set_cookie_header!
207
+
208
+ def delete_cookie_header!(header, key, value = {})
209
+ case header["Set-Cookie"]
210
+ when nil, ''
211
+ cookies = []
212
+ when String
213
+ cookies = header["Set-Cookie"].split("\n")
214
+ when Array
215
+ cookies = header["Set-Cookie"]
216
+ end
217
+
218
+ cookies.reject! { |cookie|
219
+ if value[:domain]
220
+ cookie =~ /\A#{escape(key)}=.*domain=#{value[:domain]}/
221
+ else
222
+ cookie =~ /\A#{escape(key)}=/
223
+ end
224
+ }
225
+
226
+ header["Set-Cookie"] = cookies.join("\n")
227
+
228
+ set_cookie_header!(header, key,
229
+ {:value => '', :path => nil, :domain => nil,
230
+ :expires => Time.at(0) }.merge(value))
231
+
232
+ nil
233
+ end
234
+ module_function :delete_cookie_header!
235
+
236
+ # Return the bytesize of String; uses String#size under Ruby 1.8 and
237
+ # String#bytesize under 1.9.
238
+ if ''.respond_to?(:bytesize)
239
+ def bytesize(string)
240
+ string.bytesize
241
+ end
242
+ else
243
+ def bytesize(string)
244
+ string.size
245
+ end
246
+ end
247
+ module_function :bytesize
248
+
249
+ # Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
250
+ # of '% %b %Y'.
251
+ # It assumes that the time is in GMT to comply to the RFC 2109.
252
+ #
253
+ # NOTE: I'm not sure the RFC says it requires GMT, but is ambigous enough
254
+ # that I'm certain someone implemented only that option.
255
+ # Do not use %a and %b from Time.strptime, it would use localized names for
256
+ # weekday and month.
257
+ #
258
+ def rfc2822(time)
259
+ wday = Time::RFC2822_DAY_NAME[time.wday]
260
+ mon = Time::RFC2822_MONTH_NAME[time.mon - 1]
261
+ time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
262
+ end
263
+ module_function :rfc2822
264
+
265
+ # Parses the "Range:" header, if present, into an array of Range objects.
266
+ # Returns nil if the header is missing or syntactically invalid.
267
+ # Returns an empty array if none of the ranges are satisfiable.
268
+ def byte_ranges(env, size)
269
+ # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
270
+ http_range = env['HTTP_RANGE']
271
+ return nil unless http_range
272
+ ranges = []
273
+ http_range.split(/,\s*/).each do |range_spec|
274
+ matches = range_spec.match(/bytes=(\d*)-(\d*)/)
275
+ return nil unless matches
276
+ r0,r1 = matches[1], matches[2]
277
+ if r0.empty?
278
+ return nil if r1.empty?
279
+ # suffix-byte-range-spec, represents trailing suffix of file
280
+ r0 = [size - r1.to_i, 0].max
281
+ r1 = size - 1
282
+ else
283
+ r0 = r0.to_i
284
+ if r1.empty?
285
+ r1 = size - 1
286
+ else
287
+ r1 = r1.to_i
288
+ return nil if r1 < r0 # backwards range is syntactically invalid
289
+ r1 = size-1 if r1 >= size
290
+ end
291
+ end
292
+ ranges << (r0..r1) if r0 <= r1
293
+ end
294
+ ranges
295
+ end
296
+ module_function :byte_ranges
297
+
298
+ # Context allows the use of a compatible middleware at different points
299
+ # in a request handling stack. A compatible middleware must define
300
+ # #context which should take the arguments env and app. The first of which
301
+ # would be the request environment. The second of which would be the rack
302
+ # application that the request would be forwarded to.
303
+ class Context
304
+ attr_reader :for, :app
305
+
306
+ def initialize(app_f, app_r)
307
+ raise 'running context does not respond to #context' unless app_f.respond_to? :context
308
+ @for, @app = app_f, app_r
309
+ end
310
+
311
+ def call(env)
312
+ @for.context(env, @app)
313
+ end
314
+
315
+ def recontext(app)
316
+ self.class.new(@for, app)
317
+ end
318
+
319
+ def context(env, app=@app)
320
+ recontext(app).call(env)
321
+ end
322
+ end
323
+
324
+ # A case-insensitive Hash that preserves the original case of a
325
+ # header when set.
326
+ class HeaderHash < Hash
327
+ def self.new(hash={})
328
+ HeaderHash === hash ? hash : super(hash)
329
+ end
330
+
331
+ def initialize(hash={})
332
+ super()
333
+ @names = {}
334
+ hash.each { |k, v| self[k] = v }
335
+ end
336
+
337
+ def each
338
+ super do |k, v|
339
+ yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
340
+ end
341
+ end
342
+
343
+ def to_hash
344
+ Hash[*map do |k, v|
345
+ [k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v]
346
+ end.flatten]
347
+ end
348
+
349
+ def [](k)
350
+ super(k) || super(@names[k.downcase])
351
+ end
352
+
353
+ def []=(k, v)
354
+ canonical = k.downcase
355
+ delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
356
+ @names[k] = @names[canonical] = k
357
+ super k, v
358
+ end
359
+
360
+ def delete(k)
361
+ canonical = k.downcase
362
+ result = super @names.delete(canonical)
363
+ @names.delete_if { |name,| name.downcase == canonical }
364
+ result
365
+ end
366
+
367
+ def include?(k)
368
+ @names.include?(k) || @names.include?(k.downcase)
369
+ end
370
+
371
+ alias_method :has_key?, :include?
372
+ alias_method :member?, :include?
373
+ alias_method :key?, :include?
374
+
375
+ def merge!(other)
376
+ other.each { |k, v| self[k] = v }
377
+ self
378
+ end
379
+
380
+ def merge(other)
381
+ hash = dup
382
+ hash.merge! other
383
+ end
384
+
385
+ def replace(other)
386
+ clear
387
+ other.each { |k, v| self[k] = v }
388
+ self
389
+ end
390
+ end
391
+
392
+ # Every standard HTTP code mapped to the appropriate message.
393
+ # Generated with:
394
+ # curl -s http://www.iana.org/assignments/http-status-codes | \
395
+ # ruby -ane 'm = /^(\d{3}) +(\S[^\[(]+)/.match($_) and
396
+ # puts " #{m[1]} => \x27#{m[2].strip}x27,"'
397
+ HTTP_STATUS_CODES = {
398
+ 100 => 'Continue',
399
+ 101 => 'Switching Protocols',
400
+ 102 => 'Processing',
401
+ 200 => 'OK',
402
+ 201 => 'Created',
403
+ 202 => 'Accepted',
404
+ 203 => 'Non-Authoritative Information',
405
+ 204 => 'No Content',
406
+ 205 => 'Reset Content',
407
+ 206 => 'Partial Content',
408
+ 207 => 'Multi-Status',
409
+ 226 => 'IM Used',
410
+ 300 => 'Multiple Choices',
411
+ 301 => 'Moved Permanently',
412
+ 302 => 'Found',
413
+ 303 => 'See Other',
414
+ 304 => 'Not Modified',
415
+ 305 => 'Use Proxy',
416
+ 306 => 'Reserved',
417
+ 307 => 'Temporary Redirect',
418
+ 400 => 'Bad Request',
419
+ 401 => 'Unauthorized',
420
+ 402 => 'Payment Required',
421
+ 403 => 'Forbidden',
422
+ 404 => 'Not Found',
423
+ 405 => 'Method Not Allowed',
424
+ 406 => 'Not Acceptable',
425
+ 407 => 'Proxy Authentication Required',
426
+ 408 => 'Request Timeout',
427
+ 409 => 'Conflict',
428
+ 410 => 'Gone',
429
+ 411 => 'Length Required',
430
+ 412 => 'Precondition Failed',
431
+ 413 => 'Request Entity Too Large',
432
+ 414 => 'Request-URI Too Long',
433
+ 415 => 'Unsupported Media Type',
434
+ 416 => 'Requested Range Not Satisfiable',
435
+ 417 => 'Expectation Failed',
436
+ 422 => 'Unprocessable Entity',
437
+ 423 => 'Locked',
438
+ 424 => 'Failed Dependency',
439
+ 426 => 'Upgrade Required',
440
+ 500 => 'Internal Server Error',
441
+ 501 => 'Not Implemented',
442
+ 502 => 'Bad Gateway',
443
+ 503 => 'Service Unavailable',
444
+ 504 => 'Gateway Timeout',
445
+ 505 => 'HTTP Version Not Supported',
446
+ 506 => 'Variant Also Negotiates',
447
+ 507 => 'Insufficient Storage',
448
+ 510 => 'Not Extended',
449
+ }
450
+
451
+ # Responses with HTTP status codes that should not have an entity body
452
+ STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 304)
453
+
454
+ SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
455
+ [message.downcase.gsub(/\s|-/, '_').to_sym, code]
456
+ }.flatten]
457
+
458
+ def status_code(status)
459
+ if status.is_a?(Symbol)
460
+ SYMBOL_TO_STATUS_CODE[status] || 500
461
+ else
462
+ status.to_i
463
+ end
464
+ end
465
+ module_function :status_code
466
+
467
+ # A multipart form data parser, adapted from IOWA.
468
+ #
469
+ # Usually, Rack::Request#POST takes care of calling this.
470
+
471
+ module Multipart
472
+ class UploadedFile
473
+ # The filename, *not* including the path, of the "uploaded" file
474
+ attr_reader :original_filename
475
+
476
+ # The content type of the "uploaded" file
477
+ attr_accessor :content_type
478
+
479
+ def initialize(path, content_type = "text/plain", binary = false)
480
+ raise "#{path} file does not exist" unless ::File.exist?(path)
481
+ @content_type = content_type
482
+ @original_filename = ::File.basename(path)
483
+ @tempfile = Tempfile.new(@original_filename)
484
+ @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
485
+ @tempfile.binmode if binary
486
+ FileUtils.copy_file(path, @tempfile.path)
487
+ end
488
+
489
+ def path
490
+ @tempfile.path
491
+ end
492
+ alias_method :local_path, :path
493
+
494
+ def method_missing(method_name, *args, &block) #:nodoc:
495
+ @tempfile.__send__(method_name, *args, &block)
496
+ end
497
+ end
498
+
499
+ EOL = "\r\n"
500
+ MULTIPART_BOUNDARY = "AaB03x"
501
+ MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n
502
+ TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
503
+ CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
504
+ DISPPARM = /;\s*(#{TOKEN})=("(?:\\"|[^"])*"|#{TOKEN})*/
505
+ RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
506
+ BROKEN_QUOTED = /^#{CONDISP}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i
507
+ BROKEN_UNQUOTED = /^#{CONDISP}.*;\sfilename=(#{TOKEN})/i
508
+ MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
509
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*\s+name="?([^\";]*)"?/ni
510
+ MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
511
+
512
+ def self.parse_multipart(env)
513
+ unless env['CONTENT_TYPE'] =~ MULTIPART
514
+ nil
515
+ else
516
+ boundary = "--#{$1}"
517
+
518
+ params = {}
519
+ buf = ""
520
+ content_length = env['CONTENT_LENGTH'].to_i
521
+ input = env['rack.input']
522
+ input.rewind
523
+
524
+ boundary_size = Utils.bytesize(boundary) + EOL.size
525
+ bufsize = 16384
526
+
527
+ content_length -= boundary_size
528
+
529
+ read_buffer = nil
530
+
531
+ loop do
532
+ read_buffer = input.gets
533
+ break if read_buffer == boundary + EOL
534
+ end
535
+
536
+ rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/n
537
+
538
+ loop {
539
+ head = nil
540
+ body = ''
541
+ filename = content_type = name = nil
542
+
543
+ until head && buf =~ rx
544
+ if !head && i = buf.index(EOL+EOL)
545
+ head = buf.slice!(0, i+2) # First \r\n
546
+ buf.slice!(0, 2) # Second \r\n
547
+
548
+ if head =~ RFC2183
549
+ filename = Hash[head.scan(DISPPARM)]['filename']
550
+ filename = $1 if filename and filename =~ /^"(.*)"$/
551
+ elsif head =~ BROKEN_QUOTED
552
+ filename = $1
553
+ elsif head =~ BROKEN_UNQUOTED
554
+ filename = $1
555
+ end
556
+
557
+ if filename && filename !~ /\\[^\\"]/
558
+ filename = Utils.unescape(filename).gsub(/\\(.)/, '\1')
559
+ end
560
+
561
+ content_type = head[MULTIPART_CONTENT_TYPE, 1]
562
+ name = head[MULTIPART_CONTENT_DISPOSITION, 1] || head[MULTIPART_CONTENT_ID, 1]
563
+
564
+ if filename
565
+ body = Tempfile.new("RackMultipart")
566
+ body.binmode if body.respond_to?(:binmode)
567
+ end
568
+
569
+ next
570
+ end
571
+
572
+ # Save the read body part.
573
+ if head && (boundary_size+4 < buf.size)
574
+ body << buf.slice!(0, buf.size - (boundary_size+4))
575
+ end
576
+
577
+ c = input.read(bufsize < content_length ? bufsize : content_length, read_buffer)
578
+ raise EOFError, "bad content body" if c.nil? || c.empty?
579
+ buf << c
580
+ content_length -= c.size
581
+ end
582
+
583
+ # Save the rest.
584
+ if i = buf.index(rx)
585
+ body << buf.slice!(0, i)
586
+ buf.slice!(0, boundary_size+2)
587
+
588
+ content_length = -1 if $1 == "--"
589
+ end
590
+
591
+ if filename == ""
592
+ # filename is blank which means no file has been selected
593
+ data = nil
594
+ elsif filename
595
+ body.rewind
596
+
597
+ # Take the basename of the upload's original filename.
598
+ # This handles the full Windows paths given by Internet Explorer
599
+ # (and perhaps other broken user agents) without affecting
600
+ # those which give the lone filename.
601
+ filename = filename.split(/[\/\\]/).last
602
+
603
+ data = {:filename => filename, :type => content_type,
604
+ :name => name, :tempfile => body, :head => head}
605
+ elsif !filename && content_type && body.is_a?(IO)
606
+ body.rewind
607
+
608
+ # Generic multipart cases, not coming from a form
609
+ data = {:type => content_type,
610
+ :name => name, :tempfile => body, :head => head}
611
+ else
612
+ data = body
613
+ end
614
+
615
+ Utils.normalize_params(params, name, data) unless data.nil?
616
+
617
+ # break if we're at the end of a buffer, but not if it is the end of a field
618
+ break if (buf.empty? && $1 != EOL) || content_length == -1
619
+ }
620
+
621
+ input.rewind
622
+
623
+ params
624
+ end
625
+ end
626
+
627
+ def self.build_multipart(params, first = true)
628
+ if first
629
+ unless params.is_a?(Hash)
630
+ raise ArgumentError, "value must be a Hash"
631
+ end
632
+
633
+ multipart = false
634
+ query = lambda { |value|
635
+ case value
636
+ when Array
637
+ value.each(&query)
638
+ when Hash
639
+ value.values.each(&query)
640
+ when UploadedFile
641
+ multipart = true
642
+ end
643
+ }
644
+ params.values.each(&query)
645
+ return nil unless multipart
646
+ end
647
+
648
+ flattened_params = Hash.new
649
+
650
+ params.each do |key, value|
651
+ k = first ? key.to_s : "[#{key}]"
652
+
653
+ case value
654
+ when Array
655
+ value.map { |v|
656
+ build_multipart(v, false).each { |subkey, subvalue|
657
+ flattened_params["#{k}[]#{subkey}"] = subvalue
658
+ }
659
+ }
660
+ when Hash
661
+ build_multipart(value, false).each { |subkey, subvalue|
662
+ flattened_params[k + subkey] = subvalue
663
+ }
664
+ else
665
+ flattened_params[k] = value
666
+ end
667
+ end
668
+
669
+ if first
670
+ flattened_params.map { |name, file|
671
+ if file.respond_to?(:original_filename)
672
+ ::File.open(file.path, "rb") do |f|
673
+ f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
674
+ <<-EOF
675
+ --#{MULTIPART_BOUNDARY}\r
676
+ Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
677
+ Content-Type: #{file.content_type}\r
678
+ Content-Length: #{::File.stat(file.path).size}\r
679
+ \r
680
+ #{f.read}\r
681
+ EOF
682
+ end
683
+ else
684
+ <<-EOF
685
+ --#{MULTIPART_BOUNDARY}\r
686
+ Content-Disposition: form-data; name="#{name}"\r
687
+ \r
688
+ #{file}\r
689
+ EOF
690
+ end
691
+ }.join + "--#{MULTIPART_BOUNDARY}--\r"
692
+ else
693
+ flattened_params
694
+ end
695
+ end
696
+ end
697
+ end
698
+ end