lack 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/bin/rackup +5 -0
  3. data/lib/rack.rb +26 -0
  4. data/lib/rack/body_proxy.rb +39 -0
  5. data/lib/rack/builder.rb +166 -0
  6. data/lib/rack/handler.rb +63 -0
  7. data/lib/rack/handler/webrick.rb +120 -0
  8. data/lib/rack/mime.rb +661 -0
  9. data/lib/rack/mock.rb +198 -0
  10. data/lib/rack/multipart.rb +31 -0
  11. data/lib/rack/multipart/generator.rb +93 -0
  12. data/lib/rack/multipart/parser.rb +239 -0
  13. data/lib/rack/multipart/uploaded_file.rb +34 -0
  14. data/lib/rack/request.rb +394 -0
  15. data/lib/rack/response.rb +160 -0
  16. data/lib/rack/server.rb +258 -0
  17. data/lib/rack/server/options.rb +121 -0
  18. data/lib/rack/utils.rb +653 -0
  19. data/lib/rack/version.rb +3 -0
  20. data/spec/spec_helper.rb +1 -0
  21. data/test/builder/anything.rb +5 -0
  22. data/test/builder/comment.ru +4 -0
  23. data/test/builder/end.ru +5 -0
  24. data/test/builder/line.ru +1 -0
  25. data/test/builder/options.ru +2 -0
  26. data/test/multipart/bad_robots +259 -0
  27. data/test/multipart/binary +0 -0
  28. data/test/multipart/content_type_and_no_filename +6 -0
  29. data/test/multipart/empty +10 -0
  30. data/test/multipart/fail_16384_nofile +814 -0
  31. data/test/multipart/file1.txt +1 -0
  32. data/test/multipart/filename_and_modification_param +7 -0
  33. data/test/multipart/filename_and_no_name +6 -0
  34. data/test/multipart/filename_with_escaped_quotes +6 -0
  35. data/test/multipart/filename_with_escaped_quotes_and_modification_param +7 -0
  36. data/test/multipart/filename_with_percent_escaped_quotes +6 -0
  37. data/test/multipart/filename_with_unescaped_percentages +6 -0
  38. data/test/multipart/filename_with_unescaped_percentages2 +6 -0
  39. data/test/multipart/filename_with_unescaped_percentages3 +6 -0
  40. data/test/multipart/filename_with_unescaped_quotes +6 -0
  41. data/test/multipart/ie +6 -0
  42. data/test/multipart/invalid_character +6 -0
  43. data/test/multipart/mixed_files +21 -0
  44. data/test/multipart/nested +10 -0
  45. data/test/multipart/none +9 -0
  46. data/test/multipart/semicolon +6 -0
  47. data/test/multipart/text +15 -0
  48. data/test/multipart/webkit +32 -0
  49. data/test/rackup/config.ru +31 -0
  50. data/test/registering_handler/rack/handler/registering_myself.rb +8 -0
  51. data/test/spec_body_proxy.rb +69 -0
  52. data/test/spec_builder.rb +223 -0
  53. data/test/spec_chunked.rb +101 -0
  54. data/test/spec_file.rb +221 -0
  55. data/test/spec_handler.rb +59 -0
  56. data/test/spec_head.rb +45 -0
  57. data/test/spec_lint.rb +522 -0
  58. data/test/spec_mime.rb +51 -0
  59. data/test/spec_mock.rb +277 -0
  60. data/test/spec_multipart.rb +547 -0
  61. data/test/spec_recursive.rb +72 -0
  62. data/test/spec_request.rb +1199 -0
  63. data/test/spec_response.rb +343 -0
  64. data/test/spec_rewindable_input.rb +118 -0
  65. data/test/spec_sendfile.rb +130 -0
  66. data/test/spec_server.rb +167 -0
  67. data/test/spec_utils.rb +635 -0
  68. data/test/spec_webrick.rb +184 -0
  69. data/test/testrequest.rb +78 -0
  70. data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
  71. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
  72. metadata +240 -0
@@ -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
@@ -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
+ "&" => "&amp;",
200
+ "<" => "&lt;",
201
+ ">" => "&gt;",
202
+ "'" => "&#x27;",
203
+ '"' => "&quot;",
204
+ "/" => "&#x2F;"
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