edgar-rack 1.2.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 (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