rackful 0.1.4 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGES.md +18 -8
- data/RACKFUL.md +46 -34
- data/README.md +11 -5
- data/example/config.ru +19 -25
- data/lib/rackful.rb +11 -2
- data/lib/rackful/httpstatus.rb +250 -0
- data/lib/rackful/middleware.rb +2 -3
- data/lib/rackful/middleware/headerspoofing.rb +49 -0
- data/lib/rackful/middleware/methodoverride.rb +136 -0
- data/lib/rackful/parser.rb +315 -0
- data/lib/rackful/request.rb +103 -53
- data/lib/rackful/resource.rb +158 -221
- data/lib/rackful/serializer.rb +133 -215
- data/lib/rackful/server.rb +76 -86
- data/lib/rackful/uri.rb +150 -0
- data/mkdoc.sh +4 -2
- data/rackful.gemspec +6 -5
- metadata +66 -58
- data/lib/rackful/http_status.rb +0 -285
- data/lib/rackful/middleware/header_spoofing.rb +0 -72
- data/lib/rackful/middleware/method_spoofing.rb +0 -101
- data/lib/rackful/middleware/relative_location.rb +0 -71
- data/lib/rackful/path.rb +0 -179
data/lib/rackful/middleware.rb
CHANGED
@@ -1,3 +1,2 @@
|
|
1
|
-
require 'rackful/middleware/
|
2
|
-
require 'rackful/middleware/
|
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¶m_2=world¶m_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
|