rackful 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,2 @@
1
- require 'rackful/middleware/header_spoofing.rb'
2
- require 'rackful/middleware/method_spoofing.rb'
3
- require 'rackful/middleware/relative_location.rb'
1
+ require 'rackful/middleware/headerspoofing.rb'
2
+ require 'rackful/middleware/methodoverride.rb'
@@ -0,0 +1,49 @@
1
+ # encoding: utf-8
2
+ # Required for parsing:
3
+ require 'rackful'
4
+
5
+ # Required for running:
6
+
7
+
8
+ # Rack middleware that provides header spoofing.
9
+ #
10
+ # If you use this middleware, then clients are allowed to spoof an HTTP header
11
+ # by specifying a `_http_SOME_HEADER=...` request parameter, for example
12
+ # `http://example.com/some_resource?_http_DEPTH=infinity`.
13
+ #
14
+ # This can be useful if you want to specify certain request headers from within
15
+ # a normal web browser.
16
+ #
17
+ # This middleware won’t work well together with Digest Authentication.
18
+ # @example Using this middleware
19
+ # require 'rackful/middleware/header_spoofing'
20
+ # use Rackful::HeaderSpoofing
21
+ class Rackful::HeaderSpoofing
22
+
23
+ def initialize app
24
+ @app = app
25
+ end
26
+
27
+ def call env
28
+ new_query_string = env['QUERY_STRING'].
29
+ split('&', -1).
30
+ select {
31
+ |p|
32
+ p = p.split('=', 2)
33
+ if /\A_http_([a-z]+(?:[\-_][a-z]+)*)\z/i === p[0]
34
+ header_name = p[0].gsub('-', '_').upcase[1..-1]
35
+ env[header_name] = p[1]
36
+ false
37
+ else
38
+ true
39
+ end
40
+ }.
41
+ join('&')
42
+ if env['QUERY_STRING'] != new_query_string
43
+ env['rackful.header_spoofing.QUERY_STRING'] = env['QUERY_STRING']
44
+ env['QUERY_STRING'] = new_query_string
45
+ end
46
+ @app.call env
47
+ end
48
+
49
+ end # Rackful::HeaderSpoofing
@@ -0,0 +1,136 @@
1
+ # encoding: utf-8
2
+ # Required for parsing:
3
+ require 'rackful'
4
+
5
+ # Required for running:
6
+ require 'set'
7
+
8
+
9
+ # Middleware that provides method spoofing, like {Rack::MethodOverride}.
10
+ #
11
+ # If you use this middleware, then clients are allowed to spoof an HTTP method
12
+ # by specifying a `_method=...` request parameter, for example
13
+ # `http://example.com/some_resource?_method=DELETE`.
14
+ #
15
+ # This can be useful if you want to perform `PUT` and `DELETE` requests from
16
+ # within a browser, of when you want to perform a `GET` requests with (too)
17
+ # many parameters, exceeding the maximum URI length in your client or server.
18
+ # In that case, you can put the parameters in a `POST` body, like this:
19
+ #
20
+ # POST /some_resource HTTP/1.1
21
+ # Host: example.com
22
+ # Content-Type: application/x-www-form-urlencoded
23
+ # Content-Length: 123456789
24
+ #  
25
+ # param_1=hello&param_2=world&param_3=...
26
+ #
27
+ # Caveats:
28
+ #
29
+ # * this middleware won’t work well together with Digest Authentication.
30
+ # * When a `POST` request is converted to a `GET` request, the entire request
31
+ # body is loaded into memory, which creates an attack surface for
32
+ # DoS-attacks. Hence, the maximum request body size is limited (see
33
+ # {POST_TO_GET_REQUEST_BODY_MAX_SIZE} and {#initialize}). You should choose this
34
+ # limit carefully, and/or include this middleware *after* your security
35
+ # middlewares.
36
+ #
37
+ # Improvements over Rack::MethodOverride (v1.5.2):
38
+ #
39
+ # * Rack::MethodOverride requires the original method to be `POST`. We allow
40
+ # the following overrides (`ORIGINAL_METHOD` → `OVERRIDE_WITH`):
41
+ # * `GET` → `DELETE`, `HEAD` and `OPTIONS`
42
+ # * `POST` → `GET`, `PATCH` and `PUT`
43
+ # * Rack::MethodOverride doesn’t touch `env['QUERY_STRING']`. We remove
44
+ # parameter `_method` if it was handled (but still leave it there if it
45
+ # wasn’t handled for some reason).
46
+ # * Rackful::MethodOverride is documented ;-)
47
+ # @example Using this middleware
48
+ # require 'rackful/middleware/method_override'
49
+ # use Rackful::MethodOverride
50
+ class Rackful::MethodOverride
51
+
52
+ METHOD_OVERRIDE_PARAM_KEY = '_method'.freeze
53
+
54
+ POST_TO_GET_REQUEST_BODY_MAX_SIZE = 1024 * 1024
55
+
56
+ ALLOWED_OVERRIDES = {
57
+ 'GET'.freeze => [ 'DELETE', 'HEAD', 'OPTIONS' ].to_set.freeze,
58
+ 'POST'.freeze => [ 'PATCH', 'PUT' ].to_set.freeze
59
+ }.freeze
60
+
61
+ # Constructor.
62
+ # @param app [#call]
63
+ # @param options [Hash{Symbol => Mixed}] Configuration options. The following
64
+ # options are supported:
65
+ # * **`:max_size`** the maximum size (in bytes) of the request body of a
66
+ # `POST` request that is converted to a `GET` request.
67
+ def initialize( app, options = {} )
68
+ @app = app
69
+ @max_size = options[:max_size]
70
+ end
71
+
72
+
73
+ def call env
74
+ before_call env
75
+ @app.call env
76
+ end
77
+
78
+ private
79
+
80
+
81
+ def before_call env
82
+ return unless ['GET', 'POST'].include? env['REQUEST_METHOD']
83
+ new_method = nil
84
+ new_query_string = env['QUERY_STRING'].
85
+ split('&', -1).
86
+ select { |p|
87
+ p = p.split('=', 2)
88
+ if new_method.nil? && METHOD_OVERRIDE_PARAM_KEY == p[0]
89
+ new_method = p[1].upcase
90
+ false
91
+ else
92
+ true
93
+ end
94
+ }.
95
+ join('&')
96
+ if new_method
97
+ if 'GET' == new_method &&
98
+ 'POST' == env['REQUEST_METHOD']
99
+ raise HTTP415
100
+ 'application/x-www-form-urlencoded' == env['CONTENT_TYPE'] &&
101
+ env['CONTENT_LENGTH'].to_i <= @max_size
102
+ if env.key?('rack.input')
103
+ new_query_string += '&' unless new_query_string.empty?
104
+ new_query_string += env['rack.input'].read
105
+ if env['rack.input'].respond_to?( :rewind )
106
+ env['rack.input'].rewind
107
+ env['rackful.method_override.input'] = env['rack.input']
108
+ end
109
+ env.delete 'rack.input'
110
+ end
111
+ env.delete 'CONTENT_TYPE'
112
+ env.delete 'CONTENT_LENGTH'
113
+ update_env( env, new_method, new_query_string )
114
+ elsif ALLOWED_OVERRIDES[env['REQUEST_METHOD']].include?( new_method )
115
+ update_env( env, new_method )
116
+ elsif logger = env['rack.logger']
117
+ logger.warn('Rackful::MethodOverride') {
118
+ "Client tried to override request method #{env['REQUEST_METHOD']} with #{new_method} (ignored)."
119
+ }
120
+ else
121
+ STDERR << "warning: Client tried to override request method #{env['REQUEST_METHOD']} with #{new_method} (ignored).\n"
122
+ end
123
+ end
124
+ end
125
+
126
+
127
+ def update_env env, new_method, new_query_string = nil
128
+ unless new_query_string.nil?
129
+ env['rackful.method_override.QUERY_STRING'] = env['QUERY_STRING']
130
+ env['QUERY_STRING'] = new_query_string
131
+ end
132
+ env['rackful.method_override.REQUEST_METHOD'] = env['REQUEST_METHOD']
133
+ env['REQUEST_METHOD'] = new_method
134
+ end
135
+
136
+ end # Rackful::MethodOverride
@@ -0,0 +1,315 @@
1
+ # encoding: utf-8
2
+
3
+
4
+ module Rackful
5
+
6
+
7
+ =begin markdown
8
+ Base class for all parsers.
9
+ @abstract Subclasses must implement method `#parse`, and define constant
10
+ {MEDIA_TYPES} as an array of media types this parser accepts.
11
+ @example Subclassing this class
12
+ class MyTextParser < Rackful::Parser
13
+ MEDIA_TYPES = [ 'text/plain' ]
14
+ def parse
15
+ # YOUR CODE HERE...
16
+ end
17
+ end
18
+ =end
19
+ class Parser
20
+
21
+
22
+ =begin markdown
23
+ An array of media type strings.
24
+ @!parse MEDIA_TYPES = [ 'example/type1', 'example/type2' ]
25
+ =end
26
+
27
+
28
+ # @return [Request]
29
+ attr_reader :request
30
+ # @return [Resource]
31
+ attr_reader :resource
32
+
33
+
34
+ =begin markdown
35
+ @param request [Request]
36
+ @param resource [Resource]
37
+ =end
38
+ def initialize request, resource
39
+ @request, @resource = request, resource
40
+ end
41
+
42
+
43
+ end # class Parser
44
+
45
+
46
+ =begin markdown
47
+ Parent class of all XML-parsing parsers.
48
+ @abstract
49
+ @since 0.2.0
50
+ =end
51
+ class Parser::DOM < Parser
52
+
53
+
54
+ =begin markdown
55
+ The media types parsed by this parser.
56
+ @see Parser
57
+ =end
58
+ MEDIA_TYPES = [
59
+ 'text/xml',
60
+ 'application/xml'
61
+ ]
62
+
63
+
64
+ =begin markdown
65
+ @return [Nokogiri::XML::Document]
66
+ =end
67
+ attr_reader :document
68
+
69
+
70
+ =begin markdown
71
+ @raise [HTTP400BadRequest] if the document is malformed.
72
+ =end
73
+ def initialize request, resource
74
+ super request, resource
75
+ # TODO Is ISO-8859-1 indeed the default encoding for XML documents? If so,
76
+ # that fact must be documented and referenced.
77
+ encoding = self.request.media_type_params['charset'] || 'ISO-8859-1'
78
+ begin
79
+ @document = Nokogiri.XML(
80
+ self.request.env['rack.input'].read,
81
+ self.request.canonical_uri.to_s,
82
+ encoding
83
+ ) do |config|
84
+ config.strict.nonet
85
+ end
86
+ rescue
87
+ raise HTTP400BadRequest, $!.to_s
88
+ end
89
+ raise( HTTP400BadRequest, $!.to_s ) unless @document.root
90
+ end
91
+
92
+
93
+ end # class Parser::DOM
94
+
95
+
96
+ =begin markdown
97
+ Parses XHTML as generated by {Serializer::XHTML}.
98
+ =end
99
+ class Parser::XHTML < Parser::DOM
100
+
101
+
102
+ =begin markdown
103
+ The media types parsed by this parser.
104
+ @see Parser
105
+ =end
106
+ MEDIA_TYPES = Parser::DOM::MEDIA_TYPES + [
107
+ 'application/xhtml+xml',
108
+ 'text/html'
109
+ ]
110
+
111
+
112
+ =begin markdown
113
+ @see Parser#parse
114
+ =end
115
+ def parse
116
+ # Try to find the actual content:
117
+ content = self.document.root.xpath(
118
+ '//html:div[@id="rackful-content"]',
119
+ 'html' => 'http://www.w3.org/1999/xhtml'
120
+ )
121
+ # There must be exactly one element <div id="rackful_content"/> in the document:
122
+ if content.empty?
123
+ raise HTTP400BadRequest, 'Couldn’t find div#rackful-content in request body.'
124
+ end
125
+ if content.length > 1
126
+ raise HTTP400BadRequest, 'Multiple instances of div#rackful-content found in request body.'
127
+ end
128
+ # Initialize @base_url:
129
+ base_url = self.document.root.xpath(
130
+ '/html:html/html:head/html:base',
131
+ 'html' => 'http://www.w3.org/1999/xhtml'
132
+ )
133
+ if base_url.empty?
134
+ @base_url = self.request.canonical_uri.dup
135
+ else
136
+ @base_url = URI( base_url.first.attribute('href').text ).normalize
137
+ if @base_url.relative?
138
+ @base_url = self.request.canonical_uri + @base_url
139
+ end
140
+ end
141
+ # Parse the f*cking thing:
142
+ self.parse_recursive content.first
143
+ end
144
+
145
+
146
+ =begin markdown
147
+ @api private
148
+ =end
149
+ def parse_recursive node
150
+
151
+ # A URI:
152
+ if ( nodelist = node.xpath( 'html:a', 'html' => 'http://www.w3.org/1999/xhtml' ) ).length == 1
153
+ r = URI( nodelist.first.attribute('href').text )
154
+ r.relative? ? @base_url + r : r
155
+
156
+ # An Object (AKA a Hash)
157
+ elsif ( nodelist = node.xpath( 'html:dl', 'html' => 'http://www.w3.org/1999/xhtml' ) ).length == 1
158
+ self.parse_object nodelist.first
159
+
160
+ # A list of Objects with identical keys:
161
+ elsif ( nodelist = node.xpath( 'html:table', 'html' => 'http://www.w3.org/1999/xhtml' ) ).length == 1
162
+ self.parse_object_list nodelist.first
163
+
164
+ # A list of things (AKA an Array):
165
+ elsif ( nodelist = node.xpath( 'html:ul', 'html' => 'http://www.w3.org/1999/xhtml' ) ).length == 1
166
+ nodelist.first.xpath(
167
+ 'html:li',
168
+ 'html' => 'http://www.w3.org/1999/xhtml'
169
+ ).collect do |n| self.parse_recursive n end
170
+
171
+ # A simple type:
172
+ elsif type = node.attribute_with_ns( 'type', 'http://www.w3.org/2001/XMLSchema' )
173
+ prefix, typename = type.text.split(':', 2)
174
+ unless typename && 'http://www.w3.org/2001/XMLSchema' == node.namespaces["xmlns:#{prefix}"]
175
+ raise HTTP400BadRequest, "Unknown XML Schema type: #{type}"
176
+ end
177
+ self.parse_simple_type node, typename
178
+ else
179
+ raise HTTP400BadRequest, 'Can’t parse:<br/>' + Rack::Utils.escape_html(node.to_xml)
180
+ end
181
+ end
182
+
183
+
184
+ =begin markdown
185
+ @api private
186
+ =end
187
+ def parse_simple_type node, typename
188
+ case typename
189
+ when 'boolean'
190
+ case node.inner_text.strip
191
+ when 'true' then true
192
+ when 'false' then false
193
+ else nil
194
+ end
195
+ when 'integer'
196
+ node.inner_text.strip.to_i
197
+ when 'numeric'
198
+ node.inner_text.strip.to_f
199
+ when 'dateTime'
200
+ Time.xmlschema(node.inner_text.strip)
201
+ when 'base64Binary'
202
+ Base64.decode64(node.inner_text)
203
+ when 'string'
204
+ node.inner_text
205
+ else
206
+ raise HTTP400BadRequest, "Unknown XML Schema type: #{type}"
207
+ end
208
+ end
209
+
210
+
211
+ =begin markdown
212
+ @api private
213
+ =end
214
+ def parse_object node
215
+ current_property = nil
216
+ r = {}
217
+ node.children.each do |child|
218
+ if 'dt' == child.name &&
219
+ 'http://www.w3.org/1999/xhtml' == child.namespace.href
220
+ if current_property
221
+ raise HTTP400BadRequest, 'Can’t parse:<br/>' + Rack::Utils.escape_html(node.to_xml)
222
+ end
223
+ current_property = child.inner_text.strip.split(' ').join('_').to_sym
224
+ elsif 'dd' == child.name &&
225
+ 'http://www.w3.org/1999/xhtml' == child.namespace.href
226
+ unless current_property
227
+ raise HTTP400BadRequest, 'Can’t parse:<br/>' + Rack::Utils.escape_html(node.to_xml)
228
+ end
229
+ r[current_property] = self.parse_recursive( child )
230
+ current_property = nil
231
+ end
232
+ end
233
+ r
234
+ end
235
+
236
+
237
+ =begin markdown
238
+ @api private
239
+ =end
240
+ def parse_object_list node
241
+ properties = node.xpath(
242
+ 'html:thead/html:tr/html:th',
243
+ 'html' => 'http://www.w3.org/1999/xhtml'
244
+ ).collect do |th|
245
+ th.inner_text.strip.split(' ').join('_').to_sym
246
+ end
247
+ if properties.empty?
248
+ raise HTTP400BadRequest, 'Can’t parse:<br/>' + Rack::Utils.escape_html(node.to_xml)
249
+ end
250
+ n = properties.length
251
+ node.xpath(
252
+ 'html:tbody/html:tr',
253
+ 'html' => 'http://www.w3.org/1999/xhtml'
254
+ ).collect do |row|
255
+ values = row.xpath(
256
+ 'html:td', 'html' => 'http://www.w3.org/1999/xhtml'
257
+ )
258
+ unless values.length == n
259
+ raise HTTP400BadRequest, 'Can’t parse:<br/>' + Rack::Utils.escape_html(row.to_xml)
260
+ end
261
+ object = {}
262
+ Range.new(0,n-1).each do |i|
263
+ object[properties[i]] = self.parse_recursive( values[i] )
264
+ end
265
+ object
266
+ end
267
+ end
268
+
269
+
270
+ end # class Parser::XHTML
271
+
272
+
273
+ class Parser::JSON < Parser
274
+
275
+
276
+ MEDIA_TYPES = [
277
+ 'application/json',
278
+ 'application/x-json'
279
+ ]
280
+
281
+
282
+ def parse
283
+ r = ::JSON.parse(
284
+ self.request.env['rack.input'].read,
285
+ :symbolize_names => true
286
+ )
287
+ self.recursive_datetime_parser r
288
+ end
289
+
290
+
291
+ def recursive_datetime_parser p
292
+ if p.kind_of?(String)
293
+ begin
294
+ return Time.xmlschema(p)
295
+ rescue
296
+ end
297
+ elsif p.kind_of?(Hash)
298
+ p.keys.each do
299
+ |key|
300
+ p[key] = self.recursive_datetime_parser( p[key] )
301
+ end
302
+ elsif p.kind_of?(Array)
303
+ (0 ... p.size).each do
304
+ |i|
305
+ p[i] = self.recursive_datetime_parser( p[i] )
306
+ end
307
+ end
308
+ p
309
+ end
310
+
311
+
312
+ end # class Parser::JSON
313
+
314
+
315
+ end # module Rackful