lack 2.0.0
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.
- checksums.yaml +7 -0
- data/bin/rackup +5 -0
- data/lib/rack.rb +26 -0
- data/lib/rack/body_proxy.rb +39 -0
- data/lib/rack/builder.rb +166 -0
- data/lib/rack/handler.rb +63 -0
- data/lib/rack/handler/webrick.rb +120 -0
- data/lib/rack/mime.rb +661 -0
- data/lib/rack/mock.rb +198 -0
- data/lib/rack/multipart.rb +31 -0
- data/lib/rack/multipart/generator.rb +93 -0
- data/lib/rack/multipart/parser.rb +239 -0
- data/lib/rack/multipart/uploaded_file.rb +34 -0
- data/lib/rack/request.rb +394 -0
- data/lib/rack/response.rb +160 -0
- data/lib/rack/server.rb +258 -0
- data/lib/rack/server/options.rb +121 -0
- data/lib/rack/utils.rb +653 -0
- data/lib/rack/version.rb +3 -0
- data/spec/spec_helper.rb +1 -0
- data/test/builder/anything.rb +5 -0
- data/test/builder/comment.ru +4 -0
- data/test/builder/end.ru +5 -0
- data/test/builder/line.ru +1 -0
- data/test/builder/options.ru +2 -0
- data/test/multipart/bad_robots +259 -0
- data/test/multipart/binary +0 -0
- data/test/multipart/content_type_and_no_filename +6 -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_and_no_name +6 -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_percentages +6 -0
- data/test/multipart/filename_with_unescaped_percentages2 +6 -0
- data/test/multipart/filename_with_unescaped_percentages3 +6 -0
- data/test/multipart/filename_with_unescaped_quotes +6 -0
- data/test/multipart/ie +6 -0
- data/test/multipart/invalid_character +6 -0
- data/test/multipart/mixed_files +21 -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/multipart/webkit +32 -0
- data/test/rackup/config.ru +31 -0
- data/test/registering_handler/rack/handler/registering_myself.rb +8 -0
- data/test/spec_body_proxy.rb +69 -0
- data/test/spec_builder.rb +223 -0
- data/test/spec_chunked.rb +101 -0
- data/test/spec_file.rb +221 -0
- data/test/spec_handler.rb +59 -0
- data/test/spec_head.rb +45 -0
- data/test/spec_lint.rb +522 -0
- data/test/spec_mime.rb +51 -0
- data/test/spec_mock.rb +277 -0
- data/test/spec_multipart.rb +547 -0
- data/test/spec_recursive.rb +72 -0
- data/test/spec_request.rb +1199 -0
- data/test/spec_response.rb +343 -0
- data/test/spec_rewindable_input.rb +118 -0
- data/test/spec_sendfile.rb +130 -0
- data/test/spec_server.rb +167 -0
- data/test/spec_utils.rb +635 -0
- data/test/spec_webrick.rb +184 -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 +240 -0
data/lib/rack/server.rb
ADDED
@@ -0,0 +1,258 @@
|
|
1
|
+
module Rack
|
2
|
+
class Server
|
3
|
+
require_relative "server/options"
|
4
|
+
|
5
|
+
# Start a new rack server (like running rackup). This will parse ARGV and
|
6
|
+
# provide standard ARGV rackup options, defaulting to load 'config.ru'.
|
7
|
+
#
|
8
|
+
# Providing an options hash will prevent ARGV parsing and will not include
|
9
|
+
# any default options.
|
10
|
+
#
|
11
|
+
# This method can be used to very easily launch a CGI application, for
|
12
|
+
# example:
|
13
|
+
#
|
14
|
+
# Rack::Server.start(
|
15
|
+
# :app => lambda do |e|
|
16
|
+
# [200, {'Content-Type' => 'text/html'}, ['hello world']]
|
17
|
+
# end,
|
18
|
+
# :server => 'cgi'
|
19
|
+
# )
|
20
|
+
#
|
21
|
+
# Further options available here are documented on Rack::Server#initialize
|
22
|
+
def self.start(options = nil)
|
23
|
+
new(options).start
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_writer :options
|
27
|
+
|
28
|
+
# Options may include:
|
29
|
+
# * :app
|
30
|
+
# a rack application to run (overrides :config)
|
31
|
+
# * :config
|
32
|
+
# a rackup configuration file path to load (.ru)
|
33
|
+
# * :environment
|
34
|
+
# this selects the middleware that will be wrapped around
|
35
|
+
# your application. Default options available are:
|
36
|
+
# - development: CommonLogger, ShowExceptions, and Lint
|
37
|
+
# - deployment: CommonLogger
|
38
|
+
# - none: no extra middleware
|
39
|
+
# note: when the server is a cgi server, CommonLogger is not included.
|
40
|
+
# * :server
|
41
|
+
# choose a specific Rack::Handler, e.g. cgi, fcgi, webrick
|
42
|
+
# * :daemonize
|
43
|
+
# if true, the server will daemonize itself (fork, detach, etc)
|
44
|
+
# * :pid
|
45
|
+
# path to write a pid file after daemonize
|
46
|
+
# * :Host
|
47
|
+
# the host address to bind to (used by supporting Rack::Handler)
|
48
|
+
# * :Port
|
49
|
+
# the port to bind to (used by supporting Rack::Handler)
|
50
|
+
# * :AccessLog
|
51
|
+
# webrick access log options (or supporting Rack::Handler)
|
52
|
+
# * :debug
|
53
|
+
# turn on debug output ($DEBUG = true)
|
54
|
+
# * :warn
|
55
|
+
# turn on warnings ($-w = true)
|
56
|
+
# * :include
|
57
|
+
# add given paths to $LOAD_PATH
|
58
|
+
# * :require
|
59
|
+
# require the given libraries
|
60
|
+
def initialize(options = nil)
|
61
|
+
@options = options
|
62
|
+
@app = options[:app] if options && options[:app]
|
63
|
+
end
|
64
|
+
|
65
|
+
def options
|
66
|
+
@options ||= parse_options(ARGV)
|
67
|
+
end
|
68
|
+
|
69
|
+
def default_options
|
70
|
+
environment = ENV['RACK_ENV'] || 'development'
|
71
|
+
default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
|
72
|
+
|
73
|
+
{
|
74
|
+
:environment => environment,
|
75
|
+
:pid => nil,
|
76
|
+
:Port => 9292,
|
77
|
+
:Host => default_host,
|
78
|
+
:AccessLog => [],
|
79
|
+
:config => "config.ru"
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def app
|
84
|
+
@app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.logging_middleware
|
88
|
+
lambda { |server|
|
89
|
+
server.server.name =~ /CGI/ || server.options[:quiet] ? nil : [Rack::CommonLogger, $stderr]
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.default_middleware_by_environment
|
94
|
+
m = Hash.new {|h,k| h[k] = []}
|
95
|
+
m["deployment"] = [
|
96
|
+
[Rack::ContentLength],
|
97
|
+
[Rack::Chunked],
|
98
|
+
logging_middleware,
|
99
|
+
[Rack::TempfileReaper]
|
100
|
+
]
|
101
|
+
m["development"] = [
|
102
|
+
[Rack::ContentLength],
|
103
|
+
[Rack::Chunked],
|
104
|
+
logging_middleware,
|
105
|
+
[Rack::ShowExceptions],
|
106
|
+
[Rack::Lint],
|
107
|
+
[Rack::TempfileReaper]
|
108
|
+
]
|
109
|
+
|
110
|
+
m
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.middleware
|
114
|
+
default_middleware_by_environment
|
115
|
+
end
|
116
|
+
|
117
|
+
def middleware
|
118
|
+
self.class.middleware
|
119
|
+
end
|
120
|
+
|
121
|
+
def start(&block)
|
122
|
+
if options[:warn]
|
123
|
+
$-w = true
|
124
|
+
end
|
125
|
+
|
126
|
+
if includes = options[:include]
|
127
|
+
$LOAD_PATH.unshift(*includes)
|
128
|
+
end
|
129
|
+
|
130
|
+
if library = options[:require]
|
131
|
+
require library
|
132
|
+
end
|
133
|
+
|
134
|
+
if options[:debug]
|
135
|
+
$DEBUG = true
|
136
|
+
require 'pp'
|
137
|
+
p options[:server]
|
138
|
+
pp wrapped_app
|
139
|
+
pp app
|
140
|
+
end
|
141
|
+
|
142
|
+
check_pid! if options[:pid]
|
143
|
+
|
144
|
+
# Touch the wrapped app, so that the config.ru is loaded before
|
145
|
+
# daemonization (i.e. before chdir, etc).
|
146
|
+
wrapped_app
|
147
|
+
|
148
|
+
daemonize_app if options[:daemonize]
|
149
|
+
|
150
|
+
write_pid if options[:pid]
|
151
|
+
|
152
|
+
trap(:INT) do
|
153
|
+
if server.respond_to?(:shutdown)
|
154
|
+
server.shutdown
|
155
|
+
else
|
156
|
+
exit
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
server.run wrapped_app, options, &block
|
161
|
+
end
|
162
|
+
|
163
|
+
def server
|
164
|
+
@_server ||= Rack::Handler.get(options[:server]) || Rack::Handler.default(options)
|
165
|
+
end
|
166
|
+
|
167
|
+
private def build_app_and_options_from_config
|
168
|
+
if !::File.exist? options[:config]
|
169
|
+
abort "configuration #{options[:config]} not found"
|
170
|
+
end
|
171
|
+
|
172
|
+
app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
|
173
|
+
self.options.merge! options
|
174
|
+
app
|
175
|
+
end
|
176
|
+
|
177
|
+
private def build_app_from_string
|
178
|
+
Rack::Builder.new_from_string(self.options[:builder])
|
179
|
+
end
|
180
|
+
|
181
|
+
private def parse_options(args)
|
182
|
+
options = default_options
|
183
|
+
|
184
|
+
# Don't evaluate CGI ISINDEX parameters.
|
185
|
+
# http://www.meb.uni-bonn.de/docs/cgi/cl.html
|
186
|
+
args.clear if ENV.include?("REQUEST_METHOD")
|
187
|
+
|
188
|
+
options.merge! opt_parser.parse!(args)
|
189
|
+
options[:config] = ::File.expand_path(options[:config])
|
190
|
+
ENV["RACK_ENV"] = options[:environment]
|
191
|
+
options
|
192
|
+
end
|
193
|
+
|
194
|
+
private def opt_parser
|
195
|
+
Options.new
|
196
|
+
end
|
197
|
+
|
198
|
+
private def build_app(app)
|
199
|
+
middleware[options[:environment]].reverse_each do |middleware|
|
200
|
+
middleware = middleware.call(self) if middleware.respond_to?(:call)
|
201
|
+
next unless middleware
|
202
|
+
klass, *args = middleware
|
203
|
+
app = klass.new(app, *args)
|
204
|
+
end
|
205
|
+
app
|
206
|
+
end
|
207
|
+
|
208
|
+
private def wrapped_app
|
209
|
+
@wrapped_app ||= build_app app
|
210
|
+
end
|
211
|
+
|
212
|
+
private def daemonize_app
|
213
|
+
if RUBY_VERSION < "1.9"
|
214
|
+
exit if fork
|
215
|
+
Process.setsid
|
216
|
+
exit if fork
|
217
|
+
Dir.chdir "/"
|
218
|
+
STDIN.reopen "/dev/null"
|
219
|
+
STDOUT.reopen "/dev/null", "a"
|
220
|
+
STDERR.reopen "/dev/null", "a"
|
221
|
+
else
|
222
|
+
Process.daemon
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
private def write_pid
|
227
|
+
::File.open(options[:pid], ::File::CREAT | ::File::EXCL | ::File::WRONLY ){ |f| f.write("#{Process.pid}") }
|
228
|
+
at_exit { ::File.delete(options[:pid]) if ::File.exist?(options[:pid]) }
|
229
|
+
rescue Errno::EEXIST
|
230
|
+
check_pid!
|
231
|
+
retry
|
232
|
+
end
|
233
|
+
|
234
|
+
private def check_pid!
|
235
|
+
case pidfile_process_status
|
236
|
+
when :running, :not_owned
|
237
|
+
$stderr.puts "A server is already running. Check #{options[:pid]}."
|
238
|
+
exit(1)
|
239
|
+
when :dead
|
240
|
+
::File.delete(options[:pid])
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
private def pidfile_process_status
|
245
|
+
return :exited unless ::File.exist?(options[:pid])
|
246
|
+
|
247
|
+
pid = ::File.read(options[:pid]).to_i
|
248
|
+
return :dead if pid == 0
|
249
|
+
|
250
|
+
Process.kill(0, pid)
|
251
|
+
:running
|
252
|
+
rescue Errno::ESRCH
|
253
|
+
:dead
|
254
|
+
rescue Errno::EPERM
|
255
|
+
:not_owned
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module Rack
|
2
|
+
class Server
|
3
|
+
class Options
|
4
|
+
def parse!(args)
|
5
|
+
options = {}
|
6
|
+
opt_parser = OptionParser.new("", 24, " ") do |opts|
|
7
|
+
opts.banner = "Usage: rackup [ruby options] [rack options] [rackup config]"
|
8
|
+
opts.separator ""
|
9
|
+
opts.separator "Ruby options:"
|
10
|
+
|
11
|
+
lineno = 1
|
12
|
+
|
13
|
+
opts.on("--eval LINE", "evaluate a LINE of code") do |line|
|
14
|
+
eval line, TOPLEVEL_BINDING, "-e", lineno
|
15
|
+
lineno += 1
|
16
|
+
end
|
17
|
+
|
18
|
+
opts.on("--builder BUILDER_LINE", "evaluate a BUILDER_LINE of code as a builder script") do |line|
|
19
|
+
options[:builder] = line
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on("--debug", "set debugging flags (set $DEBUG to true)") do
|
23
|
+
options[:debug] = true
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on("--warn", "turn warnings on for your script") do
|
27
|
+
options[:warn] = true
|
28
|
+
end
|
29
|
+
|
30
|
+
opts.on("--quiet", "turn off logging") do
|
31
|
+
options[:quiet] = true
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on("--include PATH",
|
35
|
+
"specify $LOAD_PATH (may be used more than once)") do |path|
|
36
|
+
(options[:include] ||= []).concat(path.split(":"))
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on("--require LIBRARY", "require the library, before executing your script") do |library|
|
40
|
+
options[:require] = library
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.separator ""
|
44
|
+
opts.separator "Rack options:"
|
45
|
+
|
46
|
+
opts.on("-s", "--server SERVER", "serve using SERVER (thin/puma/webrick/mongrel)") do |s|
|
47
|
+
options[:server] = s
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on("-o", "--host HOST", "listen on HOST (default: 0.0.0.0)") do |host|
|
51
|
+
options[:Host] = host
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on("-p", "--port PORT", "use PORT (default: 9292)") do |port|
|
55
|
+
options[:Port] = port
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.on("-O", "--option NAME[=VALUE]", "pass VALUE to the server as option NAME. If no VALUE, sets it to true. Run '#{$0} -s SERVER -h' to get a list of options for SERVER") do |name|
|
59
|
+
name, value = name.split('=', 2)
|
60
|
+
value = true if value.nil?
|
61
|
+
options[name.to_sym] = value
|
62
|
+
end
|
63
|
+
|
64
|
+
opts.on("--env ENVIRONMENT", "use ENVIRONMENT for defaults (default: development)") do |e|
|
65
|
+
options[:environment] = e
|
66
|
+
end
|
67
|
+
|
68
|
+
opts.on("--pid FILE", "file to store PID") do |file|
|
69
|
+
options[:pid] = ::File.expand_path(file)
|
70
|
+
end
|
71
|
+
|
72
|
+
opts.separator ""
|
73
|
+
opts.separator "Common options:"
|
74
|
+
|
75
|
+
opts.on_tail("-h", "-?", "--help", "Show this message") do
|
76
|
+
puts opts
|
77
|
+
puts handler_opts(options)
|
78
|
+
exit
|
79
|
+
end
|
80
|
+
|
81
|
+
opts.on_tail("--version", "Show version") do
|
82
|
+
puts "Rack #{Rack.version} (Release: #{Rack.release})"
|
83
|
+
exit
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
begin
|
88
|
+
opt_parser.parse! args
|
89
|
+
rescue OptionParser::InvalidOption => e
|
90
|
+
warn e.message
|
91
|
+
abort opt_parser.to_s
|
92
|
+
end
|
93
|
+
|
94
|
+
options[:config] = args.last if args.last
|
95
|
+
options
|
96
|
+
end
|
97
|
+
|
98
|
+
def handler_opts(options)
|
99
|
+
begin
|
100
|
+
info = []
|
101
|
+
server = Rack::Handler.get(options[:server]) || Rack::Handler.default(options)
|
102
|
+
if server && server.respond_to?(:valid_options)
|
103
|
+
info << ""
|
104
|
+
info << "Server-specific options for #{server.name}:"
|
105
|
+
|
106
|
+
has_options = false
|
107
|
+
server.valid_options.each do |name, description|
|
108
|
+
next if name.to_s.match(/^(Host|Port)[^a-zA-Z]/) # ignore handler's host and port options, we do our own.
|
109
|
+
info << " -O %-21s %s" % [name, description]
|
110
|
+
has_options = true
|
111
|
+
end
|
112
|
+
return "" if !has_options
|
113
|
+
end
|
114
|
+
info.join("\n")
|
115
|
+
rescue NameError
|
116
|
+
return "Warning: Could not find handler specified (#{options[:server] || 'default'}) to determine handler-specific options"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/lib/rack/utils.rb
ADDED
@@ -0,0 +1,653 @@
|
|
1
|
+
module Rack
|
2
|
+
# Rack::Utils contains a grab-bag of useful methods for writing web
|
3
|
+
# applications adopted from all kinds of Ruby libraries.
|
4
|
+
|
5
|
+
module Utils
|
6
|
+
# ParameterTypeError is the error that is raised when incoming structural
|
7
|
+
# parameters (parsed by parse_nested_query) contain conflicting types.
|
8
|
+
class ParameterTypeError < TypeError; end
|
9
|
+
|
10
|
+
# InvalidParameterError is the error that is raised when incoming structural
|
11
|
+
# parameters (parsed by parse_nested_query) contain invalid format or byte
|
12
|
+
# sequence.
|
13
|
+
class InvalidParameterError < ArgumentError; end
|
14
|
+
|
15
|
+
# URI escapes. (CGI style space to +)
|
16
|
+
def escape(s)
|
17
|
+
URI.encode_www_form_component(s)
|
18
|
+
end
|
19
|
+
module_function :escape
|
20
|
+
|
21
|
+
# Like URI escaping, but with %20 instead of +. Strictly speaking this is
|
22
|
+
# true URI escaping.
|
23
|
+
def escape_path(s)
|
24
|
+
escape(s).gsub('+', '%20')
|
25
|
+
end
|
26
|
+
module_function :escape_path
|
27
|
+
|
28
|
+
# Unescapes a URI escaped string with +encoding+. +encoding+ will be the
|
29
|
+
# target encoding of the string returned, and it defaults to UTF-8
|
30
|
+
if defined?(::Encoding)
|
31
|
+
def unescape(s, encoding = Encoding::UTF_8)
|
32
|
+
URI.decode_www_form_component(s, encoding)
|
33
|
+
end
|
34
|
+
else
|
35
|
+
def unescape(s, encoding = nil)
|
36
|
+
URI.decode_www_form_component(s, encoding)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
module_function :unescape
|
40
|
+
|
41
|
+
DEFAULT_SEP = /[&;] */n
|
42
|
+
|
43
|
+
class << self
|
44
|
+
attr_accessor :key_space_limit
|
45
|
+
end
|
46
|
+
|
47
|
+
# The default number of bytes to allow parameter keys to take up.
|
48
|
+
# This helps prevent a rogue client from flooding a Request.
|
49
|
+
self.key_space_limit = 65536
|
50
|
+
|
51
|
+
# Stolen from Mongrel, with some small modifications:
|
52
|
+
# Parses a query string by breaking it up at the '&'
|
53
|
+
# and ';' characters. You can also use this to parse
|
54
|
+
# cookies by changing the characters used in the second
|
55
|
+
# parameter (which defaults to '&;').
|
56
|
+
def parse_query(qs, d = nil, &unescaper)
|
57
|
+
unescaper ||= method(:unescape)
|
58
|
+
|
59
|
+
params = KeySpaceConstrainedParams.new
|
60
|
+
|
61
|
+
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
|
62
|
+
next if p.empty?
|
63
|
+
k, v = p.split('=', 2).map(&unescaper)
|
64
|
+
|
65
|
+
if cur = params[k]
|
66
|
+
if cur.class == Array
|
67
|
+
params[k] << v
|
68
|
+
else
|
69
|
+
params[k] = [cur, v]
|
70
|
+
end
|
71
|
+
else
|
72
|
+
params[k] = v
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
return params.to_params_hash
|
77
|
+
end
|
78
|
+
module_function :parse_query
|
79
|
+
|
80
|
+
# parse_nested_query expands a query string into structural types. Supported
|
81
|
+
# types are Arrays, Hashes and basic value types. It is possible to supply
|
82
|
+
# query strings with parameters of conflicting types, in this case a
|
83
|
+
# ParameterTypeError is raised. Users are encouraged to return a 400 in this
|
84
|
+
# case.
|
85
|
+
def parse_nested_query(qs, d = nil)
|
86
|
+
params = KeySpaceConstrainedParams.new
|
87
|
+
|
88
|
+
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
|
89
|
+
k, v = p.split('=', 2).map { |s| unescape(s) }
|
90
|
+
|
91
|
+
normalize_params(params, k, v)
|
92
|
+
end
|
93
|
+
|
94
|
+
return params.to_params_hash
|
95
|
+
rescue ArgumentError => e
|
96
|
+
raise InvalidParameterError, e.message
|
97
|
+
end
|
98
|
+
module_function :parse_nested_query
|
99
|
+
|
100
|
+
# normalize_params recursively expands parameters into structural types. If
|
101
|
+
# the structural types represented by two different parameter names are in
|
102
|
+
# conflict, a ParameterTypeError is raised.
|
103
|
+
def normalize_params(params, name, v = nil)
|
104
|
+
name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
|
105
|
+
k = $1 || ''
|
106
|
+
after = $' || ''
|
107
|
+
|
108
|
+
return if k.empty?
|
109
|
+
|
110
|
+
if after == ""
|
111
|
+
params[k] = v
|
112
|
+
elsif after == "["
|
113
|
+
params[name] = v
|
114
|
+
elsif after == "[]"
|
115
|
+
params[k] ||= []
|
116
|
+
raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
|
117
|
+
params[k] << v
|
118
|
+
elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
|
119
|
+
child_key = $1
|
120
|
+
params[k] ||= []
|
121
|
+
raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
|
122
|
+
if params_hash_type?(params[k].last) && !params[k].last.key?(child_key)
|
123
|
+
normalize_params(params[k].last, child_key, v)
|
124
|
+
else
|
125
|
+
params[k] << normalize_params(params.class.new, child_key, v)
|
126
|
+
end
|
127
|
+
else
|
128
|
+
params[k] ||= params.class.new
|
129
|
+
raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
|
130
|
+
params[k] = normalize_params(params[k], after, v)
|
131
|
+
end
|
132
|
+
|
133
|
+
return params
|
134
|
+
end
|
135
|
+
module_function :normalize_params
|
136
|
+
|
137
|
+
def params_hash_type?(obj)
|
138
|
+
obj.kind_of?(KeySpaceConstrainedParams) || obj.kind_of?(Hash)
|
139
|
+
end
|
140
|
+
module_function :params_hash_type?
|
141
|
+
|
142
|
+
def build_query(params)
|
143
|
+
params.map { |k, v|
|
144
|
+
if v.class == Array
|
145
|
+
build_query(v.map { |x| [k, x] })
|
146
|
+
else
|
147
|
+
v.nil? ? escape(k) : "#{escape(k)}=#{escape(v)}"
|
148
|
+
end
|
149
|
+
}.join("&")
|
150
|
+
end
|
151
|
+
module_function :build_query
|
152
|
+
|
153
|
+
def build_nested_query(value, prefix = nil)
|
154
|
+
case value
|
155
|
+
when Array
|
156
|
+
value.map { |v|
|
157
|
+
build_nested_query(v, "#{prefix}[]")
|
158
|
+
}.join("&")
|
159
|
+
when Hash
|
160
|
+
value.map { |k, v|
|
161
|
+
build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
|
162
|
+
}.reject(&:empty?).join('&')
|
163
|
+
when nil
|
164
|
+
prefix
|
165
|
+
else
|
166
|
+
raise ArgumentError, "value must be a Hash" if prefix.nil?
|
167
|
+
"#{prefix}=#{escape(value)}"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
module_function :build_nested_query
|
171
|
+
|
172
|
+
def q_values(q_value_header)
|
173
|
+
q_value_header.to_s.split(/\s*,\s*/).map do |part|
|
174
|
+
value, parameters = part.split(/\s*;\s*/, 2)
|
175
|
+
quality = 1.0
|
176
|
+
if md = /\Aq=([\d.]+)/.match(parameters)
|
177
|
+
quality = md[1].to_f
|
178
|
+
end
|
179
|
+
[value, quality]
|
180
|
+
end
|
181
|
+
end
|
182
|
+
module_function :q_values
|
183
|
+
|
184
|
+
def best_q_match(q_value_header, available_mimes)
|
185
|
+
values = q_values(q_value_header)
|
186
|
+
|
187
|
+
matches = values.map do |req_mime, quality|
|
188
|
+
match = available_mimes.find { |am| Rack::Mime.match?(am, req_mime) }
|
189
|
+
next unless match
|
190
|
+
[match, quality]
|
191
|
+
end.compact.sort_by do |match, quality|
|
192
|
+
(match.split('/', 2).count('*') * -10) + quality
|
193
|
+
end.last
|
194
|
+
matches && matches.first
|
195
|
+
end
|
196
|
+
module_function :best_q_match
|
197
|
+
|
198
|
+
ESCAPE_HTML = {
|
199
|
+
"&" => "&",
|
200
|
+
"<" => "<",
|
201
|
+
">" => ">",
|
202
|
+
"'" => "'",
|
203
|
+
'"' => """,
|
204
|
+
"/" => "/"
|
205
|
+
}
|
206
|
+
if //.respond_to?(:encoding)
|
207
|
+
ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
|
208
|
+
else
|
209
|
+
# On 1.8, there is a kcode = 'u' bug that allows for XSS otherwise
|
210
|
+
# TODO doesn't apply to jruby, so a better condition above might be preferable?
|
211
|
+
ESCAPE_HTML_PATTERN = /#{Regexp.union(*ESCAPE_HTML.keys)}/n
|
212
|
+
end
|
213
|
+
|
214
|
+
# Escape ampersands, brackets and quotes to their HTML/XML entities.
|
215
|
+
def escape_html(string)
|
216
|
+
string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
|
217
|
+
end
|
218
|
+
module_function :escape_html
|
219
|
+
|
220
|
+
def select_best_encoding(available_encodings, accept_encoding)
|
221
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
222
|
+
|
223
|
+
expanded_accept_encoding =
|
224
|
+
accept_encoding.map { |m, q|
|
225
|
+
if m == "*"
|
226
|
+
(available_encodings - accept_encoding.map { |m2, _| m2 }).map { |m2| [m2, q] }
|
227
|
+
else
|
228
|
+
[[m, q]]
|
229
|
+
end
|
230
|
+
}.inject([]) { |mem, list|
|
231
|
+
mem + list
|
232
|
+
}
|
233
|
+
|
234
|
+
encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map { |m, _| m }
|
235
|
+
|
236
|
+
unless encoding_candidates.include?("identity")
|
237
|
+
encoding_candidates.push("identity")
|
238
|
+
end
|
239
|
+
|
240
|
+
expanded_accept_encoding.each { |m, q|
|
241
|
+
encoding_candidates.delete(m) if q == 0.0
|
242
|
+
}
|
243
|
+
|
244
|
+
return (encoding_candidates & available_encodings)[0]
|
245
|
+
end
|
246
|
+
module_function :select_best_encoding
|
247
|
+
|
248
|
+
def set_cookie_header!(header, key, value)
|
249
|
+
case value
|
250
|
+
when Hash
|
251
|
+
domain = "; domain=" + value[:domain] if value[:domain]
|
252
|
+
path = "; path=" + value[:path] if value[:path]
|
253
|
+
max_age = "; max-age=" + value[:max_age].to_s if value[:max_age]
|
254
|
+
# There is an RFC mess in the area of date formatting for Cookies. Not
|
255
|
+
# only are there contradicting RFCs and examples within RFC text, but
|
256
|
+
# there are also numerous conflicting names of fields and partially
|
257
|
+
# cross-applicable specifications.
|
258
|
+
#
|
259
|
+
# These are best described in RFC 2616 3.3.1. This RFC text also
|
260
|
+
# specifies that RFC 822 as updated by RFC 1123 is preferred. That is a
|
261
|
+
# fixed length format with space-date delimeted fields.
|
262
|
+
#
|
263
|
+
# See also RFC 1123 section 5.2.14.
|
264
|
+
#
|
265
|
+
# RFC 6265 also specifies "sane-cookie-date" as RFC 1123 date, defined
|
266
|
+
# in RFC 2616 3.3.1. RFC 6265 also gives examples that clearly denote
|
267
|
+
# the space delimited format. These formats are compliant with RFC 2822.
|
268
|
+
#
|
269
|
+
# For reference, all involved RFCs are:
|
270
|
+
# RFC 822
|
271
|
+
# RFC 1123
|
272
|
+
# RFC 2109
|
273
|
+
# RFC 2616
|
274
|
+
# RFC 2822
|
275
|
+
# RFC 2965
|
276
|
+
# RFC 6265
|
277
|
+
expires = "; expires=" +
|
278
|
+
rfc2822(value[:expires].clone.gmtime) if value[:expires]
|
279
|
+
secure = "; secure" if value[:secure]
|
280
|
+
httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
|
281
|
+
value = value[:value]
|
282
|
+
end
|
283
|
+
value = [value] unless Array === value
|
284
|
+
cookie = escape(key) + "=" +
|
285
|
+
value.map { |v| escape v }.join("&") +
|
286
|
+
"#{domain}#{path}#{max_age}#{expires}#{secure}#{httponly}"
|
287
|
+
|
288
|
+
case header["Set-Cookie"]
|
289
|
+
when nil, ''
|
290
|
+
header["Set-Cookie"] = cookie
|
291
|
+
when String
|
292
|
+
header["Set-Cookie"] = [header["Set-Cookie"], cookie].join("\n")
|
293
|
+
when Array
|
294
|
+
header["Set-Cookie"] = (header["Set-Cookie"] + [cookie]).join("\n")
|
295
|
+
end
|
296
|
+
|
297
|
+
nil
|
298
|
+
end
|
299
|
+
module_function :set_cookie_header!
|
300
|
+
|
301
|
+
def delete_cookie_header!(header, key, value = {})
|
302
|
+
case header["Set-Cookie"]
|
303
|
+
when nil, ''
|
304
|
+
cookies = []
|
305
|
+
when String
|
306
|
+
cookies = header["Set-Cookie"].split("\n")
|
307
|
+
when Array
|
308
|
+
cookies = header["Set-Cookie"]
|
309
|
+
end
|
310
|
+
|
311
|
+
cookies.reject! { |cookie|
|
312
|
+
if value[:domain]
|
313
|
+
cookie =~ /\A#{escape(key)}=.*domain=#{value[:domain]}/
|
314
|
+
elsif value[:path]
|
315
|
+
cookie =~ /\A#{escape(key)}=.*path=#{value[:path]}/
|
316
|
+
else
|
317
|
+
cookie =~ /\A#{escape(key)}=/
|
318
|
+
end
|
319
|
+
}
|
320
|
+
|
321
|
+
header["Set-Cookie"] = cookies.join("\n")
|
322
|
+
|
323
|
+
set_cookie_header!(header, key,
|
324
|
+
{:value => '', :path => nil, :domain => nil,
|
325
|
+
:max_age => '0',
|
326
|
+
:expires => Time.at(0) }.merge(value))
|
327
|
+
|
328
|
+
nil
|
329
|
+
end
|
330
|
+
module_function :delete_cookie_header!
|
331
|
+
|
332
|
+
# Return the bytesize of String; uses String#size under Ruby 1.8 and
|
333
|
+
# String#bytesize under 1.9.
|
334
|
+
if ''.respond_to?(:bytesize)
|
335
|
+
def bytesize(string)
|
336
|
+
string.bytesize
|
337
|
+
end
|
338
|
+
else
|
339
|
+
def bytesize(string)
|
340
|
+
string.size
|
341
|
+
end
|
342
|
+
end
|
343
|
+
module_function :bytesize
|
344
|
+
|
345
|
+
def rfc2822(time)
|
346
|
+
time.rfc2822
|
347
|
+
end
|
348
|
+
module_function :rfc2822
|
349
|
+
|
350
|
+
# Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
|
351
|
+
# of '% %b %Y'.
|
352
|
+
# It assumes that the time is in GMT to comply to the RFC 2109.
|
353
|
+
#
|
354
|
+
# NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
|
355
|
+
# that I'm certain someone implemented only that option.
|
356
|
+
# Do not use %a and %b from Time.strptime, it would use localized names for
|
357
|
+
# weekday and month.
|
358
|
+
#
|
359
|
+
def rfc2109(time)
|
360
|
+
wday = Time::RFC2822_DAY_NAME[time.wday]
|
361
|
+
mon = Time::RFC2822_MONTH_NAME[time.mon - 1]
|
362
|
+
time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
|
363
|
+
end
|
364
|
+
module_function :rfc2109
|
365
|
+
|
366
|
+
# Parses the "Range:" header, if present, into an array of Range objects.
|
367
|
+
# Returns nil if the header is missing or syntactically invalid.
|
368
|
+
# Returns an empty array if none of the ranges are satisfiable.
|
369
|
+
def byte_ranges(env, size)
|
370
|
+
# See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
|
371
|
+
http_range = env['HTTP_RANGE']
|
372
|
+
return nil unless http_range && http_range =~ /bytes=([^;]+)/
|
373
|
+
ranges = []
|
374
|
+
$1.split(/,\s*/).each do |range_spec|
|
375
|
+
return nil unless range_spec =~ /(\d*)-(\d*)/
|
376
|
+
r0,r1 = $1, $2
|
377
|
+
if r0.empty?
|
378
|
+
return nil if r1.empty?
|
379
|
+
# suffix-byte-range-spec, represents trailing suffix of file
|
380
|
+
r0 = size - r1.to_i
|
381
|
+
r0 = 0 if r0 < 0
|
382
|
+
r1 = size - 1
|
383
|
+
else
|
384
|
+
r0 = r0.to_i
|
385
|
+
if r1.empty?
|
386
|
+
r1 = size - 1
|
387
|
+
else
|
388
|
+
r1 = r1.to_i
|
389
|
+
return nil if r1 < r0 # backwards range is syntactically invalid
|
390
|
+
r1 = size-1 if r1 >= size
|
391
|
+
end
|
392
|
+
end
|
393
|
+
ranges << (r0..r1) if r0 <= r1
|
394
|
+
end
|
395
|
+
ranges
|
396
|
+
end
|
397
|
+
module_function :byte_ranges
|
398
|
+
|
399
|
+
# Constant time string comparison.
|
400
|
+
#
|
401
|
+
# NOTE: the values compared should be of fixed length, such as strings
|
402
|
+
# that have aready been processed by HMAC. This should not be used
|
403
|
+
# on variable length plaintext strings because it could leak length info
|
404
|
+
# via timing attacks.
|
405
|
+
def secure_compare(a, b)
|
406
|
+
return false unless bytesize(a) == bytesize(b)
|
407
|
+
|
408
|
+
l = a.unpack("C*")
|
409
|
+
|
410
|
+
r, i = 0, -1
|
411
|
+
b.each_byte { |v| r |= v ^ l[i+=1] }
|
412
|
+
r == 0
|
413
|
+
end
|
414
|
+
module_function :secure_compare
|
415
|
+
|
416
|
+
# Context allows the use of a compatible middleware at different points
|
417
|
+
# in a request handling stack. A compatible middleware must define
|
418
|
+
# #context which should take the arguments env and app. The first of which
|
419
|
+
# would be the request environment. The second of which would be the rack
|
420
|
+
# application that the request would be forwarded to.
|
421
|
+
class Context
|
422
|
+
attr_reader :for, :app
|
423
|
+
|
424
|
+
def initialize(app_f, app_r)
|
425
|
+
raise 'running context does not respond to #context' unless app_f.respond_to? :context
|
426
|
+
@for, @app = app_f, app_r
|
427
|
+
end
|
428
|
+
|
429
|
+
def call(env)
|
430
|
+
@for.context(env, @app)
|
431
|
+
end
|
432
|
+
|
433
|
+
def recontext(app)
|
434
|
+
self.class.new(@for, app)
|
435
|
+
end
|
436
|
+
|
437
|
+
def context(env, app=@app)
|
438
|
+
recontext(app).call(env)
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
# A case-insensitive Hash that preserves the original case of a
|
443
|
+
# header when set.
|
444
|
+
class HeaderHash < Hash
|
445
|
+
def self.new(hash={})
|
446
|
+
HeaderHash === hash ? hash : super(hash)
|
447
|
+
end
|
448
|
+
|
449
|
+
def initialize(hash={})
|
450
|
+
super()
|
451
|
+
@names = {}
|
452
|
+
hash.each { |k, v| self[k] = v }
|
453
|
+
end
|
454
|
+
|
455
|
+
def each
|
456
|
+
super do |k, v|
|
457
|
+
yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
def to_hash
|
462
|
+
hash = {}
|
463
|
+
each { |k,v| hash[k] = v }
|
464
|
+
hash
|
465
|
+
end
|
466
|
+
|
467
|
+
def [](k)
|
468
|
+
super(k) || super(@names[k.downcase])
|
469
|
+
end
|
470
|
+
|
471
|
+
def []=(k, v)
|
472
|
+
canonical = k.downcase
|
473
|
+
delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
|
474
|
+
@names[k] = @names[canonical] = k
|
475
|
+
super k, v
|
476
|
+
end
|
477
|
+
|
478
|
+
def delete(k)
|
479
|
+
canonical = k.downcase
|
480
|
+
result = super @names.delete(canonical)
|
481
|
+
@names.delete_if { |name,| name.downcase == canonical }
|
482
|
+
result
|
483
|
+
end
|
484
|
+
|
485
|
+
def include?(k)
|
486
|
+
@names.include?(k) || @names.include?(k.downcase)
|
487
|
+
end
|
488
|
+
|
489
|
+
alias_method :has_key?, :include?
|
490
|
+
alias_method :member?, :include?
|
491
|
+
alias_method :key?, :include?
|
492
|
+
|
493
|
+
def merge!(other)
|
494
|
+
other.each { |k, v| self[k] = v }
|
495
|
+
self
|
496
|
+
end
|
497
|
+
|
498
|
+
def merge(other)
|
499
|
+
hash = dup
|
500
|
+
hash.merge! other
|
501
|
+
end
|
502
|
+
|
503
|
+
def replace(other)
|
504
|
+
clear
|
505
|
+
other.each { |k, v| self[k] = v }
|
506
|
+
self
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
class KeySpaceConstrainedParams
|
511
|
+
def initialize(limit = Utils.key_space_limit)
|
512
|
+
@limit = limit
|
513
|
+
@size = 0
|
514
|
+
@params = {}
|
515
|
+
end
|
516
|
+
|
517
|
+
def [](key)
|
518
|
+
@params[key]
|
519
|
+
end
|
520
|
+
|
521
|
+
def []=(key, value)
|
522
|
+
@size += key.size if key && !@params.key?(key)
|
523
|
+
raise RangeError, 'exceeded available parameter key space' if @size > @limit
|
524
|
+
@params[key] = value
|
525
|
+
end
|
526
|
+
|
527
|
+
def key?(key)
|
528
|
+
@params.key?(key)
|
529
|
+
end
|
530
|
+
|
531
|
+
def to_params_hash
|
532
|
+
hash = @params
|
533
|
+
hash.keys.each do |key|
|
534
|
+
value = hash[key]
|
535
|
+
if value.kind_of?(self.class)
|
536
|
+
if value.object_id == self.object_id
|
537
|
+
hash[key] = hash
|
538
|
+
else
|
539
|
+
hash[key] = value.to_params_hash
|
540
|
+
end
|
541
|
+
elsif value.kind_of?(Array)
|
542
|
+
value.map! {|x| x.kind_of?(self.class) ? x.to_params_hash : x}
|
543
|
+
end
|
544
|
+
end
|
545
|
+
hash
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
# Every standard HTTP code mapped to the appropriate message.
|
550
|
+
# Generated with:
|
551
|
+
# ruby -ropen-uri -rnokogiri -e "Nokogiri::XML(open(
|
552
|
+
# 'http://www.iana.org/assignments/http-status-codes/http-status-codes.xml')).css('record').each{|r|
|
553
|
+
# name = r.css('description').text; puts %Q[#{r.css('value').text} => '#{name}',] unless name == 'Unassigned' }"
|
554
|
+
HTTP_STATUS_CODES = {
|
555
|
+
100 => 'Continue',
|
556
|
+
101 => 'Switching Protocols',
|
557
|
+
102 => 'Processing',
|
558
|
+
200 => 'OK',
|
559
|
+
201 => 'Created',
|
560
|
+
202 => 'Accepted',
|
561
|
+
203 => 'Non-Authoritative Information',
|
562
|
+
204 => 'No Content',
|
563
|
+
205 => 'Reset Content',
|
564
|
+
206 => 'Partial Content',
|
565
|
+
207 => 'Multi-Status',
|
566
|
+
208 => 'Already Reported',
|
567
|
+
226 => 'IM Used',
|
568
|
+
300 => 'Multiple Choices',
|
569
|
+
301 => 'Moved Permanently',
|
570
|
+
302 => 'Found',
|
571
|
+
303 => 'See Other',
|
572
|
+
304 => 'Not Modified',
|
573
|
+
305 => 'Use Proxy',
|
574
|
+
306 => 'Reserved',
|
575
|
+
307 => 'Temporary Redirect',
|
576
|
+
308 => 'Permanent Redirect',
|
577
|
+
400 => 'Bad Request',
|
578
|
+
401 => 'Unauthorized',
|
579
|
+
402 => 'Payment Required',
|
580
|
+
403 => 'Forbidden',
|
581
|
+
404 => 'Not Found',
|
582
|
+
405 => 'Method Not Allowed',
|
583
|
+
406 => 'Not Acceptable',
|
584
|
+
407 => 'Proxy Authentication Required',
|
585
|
+
408 => 'Request Timeout',
|
586
|
+
409 => 'Conflict',
|
587
|
+
410 => 'Gone',
|
588
|
+
411 => 'Length Required',
|
589
|
+
412 => 'Precondition Failed',
|
590
|
+
413 => 'Request Entity Too Large',
|
591
|
+
414 => 'Request-URI Too Long',
|
592
|
+
415 => 'Unsupported Media Type',
|
593
|
+
416 => 'Requested Range Not Satisfiable',
|
594
|
+
417 => 'Expectation Failed',
|
595
|
+
418 => 'I\'m a teapot',
|
596
|
+
422 => 'Unprocessable Entity',
|
597
|
+
423 => 'Locked',
|
598
|
+
424 => 'Failed Dependency',
|
599
|
+
426 => 'Upgrade Required',
|
600
|
+
428 => 'Precondition Required',
|
601
|
+
429 => 'Too Many Requests',
|
602
|
+
431 => 'Request Header Fields Too Large',
|
603
|
+
500 => 'Internal Server Error',
|
604
|
+
501 => 'Not Implemented',
|
605
|
+
502 => 'Bad Gateway',
|
606
|
+
503 => 'Service Unavailable',
|
607
|
+
504 => 'Gateway Timeout',
|
608
|
+
505 => 'HTTP Version Not Supported',
|
609
|
+
506 => 'Variant Also Negotiates (Experimental)',
|
610
|
+
507 => 'Insufficient Storage',
|
611
|
+
508 => 'Loop Detected',
|
612
|
+
510 => 'Not Extended',
|
613
|
+
511 => 'Network Authentication Required'
|
614
|
+
}
|
615
|
+
|
616
|
+
# Responses with HTTP status codes that should not have an entity body
|
617
|
+
STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 205 << 304)
|
618
|
+
|
619
|
+
SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
|
620
|
+
[message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
|
621
|
+
}.flatten]
|
622
|
+
|
623
|
+
def status_code(status)
|
624
|
+
if status.is_a?(Symbol)
|
625
|
+
SYMBOL_TO_STATUS_CODE[status] || 500
|
626
|
+
else
|
627
|
+
status.to_i
|
628
|
+
end
|
629
|
+
end
|
630
|
+
module_function :status_code
|
631
|
+
|
632
|
+
Multipart = Rack::Multipart
|
633
|
+
|
634
|
+
PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
|
635
|
+
|
636
|
+
def clean_path_info(path_info)
|
637
|
+
parts = path_info.split PATH_SEPS
|
638
|
+
|
639
|
+
clean = []
|
640
|
+
|
641
|
+
parts.each do |part|
|
642
|
+
next if part.empty? || part == '.'
|
643
|
+
part == '..' ? clean.pop : clean << part
|
644
|
+
end
|
645
|
+
|
646
|
+
clean.unshift '/' if parts.empty? || parts.first.empty?
|
647
|
+
|
648
|
+
::File.join(*clean)
|
649
|
+
end
|
650
|
+
module_function :clean_path_info
|
651
|
+
|
652
|
+
end
|
653
|
+
end
|