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.
- data/COPYING +18 -0
- data/KNOWN-ISSUES +21 -0
- data/README +401 -0
- data/Rakefile +101 -0
- data/SPEC +171 -0
- data/bin/rackup +4 -0
- data/contrib/rack_logo.svg +111 -0
- data/example/lobster.ru +4 -0
- data/example/protectedlobster.rb +14 -0
- data/example/protectedlobster.ru +8 -0
- data/lib/rack.rb +81 -0
- data/lib/rack/auth/abstract/handler.rb +37 -0
- data/lib/rack/auth/abstract/request.rb +43 -0
- data/lib/rack/auth/basic.rb +58 -0
- data/lib/rack/auth/digest/md5.rb +124 -0
- data/lib/rack/auth/digest/nonce.rb +51 -0
- data/lib/rack/auth/digest/params.rb +53 -0
- data/lib/rack/auth/digest/request.rb +40 -0
- data/lib/rack/builder.rb +80 -0
- data/lib/rack/cascade.rb +41 -0
- data/lib/rack/chunked.rb +52 -0
- data/lib/rack/commonlogger.rb +49 -0
- data/lib/rack/conditionalget.rb +63 -0
- data/lib/rack/config.rb +15 -0
- data/lib/rack/content_length.rb +29 -0
- data/lib/rack/content_type.rb +23 -0
- data/lib/rack/deflater.rb +96 -0
- data/lib/rack/directory.rb +157 -0
- data/lib/rack/etag.rb +59 -0
- data/lib/rack/file.rb +118 -0
- data/lib/rack/handler.rb +88 -0
- data/lib/rack/handler/cgi.rb +61 -0
- data/lib/rack/handler/evented_mongrel.rb +8 -0
- data/lib/rack/handler/fastcgi.rb +90 -0
- data/lib/rack/handler/lsws.rb +61 -0
- data/lib/rack/handler/mongrel.rb +90 -0
- data/lib/rack/handler/scgi.rb +59 -0
- data/lib/rack/handler/swiftiplied_mongrel.rb +8 -0
- data/lib/rack/handler/thin.rb +17 -0
- data/lib/rack/handler/webrick.rb +73 -0
- data/lib/rack/head.rb +19 -0
- data/lib/rack/lint.rb +567 -0
- data/lib/rack/lobster.rb +65 -0
- data/lib/rack/lock.rb +44 -0
- data/lib/rack/logger.rb +18 -0
- data/lib/rack/methodoverride.rb +27 -0
- data/lib/rack/mime.rb +210 -0
- data/lib/rack/mock.rb +185 -0
- data/lib/rack/nulllogger.rb +18 -0
- data/lib/rack/recursive.rb +61 -0
- data/lib/rack/reloader.rb +109 -0
- data/lib/rack/request.rb +307 -0
- data/lib/rack/response.rb +151 -0
- data/lib/rack/rewindable_input.rb +104 -0
- data/lib/rack/runtime.rb +27 -0
- data/lib/rack/sendfile.rb +139 -0
- data/lib/rack/server.rb +289 -0
- data/lib/rack/session/abstract/id.rb +348 -0
- data/lib/rack/session/cookie.rb +152 -0
- data/lib/rack/session/memcache.rb +93 -0
- data/lib/rack/session/pool.rb +79 -0
- data/lib/rack/showexceptions.rb +378 -0
- data/lib/rack/showstatus.rb +113 -0
- data/lib/rack/static.rb +53 -0
- data/lib/rack/urlmap.rb +55 -0
- data/lib/rack/utils.rb +698 -0
- data/rack.gemspec +39 -0
- data/test/cgi/lighttpd.conf +25 -0
- data/test/cgi/rackup_stub.rb +6 -0
- data/test/cgi/sample_rackup.ru +5 -0
- data/test/cgi/test +9 -0
- data/test/cgi/test.fcgi +8 -0
- data/test/cgi/test.ru +5 -0
- data/test/gemloader.rb +6 -0
- data/test/multipart/bad_robots +259 -0
- data/test/multipart/binary +0 -0
- data/test/multipart/empty +10 -0
- data/test/multipart/fail_16384_nofile +814 -0
- data/test/multipart/file1.txt +1 -0
- data/test/multipart/filename_and_modification_param +7 -0
- data/test/multipart/filename_with_escaped_quotes +6 -0
- data/test/multipart/filename_with_escaped_quotes_and_modification_param +7 -0
- data/test/multipart/filename_with_percent_escaped_quotes +6 -0
- data/test/multipart/filename_with_unescaped_quotes +6 -0
- data/test/multipart/ie +6 -0
- data/test/multipart/nested +10 -0
- data/test/multipart/none +9 -0
- data/test/multipart/semicolon +6 -0
- data/test/multipart/text +15 -0
- data/test/rackup/config.ru +31 -0
- data/test/spec_auth_basic.rb +70 -0
- data/test/spec_auth_digest.rb +241 -0
- data/test/spec_builder.rb +123 -0
- data/test/spec_cascade.rb +45 -0
- data/test/spec_cgi.rb +102 -0
- data/test/spec_chunked.rb +60 -0
- data/test/spec_commonlogger.rb +56 -0
- data/test/spec_conditionalget.rb +86 -0
- data/test/spec_config.rb +23 -0
- data/test/spec_content_length.rb +36 -0
- data/test/spec_content_type.rb +29 -0
- data/test/spec_deflater.rb +125 -0
- data/test/spec_directory.rb +57 -0
- data/test/spec_etag.rb +75 -0
- data/test/spec_fastcgi.rb +107 -0
- data/test/spec_file.rb +92 -0
- data/test/spec_handler.rb +49 -0
- data/test/spec_head.rb +30 -0
- data/test/spec_lint.rb +515 -0
- data/test/spec_lobster.rb +43 -0
- data/test/spec_lock.rb +142 -0
- data/test/spec_logger.rb +28 -0
- data/test/spec_methodoverride.rb +58 -0
- data/test/spec_mock.rb +241 -0
- data/test/spec_mongrel.rb +182 -0
- data/test/spec_nulllogger.rb +12 -0
- data/test/spec_recursive.rb +69 -0
- data/test/spec_request.rb +774 -0
- data/test/spec_response.rb +245 -0
- data/test/spec_rewindable_input.rb +118 -0
- data/test/spec_runtime.rb +39 -0
- data/test/spec_sendfile.rb +83 -0
- data/test/spec_server.rb +8 -0
- data/test/spec_session_abstract_id.rb +43 -0
- data/test/spec_session_cookie.rb +171 -0
- data/test/spec_session_memcache.rb +289 -0
- data/test/spec_session_pool.rb +200 -0
- data/test/spec_showexceptions.rb +87 -0
- data/test/spec_showstatus.rb +79 -0
- data/test/spec_static.rb +48 -0
- data/test/spec_thin.rb +86 -0
- data/test/spec_urlmap.rb +213 -0
- data/test/spec_utils.rb +678 -0
- data/test/spec_webrick.rb +141 -0
- data/test/testrequest.rb +78 -0
- data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
- data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
- 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
|
data/lib/rack/static.rb
ADDED
|
@@ -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
|
data/lib/rack/urlmap.rb
ADDED
|
@@ -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
|
+
|
data/lib/rack/utils.rb
ADDED
|
@@ -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
|
+
"&" => "&",
|
|
133
|
+
"<" => "<",
|
|
134
|
+
">" => ">",
|
|
135
|
+
"'" => "'",
|
|
136
|
+
'"' => """,
|
|
137
|
+
"/" => "/"
|
|
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
|