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.
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