rackful 0.1.4 → 0.2.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.
@@ -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