rack 1.4.7 → 2.1.4

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 (183) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +77 -0
  3. data/{COPYING → MIT-LICENSE} +4 -2
  4. data/README.rdoc +122 -456
  5. data/Rakefile +32 -31
  6. data/SPEC +119 -29
  7. data/bin/rackup +1 -0
  8. data/contrib/rack_logo.svg +164 -111
  9. data/example/lobster.ru +2 -0
  10. data/example/protectedlobster.rb +4 -2
  11. data/example/protectedlobster.ru +3 -1
  12. data/lib/rack/auth/abstract/handler.rb +7 -5
  13. data/lib/rack/auth/abstract/request.rb +8 -6
  14. data/lib/rack/auth/basic.rb +5 -2
  15. data/lib/rack/auth/digest/md5.rb +10 -8
  16. data/lib/rack/auth/digest/nonce.rb +6 -3
  17. data/lib/rack/auth/digest/params.rb +5 -4
  18. data/lib/rack/auth/digest/request.rb +4 -2
  19. data/lib/rack/body_proxy.rb +11 -9
  20. data/lib/rack/builder.rb +63 -20
  21. data/lib/rack/cascade.rb +10 -9
  22. data/lib/rack/chunked.rb +45 -11
  23. data/lib/rack/{commonlogger.rb → common_logger.rb} +24 -15
  24. data/lib/rack/{conditionalget.rb → conditional_get.rb} +20 -6
  25. data/lib/rack/config.rb +7 -0
  26. data/lib/rack/content_length.rb +12 -6
  27. data/lib/rack/content_type.rb +4 -2
  28. data/lib/rack/core_ext/regexp.rb +14 -0
  29. data/lib/rack/deflater.rb +73 -42
  30. data/lib/rack/directory.rb +77 -56
  31. data/lib/rack/etag.rb +25 -13
  32. data/lib/rack/events.rb +156 -0
  33. data/lib/rack/file.rb +4 -143
  34. data/lib/rack/files.rb +178 -0
  35. data/lib/rack/handler/cgi.rb +18 -17
  36. data/lib/rack/handler/fastcgi.rb +21 -17
  37. data/lib/rack/handler/lsws.rb +14 -12
  38. data/lib/rack/handler/scgi.rb +27 -21
  39. data/lib/rack/handler/thin.rb +19 -5
  40. data/lib/rack/handler/webrick.rb +66 -24
  41. data/lib/rack/handler.rb +29 -19
  42. data/lib/rack/head.rb +21 -14
  43. data/lib/rack/lint.rb +259 -65
  44. data/lib/rack/lobster.rb +17 -10
  45. data/lib/rack/lock.rb +19 -10
  46. data/lib/rack/logger.rb +4 -2
  47. data/lib/rack/media_type.rb +43 -0
  48. data/lib/rack/method_override.rb +52 -0
  49. data/lib/rack/mime.rb +43 -6
  50. data/lib/rack/mock.rb +109 -44
  51. data/lib/rack/multipart/generator.rb +11 -12
  52. data/lib/rack/multipart/parser.rb +302 -115
  53. data/lib/rack/multipart/uploaded_file.rb +4 -3
  54. data/lib/rack/multipart.rb +40 -9
  55. data/lib/rack/null_logger.rb +39 -0
  56. data/lib/rack/query_parser.rb +218 -0
  57. data/lib/rack/recursive.rb +14 -11
  58. data/lib/rack/reloader.rb +12 -5
  59. data/lib/rack/request.rb +484 -270
  60. data/lib/rack/response.rb +196 -77
  61. data/lib/rack/rewindable_input.rb +5 -14
  62. data/lib/rack/runtime.rb +13 -6
  63. data/lib/rack/sendfile.rb +44 -20
  64. data/lib/rack/server.rb +175 -61
  65. data/lib/rack/session/abstract/id.rb +276 -133
  66. data/lib/rack/session/cookie.rb +75 -40
  67. data/lib/rack/session/memcache.rb +4 -87
  68. data/lib/rack/session/pool.rb +24 -18
  69. data/lib/rack/show_exceptions.rb +392 -0
  70. data/lib/rack/{showstatus.rb → show_status.rb} +11 -9
  71. data/lib/rack/static.rb +65 -38
  72. data/lib/rack/tempfile_reaper.rb +24 -0
  73. data/lib/rack/urlmap.rb +40 -15
  74. data/lib/rack/utils.rb +316 -285
  75. data/lib/rack.rb +78 -23
  76. data/rack.gemspec +26 -19
  77. metadata +44 -209
  78. data/KNOWN-ISSUES +0 -30
  79. data/lib/rack/backports/uri/common_18.rb +0 -56
  80. data/lib/rack/backports/uri/common_192.rb +0 -52
  81. data/lib/rack/backports/uri/common_193.rb +0 -29
  82. data/lib/rack/handler/evented_mongrel.rb +0 -8
  83. data/lib/rack/handler/mongrel.rb +0 -100
  84. data/lib/rack/handler/swiftiplied_mongrel.rb +0 -8
  85. data/lib/rack/methodoverride.rb +0 -33
  86. data/lib/rack/nulllogger.rb +0 -18
  87. data/lib/rack/showexceptions.rb +0 -378
  88. data/test/builder/anything.rb +0 -5
  89. data/test/builder/comment.ru +0 -4
  90. data/test/builder/end.ru +0 -5
  91. data/test/builder/line.ru +0 -1
  92. data/test/builder/options.ru +0 -2
  93. data/test/cgi/assets/folder/test.js +0 -1
  94. data/test/cgi/assets/fonts/font.eot +0 -1
  95. data/test/cgi/assets/images/image.png +0 -1
  96. data/test/cgi/assets/index.html +0 -1
  97. data/test/cgi/assets/javascripts/app.js +0 -1
  98. data/test/cgi/assets/stylesheets/app.css +0 -1
  99. data/test/cgi/lighttpd.conf +0 -26
  100. data/test/cgi/lighttpd.errors +0 -1
  101. data/test/cgi/rackup_stub.rb +0 -6
  102. data/test/cgi/sample_rackup.ru +0 -5
  103. data/test/cgi/test +0 -9
  104. data/test/cgi/test+directory/test+file +0 -1
  105. data/test/cgi/test.fcgi +0 -8
  106. data/test/cgi/test.ru +0 -5
  107. data/test/gemloader.rb +0 -10
  108. data/test/multipart/bad_robots +0 -259
  109. data/test/multipart/binary +0 -0
  110. data/test/multipart/content_type_and_no_filename +0 -6
  111. data/test/multipart/empty +0 -10
  112. data/test/multipart/fail_16384_nofile +0 -814
  113. data/test/multipart/file1.txt +0 -1
  114. data/test/multipart/filename_and_modification_param +0 -7
  115. data/test/multipart/filename_with_escaped_quotes +0 -6
  116. data/test/multipart/filename_with_escaped_quotes_and_modification_param +0 -7
  117. data/test/multipart/filename_with_percent_escaped_quotes +0 -6
  118. data/test/multipart/filename_with_unescaped_percentages +0 -6
  119. data/test/multipart/filename_with_unescaped_percentages2 +0 -6
  120. data/test/multipart/filename_with_unescaped_percentages3 +0 -6
  121. data/test/multipart/filename_with_unescaped_quotes +0 -6
  122. data/test/multipart/ie +0 -6
  123. data/test/multipart/mixed_files +0 -21
  124. data/test/multipart/nested +0 -10
  125. data/test/multipart/none +0 -9
  126. data/test/multipart/semicolon +0 -6
  127. data/test/multipart/text +0 -15
  128. data/test/multipart/three_files_three_fields +0 -31
  129. data/test/multipart/webkit +0 -32
  130. data/test/rackup/config.ru +0 -31
  131. data/test/registering_handler/rack/handler/registering_myself.rb +0 -8
  132. data/test/spec_auth.rb +0 -57
  133. data/test/spec_auth_basic.rb +0 -81
  134. data/test/spec_auth_digest.rb +0 -259
  135. data/test/spec_body_proxy.rb +0 -69
  136. data/test/spec_builder.rb +0 -207
  137. data/test/spec_cascade.rb +0 -61
  138. data/test/spec_cgi.rb +0 -102
  139. data/test/spec_chunked.rb +0 -87
  140. data/test/spec_commonlogger.rb +0 -57
  141. data/test/spec_conditionalget.rb +0 -102
  142. data/test/spec_config.rb +0 -22
  143. data/test/spec_content_length.rb +0 -86
  144. data/test/spec_content_type.rb +0 -45
  145. data/test/spec_deflater.rb +0 -187
  146. data/test/spec_directory.rb +0 -88
  147. data/test/spec_etag.rb +0 -98
  148. data/test/spec_fastcgi.rb +0 -107
  149. data/test/spec_file.rb +0 -200
  150. data/test/spec_handler.rb +0 -59
  151. data/test/spec_head.rb +0 -48
  152. data/test/spec_lint.rb +0 -515
  153. data/test/spec_lobster.rb +0 -58
  154. data/test/spec_lock.rb +0 -167
  155. data/test/spec_logger.rb +0 -23
  156. data/test/spec_methodoverride.rb +0 -72
  157. data/test/spec_mock.rb +0 -269
  158. data/test/spec_mongrel.rb +0 -182
  159. data/test/spec_multipart.rb +0 -479
  160. data/test/spec_nulllogger.rb +0 -23
  161. data/test/spec_recursive.rb +0 -72
  162. data/test/spec_request.rb +0 -955
  163. data/test/spec_response.rb +0 -313
  164. data/test/spec_rewindable_input.rb +0 -118
  165. data/test/spec_runtime.rb +0 -49
  166. data/test/spec_sendfile.rb +0 -90
  167. data/test/spec_server.rb +0 -121
  168. data/test/spec_session_abstract_id.rb +0 -43
  169. data/test/spec_session_cookie.rb +0 -361
  170. data/test/spec_session_memcache.rb +0 -321
  171. data/test/spec_session_pool.rb +0 -209
  172. data/test/spec_showexceptions.rb +0 -92
  173. data/test/spec_showstatus.rb +0 -84
  174. data/test/spec_static.rb +0 -145
  175. data/test/spec_thin.rb +0 -86
  176. data/test/spec_urlmap.rb +0 -213
  177. data/test/spec_utils.rb +0 -554
  178. data/test/spec_webrick.rb +0 -143
  179. data/test/static/another/index.html +0 -1
  180. data/test/static/index.html +0 -1
  181. data/test/testrequest.rb +0 -78
  182. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  183. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
data/lib/rack/request.rb CHANGED
@@ -1,4 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack/utils'
4
+ require 'rack/media_type'
5
+
6
+ require_relative 'core_ext/regexp'
2
7
 
3
8
  module Rack
4
9
  # Rack::Request provides a convenient interface to a Rack
@@ -8,332 +13,541 @@ module Rack
8
13
  # req = Rack::Request.new(env)
9
14
  # req.post?
10
15
  # req.params["data"]
11
- #
12
- # The environment hash passed will store a reference to the Request object
13
- # instantiated so that it will only instantiate if an instance of the Request
14
- # object doesn't already exist.
15
16
 
16
17
  class Request
17
- # The environment of the request.
18
- attr_reader :env
18
+ using ::Rack::RegexpExtensions
19
19
 
20
- def initialize(env)
21
- @env = env
20
+ class << self
21
+ attr_accessor :ip_filter
22
22
  end
23
23
 
24
- def body; @env["rack.input"] end
25
- def script_name; @env["SCRIPT_NAME"].to_s end
26
- def path_info; @env["PATH_INFO"].to_s end
27
- def request_method; @env["REQUEST_METHOD"] end
28
- def query_string; @env["QUERY_STRING"].to_s end
29
- def content_length; @env['CONTENT_LENGTH'] end
24
+ self.ip_filter = lambda { |ip| /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i.match?(ip) }
25
+ ALLOWED_SCHEMES = %w(https http).freeze
26
+ SCHEME_WHITELIST = ALLOWED_SCHEMES
27
+ if Object.respond_to?(:deprecate_constant)
28
+ deprecate_constant :SCHEME_WHITELIST
29
+ end
30
30
 
31
- def content_type
32
- content_type = @env['CONTENT_TYPE']
33
- content_type.nil? || content_type.empty? ? nil : content_type
31
+ def initialize(env)
32
+ @params = nil
33
+ super(env)
34
34
  end
35
35
 
36
- def session; @env['rack.session'] ||= {} end
37
- def session_options; @env['rack.session.options'] ||= {} end
38
- def logger; @env['rack.logger'] end
39
-
40
- # The media type (type/subtype) portion of the CONTENT_TYPE header
41
- # without any media type parameters. e.g., when CONTENT_TYPE is
42
- # "text/plain;charset=utf-8", the media-type is "text/plain".
43
- #
44
- # For more information on the use of media types in HTTP, see:
45
- # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
46
- def media_type
47
- content_type && content_type.split(/\s*[;,]\s*/, 2).first.downcase
36
+ def params
37
+ @params ||= super
48
38
  end
49
39
 
50
- # The media type parameters provided in CONTENT_TYPE as a Hash, or
51
- # an empty Hash if no CONTENT_TYPE or media-type parameters were
52
- # provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
53
- # this method responds with the following Hash:
54
- # { 'charset' => 'utf-8' }
55
- def media_type_params
56
- return {} if content_type.nil?
57
- Hash[*content_type.split(/\s*[;,]\s*/)[1..-1].
58
- collect { |s| s.split('=', 2) }.
59
- map { |k,v| [k.downcase, v] }.flatten]
40
+ def update_param(k, v)
41
+ super
42
+ @params = nil
60
43
  end
61
44
 
62
- # The character set of the request body if a "charset" media type
63
- # parameter was given, or nil if no "charset" was specified. Note
64
- # that, per RFC2616, text/* media types that specify no explicit
65
- # charset are to be considered ISO-8859-1.
66
- def content_charset
67
- media_type_params['charset']
45
+ def delete_param(k)
46
+ v = super
47
+ @params = nil
48
+ v
68
49
  end
69
50
 
70
- def scheme
71
- if @env['HTTPS'] == 'on'
72
- 'https'
73
- elsif @env['HTTP_X_FORWARDED_SSL'] == 'on'
74
- 'https'
75
- elsif @env['HTTP_X_FORWARDED_SCHEME']
76
- @env['HTTP_X_FORWARDED_SCHEME']
77
- elsif @env['HTTP_X_FORWARDED_PROTO']
78
- @env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
79
- else
80
- @env["rack.url_scheme"]
51
+ module Env
52
+ # The environment of the request.
53
+ attr_reader :env
54
+
55
+ def initialize(env)
56
+ @env = env
57
+ super()
81
58
  end
82
- end
83
59
 
84
- def ssl?
85
- scheme == 'https'
86
- end
60
+ # Predicate method to test to see if `name` has been set as request
61
+ # specific data
62
+ def has_header?(name)
63
+ @env.key? name
64
+ end
87
65
 
88
- def host_with_port
89
- if forwarded = @env["HTTP_X_FORWARDED_HOST"]
90
- forwarded.split(/,\s?/).last
91
- else
92
- @env['HTTP_HOST'] || "#{@env['SERVER_NAME'] || @env['SERVER_ADDR']}:#{@env['SERVER_PORT']}"
66
+ # Get a request specific value for `name`.
67
+ def get_header(name)
68
+ @env[name]
93
69
  end
94
- end
95
70
 
96
- def port
97
- if port = host_with_port.split(/:/)[1]
98
- port.to_i
99
- elsif port = @env['HTTP_X_FORWARDED_PORT']
100
- port.to_i
101
- elsif ssl?
102
- 443
103
- elsif @env.has_key?("HTTP_X_FORWARDED_HOST")
104
- 80
105
- else
106
- @env["SERVER_PORT"].to_i
71
+ # If a block is given, it yields to the block if the value hasn't been set
72
+ # on the request.
73
+ def fetch_header(name, &block)
74
+ @env.fetch(name, &block)
107
75
  end
108
- end
109
76
 
110
- def host
111
- # Remove port number.
112
- host_with_port.to_s.gsub(/:\d+\z/, '')
113
- end
77
+ # Loops through each key / value pair in the request specific data.
78
+ def each_header(&block)
79
+ @env.each(&block)
80
+ end
114
81
 
115
- def script_name=(s); @env["SCRIPT_NAME"] = s.to_s end
116
- def path_info=(s); @env["PATH_INFO"] = s.to_s end
117
-
118
-
119
- # Checks the HTTP request method (or verb) to see if it was of type DELETE
120
- def delete?; request_method == "DELETE" end
121
-
122
- # Checks the HTTP request method (or verb) to see if it was of type GET
123
- def get?; request_method == "GET" end
124
-
125
- # Checks the HTTP request method (or verb) to see if it was of type HEAD
126
- def head?; request_method == "HEAD" end
127
-
128
- # Checks the HTTP request method (or verb) to see if it was of type OPTIONS
129
- def options?; request_method == "OPTIONS" end
130
-
131
- # Checks the HTTP request method (or verb) to see if it was of type PATCH
132
- def patch?; request_method == "PATCH" end
133
-
134
- # Checks the HTTP request method (or verb) to see if it was of type POST
135
- def post?; request_method == "POST" end
136
-
137
- # Checks the HTTP request method (or verb) to see if it was of type PUT
138
- def put?; request_method == "PUT" end
139
-
140
- # Checks the HTTP request method (or verb) to see if it was of type TRACE
141
- def trace?; request_method == "TRACE" end
142
-
143
-
144
- # The set of form-data media-types. Requests that do not indicate
145
- # one of the media types presents in this list will not be eligible
146
- # for form-data / param parsing.
147
- FORM_DATA_MEDIA_TYPES = [
148
- 'application/x-www-form-urlencoded',
149
- 'multipart/form-data'
150
- ]
151
-
152
- # The set of media-types. Requests that do not indicate
153
- # one of the media types presents in this list will not be eligible
154
- # for param parsing like soap attachments or generic multiparts
155
- PARSEABLE_DATA_MEDIA_TYPES = [
156
- 'multipart/related',
157
- 'multipart/mixed'
158
- ]
159
-
160
- # Determine whether the request body contains form-data by checking
161
- # the request Content-Type for one of the media-types:
162
- # "application/x-www-form-urlencoded" or "multipart/form-data". The
163
- # list of form-data media types can be modified through the
164
- # +FORM_DATA_MEDIA_TYPES+ array.
165
- #
166
- # A request body is also assumed to contain form-data when no
167
- # Content-Type header is provided and the request_method is POST.
168
- def form_data?
169
- type = media_type
170
- meth = env["rack.methodoverride.original_method"] || env['REQUEST_METHOD']
171
- (meth == 'POST' && type.nil?) || FORM_DATA_MEDIA_TYPES.include?(type)
172
- end
82
+ # Set a request specific value for `name` to `v`
83
+ def set_header(name, v)
84
+ @env[name] = v
85
+ end
173
86
 
174
- # Determine whether the request body contains data by checking
175
- # the request media_type against registered parse-data media-types
176
- def parseable_data?
177
- PARSEABLE_DATA_MEDIA_TYPES.include?(media_type)
178
- end
87
+ # Add a header that may have multiple values.
88
+ #
89
+ # Example:
90
+ # request.add_header 'Accept', 'image/png'
91
+ # request.add_header 'Accept', '*/*'
92
+ #
93
+ # assert_equal 'image/png,*/*', request.get_header('Accept')
94
+ #
95
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
96
+ def add_header key, v
97
+ if v.nil?
98
+ get_header key
99
+ elsif has_header? key
100
+ set_header key, "#{get_header key},#{v}"
101
+ else
102
+ set_header key, v
103
+ end
104
+ end
105
+
106
+ # Delete a request specific value for `name`.
107
+ def delete_header(name)
108
+ @env.delete name
109
+ end
179
110
 
180
- # Returns the data received in the query string.
181
- def GET
182
- if @env["rack.request.query_string"] == query_string
183
- @env["rack.request.query_hash"]
184
- else
185
- @env["rack.request.query_string"] = query_string
186
- @env["rack.request.query_hash"] = parse_query(query_string)
111
+ def initialize_copy(other)
112
+ @env = other.env.dup
187
113
  end
188
114
  end
189
115
 
190
- # Returns the data received in the request body.
191
- #
192
- # This method support both application/x-www-form-urlencoded and
193
- # multipart/form-data.
194
- def POST
195
- if @env["rack.input"].nil?
196
- raise "Missing rack.input"
197
- elsif @env["rack.request.form_input"].eql? @env["rack.input"]
198
- @env["rack.request.form_hash"]
199
- elsif form_data? || parseable_data?
200
- @env["rack.request.form_input"] = @env["rack.input"]
201
- unless @env["rack.request.form_hash"] = parse_multipart(env)
202
- form_vars = @env["rack.input"].read
203
-
204
- # Fix for Safari Ajax postings that always append \0
205
- # form_vars.sub!(/\0\z/, '') # performance replacement:
206
- form_vars.slice!(-1) if form_vars[-1] == ?\0
207
-
208
- @env["rack.request.form_vars"] = form_vars
209
- @env["rack.request.form_hash"] = parse_query(form_vars)
210
-
211
- @env["rack.input"].rewind
116
+ module Helpers
117
+ # The set of form-data media-types. Requests that do not indicate
118
+ # one of the media types present in this list will not be eligible
119
+ # for form-data / param parsing.
120
+ FORM_DATA_MEDIA_TYPES = [
121
+ 'application/x-www-form-urlencoded',
122
+ 'multipart/form-data'
123
+ ]
124
+
125
+ # The set of media-types. Requests that do not indicate
126
+ # one of the media types present in this list will not be eligible
127
+ # for param parsing like soap attachments or generic multiparts
128
+ PARSEABLE_DATA_MEDIA_TYPES = [
129
+ 'multipart/related',
130
+ 'multipart/mixed'
131
+ ]
132
+
133
+ # Default ports depending on scheme. Used to decide whether or not
134
+ # to include the port in a generated URI.
135
+ DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
136
+
137
+ HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME'
138
+ HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO'
139
+ HTTP_X_FORWARDED_HOST = 'HTTP_X_FORWARDED_HOST'
140
+ HTTP_X_FORWARDED_PORT = 'HTTP_X_FORWARDED_PORT'
141
+ HTTP_X_FORWARDED_SSL = 'HTTP_X_FORWARDED_SSL'
142
+
143
+ def body; get_header(RACK_INPUT) end
144
+ def script_name; get_header(SCRIPT_NAME).to_s end
145
+ def script_name=(s); set_header(SCRIPT_NAME, s.to_s) end
146
+
147
+ def path_info; get_header(PATH_INFO).to_s end
148
+ def path_info=(s); set_header(PATH_INFO, s.to_s) end
149
+
150
+ def request_method; get_header(REQUEST_METHOD) end
151
+ def query_string; get_header(QUERY_STRING).to_s end
152
+ def content_length; get_header('CONTENT_LENGTH') end
153
+ def logger; get_header(RACK_LOGGER) end
154
+ def user_agent; get_header('HTTP_USER_AGENT') end
155
+ def multithread?; get_header(RACK_MULTITHREAD) end
156
+
157
+ # the referer of the client
158
+ def referer; get_header('HTTP_REFERER') end
159
+ alias referrer referer
160
+
161
+ def session
162
+ fetch_header(RACK_SESSION) do |k|
163
+ set_header RACK_SESSION, default_session
212
164
  end
213
- @env["rack.request.form_hash"]
214
- else
215
- {}
216
165
  end
217
- end
218
166
 
219
- # The union of GET and POST data.
220
- def params
221
- @params ||= self.GET.merge(self.POST)
222
- rescue EOFError
223
- self.GET
224
- end
167
+ def session_options
168
+ fetch_header(RACK_SESSION_OPTIONS) do |k|
169
+ set_header RACK_SESSION_OPTIONS, {}
170
+ end
171
+ end
225
172
 
226
- # shortcut for request.params[key]
227
- def [](key)
228
- params[key.to_s]
229
- end
173
+ # Checks the HTTP request method (or verb) to see if it was of type DELETE
174
+ def delete?; request_method == DELETE end
230
175
 
231
- # shortcut for request.params[key] = value
232
- def []=(key, value)
233
- params[key.to_s] = value
234
- end
176
+ # Checks the HTTP request method (or verb) to see if it was of type GET
177
+ def get?; request_method == GET end
235
178
 
236
- # like Hash#values_at
237
- def values_at(*keys)
238
- keys.map{|key| params[key] }
239
- end
179
+ # Checks the HTTP request method (or verb) to see if it was of type HEAD
180
+ def head?; request_method == HEAD end
240
181
 
241
- # the referer of the client
242
- def referer
243
- @env['HTTP_REFERER']
244
- end
245
- alias referrer referer
182
+ # Checks the HTTP request method (or verb) to see if it was of type OPTIONS
183
+ def options?; request_method == OPTIONS end
246
184
 
247
- def user_agent
248
- @env['HTTP_USER_AGENT']
249
- end
185
+ # Checks the HTTP request method (or verb) to see if it was of type LINK
186
+ def link?; request_method == LINK end
250
187
 
251
- def cookies
252
- hash = @env["rack.request.cookie_hash"] ||= {}
253
- string = @env["HTTP_COOKIE"]
254
-
255
- return hash if string == @env["rack.request.cookie_string"]
256
- hash.clear
257
-
258
- # According to RFC 2109:
259
- # If multiple cookies satisfy the criteria above, they are ordered in
260
- # the Cookie header such that those with more specific Path attributes
261
- # precede those with less specific. Ordering with respect to other
262
- # attributes (e.g., Domain) is unspecified.
263
- cookies = Utils.parse_query(string, ';,') { |s| Rack::Utils.unescape(s) rescue s }
264
- cookies.each { |k,v| hash[k] = Array === v ? v.first : v }
265
- @env["rack.request.cookie_string"] = string
266
- hash
267
- end
188
+ # Checks the HTTP request method (or verb) to see if it was of type PATCH
189
+ def patch?; request_method == PATCH end
268
190
 
269
- def xhr?
270
- @env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest"
271
- end
191
+ # Checks the HTTP request method (or verb) to see if it was of type POST
192
+ def post?; request_method == POST end
272
193
 
273
- def base_url
274
- url = scheme + "://"
275
- url << host
194
+ # Checks the HTTP request method (or verb) to see if it was of type PUT
195
+ def put?; request_method == PUT end
276
196
 
277
- if scheme == "https" && port != 443 ||
278
- scheme == "http" && port != 80
279
- url << ":#{port}"
197
+ # Checks the HTTP request method (or verb) to see if it was of type TRACE
198
+ def trace?; request_method == TRACE end
199
+
200
+ # Checks the HTTP request method (or verb) to see if it was of type UNLINK
201
+ def unlink?; request_method == UNLINK end
202
+
203
+ def scheme
204
+ if get_header(HTTPS) == 'on'
205
+ 'https'
206
+ elsif get_header(HTTP_X_FORWARDED_SSL) == 'on'
207
+ 'https'
208
+ elsif forwarded_scheme
209
+ forwarded_scheme
210
+ else
211
+ get_header(RACK_URL_SCHEME)
212
+ end
280
213
  end
281
214
 
282
- url
283
- end
215
+ def authority
216
+ get_header(SERVER_NAME) + ':' + get_header(SERVER_PORT)
217
+ end
284
218
 
285
- # Tries to return a remake of the original request URL as a string.
286
- def url
287
- base_url + fullpath
288
- end
219
+ def cookies
220
+ hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |k|
221
+ set_header(k, {})
222
+ end
223
+ string = get_header HTTP_COOKIE
289
224
 
290
- def path
291
- script_name + path_info
292
- end
225
+ return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)
226
+ hash.replace Utils.parse_cookies_header string
227
+ set_header(RACK_REQUEST_COOKIE_STRING, string)
228
+ hash
229
+ end
293
230
 
294
- def fullpath
295
- query_string.empty? ? path : "#{path}?#{query_string}"
296
- end
231
+ def content_type
232
+ content_type = get_header('CONTENT_TYPE')
233
+ content_type.nil? || content_type.empty? ? nil : content_type
234
+ end
235
+
236
+ def xhr?
237
+ get_header("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest"
238
+ end
297
239
 
298
- def accept_encoding
299
- @env["HTTP_ACCEPT_ENCODING"].to_s.split(/\s*,\s*/).map do |part|
300
- encoding, parameters = part.split(/\s*;\s*/, 2)
301
- quality = 1.0
302
- if parameters and /\Aq=([\d.]+)/ =~ parameters
303
- quality = $1.to_f
240
+ def host_with_port
241
+ if forwarded = get_header(HTTP_X_FORWARDED_HOST)
242
+ forwarded.split(/,\s?/).last
243
+ else
244
+ get_header(HTTP_HOST) || "#{get_header(SERVER_NAME) || get_header(SERVER_ADDR)}:#{get_header(SERVER_PORT)}"
304
245
  end
305
- [encoding, quality]
306
246
  end
307
- end
308
247
 
309
- def trusted_proxy?(ip)
310
- ip =~ /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|^::1$|^fd[0-9a-f]{2}:.+|^localhost$/i
311
- end
248
+ def host
249
+ # Remove port number.
250
+ h = host_with_port
251
+ if colon_index = h.index(":")
252
+ h[0, colon_index]
253
+ else
254
+ h
255
+ end
256
+ end
312
257
 
313
- def ip
314
- remote_addrs = @env['REMOTE_ADDR'] ? @env['REMOTE_ADDR'].split(/[,\s]+/) : []
315
- remote_addrs.reject! { |addr| trusted_proxy?(addr) }
316
-
317
- return remote_addrs.first if remote_addrs.any?
258
+ def port
259
+ if port = extract_port(host_with_port)
260
+ port.to_i
261
+ elsif port = get_header(HTTP_X_FORWARDED_PORT)
262
+ port.to_i
263
+ elsif has_header?(HTTP_X_FORWARDED_HOST)
264
+ DEFAULT_PORTS[scheme]
265
+ elsif has_header?(HTTP_X_FORWARDED_PROTO)
266
+ DEFAULT_PORTS[extract_proto_header(get_header(HTTP_X_FORWARDED_PROTO))]
267
+ else
268
+ get_header(SERVER_PORT).to_i
269
+ end
270
+ end
271
+
272
+ def ssl?
273
+ scheme == 'https'
274
+ end
275
+
276
+ def ip
277
+ remote_addrs = split_ip_addresses(get_header('REMOTE_ADDR'))
278
+ remote_addrs = reject_trusted_ip_addresses(remote_addrs)
318
279
 
319
- forwarded_ips = @env['HTTP_X_FORWARDED_FOR'] ? @env['HTTP_X_FORWARDED_FOR'].strip.split(/[,\s]+/) : []
280
+ return remote_addrs.first if remote_addrs.any?
320
281
 
321
- if client_ip = @env['HTTP_CLIENT_IP']
322
- # If forwarded_ips doesn't include the client_ip, it might be an
323
- # ip spoofing attempt, so we ignore HTTP_CLIENT_IP
324
- return client_ip if forwarded_ips.include?(client_ip)
282
+ forwarded_ips = split_ip_addresses(get_header('HTTP_X_FORWARDED_FOR'))
283
+ .map { |ip| strip_port(ip) }
284
+
285
+ return reject_trusted_ip_addresses(forwarded_ips).last || forwarded_ips.first || get_header("REMOTE_ADDR")
325
286
  end
326
287
 
327
- return forwarded_ips.reject { |ip| trusted_proxy?(ip) }.last || @env["REMOTE_ADDR"]
328
- end
288
+ # The media type (type/subtype) portion of the CONTENT_TYPE header
289
+ # without any media type parameters. e.g., when CONTENT_TYPE is
290
+ # "text/plain;charset=utf-8", the media-type is "text/plain".
291
+ #
292
+ # For more information on the use of media types in HTTP, see:
293
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
294
+ def media_type
295
+ MediaType.type(content_type)
296
+ end
329
297
 
330
- protected
331
- def parse_query(qs)
332
- Utils.parse_nested_query(qs)
298
+ # The media type parameters provided in CONTENT_TYPE as a Hash, or
299
+ # an empty Hash if no CONTENT_TYPE or media-type parameters were
300
+ # provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
301
+ # this method responds with the following Hash:
302
+ # { 'charset' => 'utf-8' }
303
+ def media_type_params
304
+ MediaType.params(content_type)
333
305
  end
334
306
 
335
- def parse_multipart(env)
336
- Rack::Multipart.parse_multipart(env)
307
+ # The character set of the request body if a "charset" media type
308
+ # parameter was given, or nil if no "charset" was specified. Note
309
+ # that, per RFC2616, text/* media types that specify no explicit
310
+ # charset are to be considered ISO-8859-1.
311
+ def content_charset
312
+ media_type_params['charset']
337
313
  end
314
+
315
+ # Determine whether the request body contains form-data by checking
316
+ # the request Content-Type for one of the media-types:
317
+ # "application/x-www-form-urlencoded" or "multipart/form-data". The
318
+ # list of form-data media types can be modified through the
319
+ # +FORM_DATA_MEDIA_TYPES+ array.
320
+ #
321
+ # A request body is also assumed to contain form-data when no
322
+ # Content-Type header is provided and the request_method is POST.
323
+ def form_data?
324
+ type = media_type
325
+ meth = get_header(RACK_METHODOVERRIDE_ORIGINAL_METHOD) || get_header(REQUEST_METHOD)
326
+ (meth == POST && type.nil?) || FORM_DATA_MEDIA_TYPES.include?(type)
327
+ end
328
+
329
+ # Determine whether the request body contains data by checking
330
+ # the request media_type against registered parse-data media-types
331
+ def parseable_data?
332
+ PARSEABLE_DATA_MEDIA_TYPES.include?(media_type)
333
+ end
334
+
335
+ # Returns the data received in the query string.
336
+ def GET
337
+ if get_header(RACK_REQUEST_QUERY_STRING) == query_string
338
+ get_header(RACK_REQUEST_QUERY_HASH)
339
+ else
340
+ query_hash = parse_query(query_string, '&;')
341
+ set_header(RACK_REQUEST_QUERY_STRING, query_string)
342
+ set_header(RACK_REQUEST_QUERY_HASH, query_hash)
343
+ end
344
+ end
345
+
346
+ # Returns the data received in the request body.
347
+ #
348
+ # This method support both application/x-www-form-urlencoded and
349
+ # multipart/form-data.
350
+ def POST
351
+ if get_header(RACK_INPUT).nil?
352
+ raise "Missing rack.input"
353
+ elsif get_header(RACK_REQUEST_FORM_INPUT) == get_header(RACK_INPUT)
354
+ get_header(RACK_REQUEST_FORM_HASH)
355
+ elsif form_data? || parseable_data?
356
+ unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart)
357
+ form_vars = get_header(RACK_INPUT).read
358
+
359
+ # Fix for Safari Ajax postings that always append \0
360
+ # form_vars.sub!(/\0\z/, '') # performance replacement:
361
+ form_vars.slice!(-1) if form_vars.end_with?("\0")
362
+
363
+ set_header RACK_REQUEST_FORM_VARS, form_vars
364
+ set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&')
365
+
366
+ get_header(RACK_INPUT).rewind
367
+ end
368
+ set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
369
+ get_header RACK_REQUEST_FORM_HASH
370
+ else
371
+ {}
372
+ end
373
+ end
374
+
375
+ # The union of GET and POST data.
376
+ #
377
+ # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
378
+ def params
379
+ self.GET.merge(self.POST)
380
+ rescue EOFError
381
+ self.GET.dup
382
+ end
383
+
384
+ # Destructively update a parameter, whether it's in GET and/or POST. Returns nil.
385
+ #
386
+ # The parameter is updated wherever it was previous defined, so GET, POST, or both. If it wasn't previously defined, it's inserted into GET.
387
+ #
388
+ # <tt>env['rack.input']</tt> is not touched.
389
+ def update_param(k, v)
390
+ found = false
391
+ if self.GET.has_key?(k)
392
+ found = true
393
+ self.GET[k] = v
394
+ end
395
+ if self.POST.has_key?(k)
396
+ found = true
397
+ self.POST[k] = v
398
+ end
399
+ unless found
400
+ self.GET[k] = v
401
+ end
402
+ end
403
+
404
+ # Destructively delete a parameter, whether it's in GET or POST. Returns the value of the deleted parameter.
405
+ #
406
+ # If the parameter is in both GET and POST, the POST value takes precedence since that's how #params works.
407
+ #
408
+ # <tt>env['rack.input']</tt> is not touched.
409
+ def delete_param(k)
410
+ post_value, get_value = self.POST.delete(k), self.GET.delete(k)
411
+ post_value || get_value
412
+ end
413
+
414
+ def base_url
415
+ url = "#{scheme}://#{host}"
416
+ url = "#{url}:#{port}" if port != DEFAULT_PORTS[scheme]
417
+ url
418
+ end
419
+
420
+ # Tries to return a remake of the original request URL as a string.
421
+ def url
422
+ base_url + fullpath
423
+ end
424
+
425
+ def path
426
+ script_name + path_info
427
+ end
428
+
429
+ def fullpath
430
+ query_string.empty? ? path : "#{path}?#{query_string}"
431
+ end
432
+
433
+ def accept_encoding
434
+ parse_http_accept_header(get_header("HTTP_ACCEPT_ENCODING"))
435
+ end
436
+
437
+ def accept_language
438
+ parse_http_accept_header(get_header("HTTP_ACCEPT_LANGUAGE"))
439
+ end
440
+
441
+ def trusted_proxy?(ip)
442
+ Rack::Request.ip_filter.call(ip)
443
+ end
444
+
445
+ # shortcut for <tt>request.params[key]</tt>
446
+ def [](key)
447
+ if $VERBOSE
448
+ warn("Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead")
449
+ end
450
+
451
+ params[key.to_s]
452
+ end
453
+
454
+ # shortcut for <tt>request.params[key] = value</tt>
455
+ #
456
+ # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
457
+ def []=(key, value)
458
+ if $VERBOSE
459
+ warn("Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead")
460
+ end
461
+
462
+ params[key.to_s] = value
463
+ end
464
+
465
+ # like Hash#values_at
466
+ def values_at(*keys)
467
+ keys.map { |key| params[key] }
468
+ end
469
+
470
+ private
471
+
472
+ def default_session; {}; end
473
+
474
+ def parse_http_accept_header(header)
475
+ header.to_s.split(/\s*,\s*/).map do |part|
476
+ attribute, parameters = part.split(/\s*;\s*/, 2)
477
+ quality = 1.0
478
+ if parameters and /\Aq=([\d.]+)/ =~ parameters
479
+ quality = $1.to_f
480
+ end
481
+ [attribute, quality]
482
+ end
483
+ end
484
+
485
+ def query_parser
486
+ Utils.default_query_parser
487
+ end
488
+
489
+ def parse_query(qs, d = '&')
490
+ query_parser.parse_nested_query(qs, d)
491
+ end
492
+
493
+ def parse_multipart
494
+ Rack::Multipart.extract_multipart(self, query_parser)
495
+ end
496
+
497
+ def split_ip_addresses(ip_addresses)
498
+ ip_addresses ? ip_addresses.strip.split(/[,\s]+/) : []
499
+ end
500
+
501
+ def strip_port(ip_address)
502
+ # IPv6 format with optional port: "[2001:db8:cafe::17]:47011"
503
+ # returns: "2001:db8:cafe::17"
504
+ sep_start = ip_address.index('[')
505
+ sep_end = ip_address.index(']')
506
+ if (sep_start && sep_end)
507
+ return ip_address[sep_start + 1, sep_end - 1]
508
+ end
509
+
510
+ # IPv4 format with optional port: "192.0.2.43:47011"
511
+ # returns: "192.0.2.43"
512
+ sep = ip_address.index(':')
513
+ if (sep && ip_address.count(':') == 1)
514
+ return ip_address[0, sep]
515
+ end
516
+
517
+ ip_address
518
+ end
519
+
520
+ def reject_trusted_ip_addresses(ip_addresses)
521
+ ip_addresses.reject { |ip| trusted_proxy?(ip) }
522
+ end
523
+
524
+ def forwarded_scheme
525
+ allowed_scheme(get_header(HTTP_X_FORWARDED_SCHEME)) ||
526
+ allowed_scheme(extract_proto_header(get_header(HTTP_X_FORWARDED_PROTO)))
527
+ end
528
+
529
+ def allowed_scheme(header)
530
+ header if ALLOWED_SCHEMES.include?(header)
531
+ end
532
+
533
+ def extract_proto_header(header)
534
+ if header
535
+ if (comma_index = header.index(','))
536
+ header[0, comma_index]
537
+ else
538
+ header
539
+ end
540
+ end
541
+ end
542
+
543
+ def extract_port(uri)
544
+ if (colon_index = uri.index(':'))
545
+ uri[colon_index + 1, uri.length]
546
+ end
547
+ end
548
+ end
549
+
550
+ include Env
551
+ include Helpers
338
552
  end
339
553
  end