rackful 0.0.2 → 0.1.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.
- data/README.md +14 -2
- data/example/config.ru +19 -13
- data/example/config2.ru +41 -0
- data/lib/rackful/header_spoofing.rb +39 -32
- data/lib/rackful/method_spoofing.rb +56 -58
- data/lib/rackful/relative_location.rb +35 -21
- data/lib/rackful.rb +6 -934
- data/lib/rackful_http_status.rb +288 -0
- data/lib/rackful_path.rb +112 -0
- data/lib/rackful_request.rb +268 -0
- data/lib/rackful_resource.rb +454 -0
- data/lib/rackful_serializer.rb +318 -0
- data/lib/rackful_server.rb +124 -0
- data/rackful.gemspec +3 -1
- metadata +49 -52
@@ -0,0 +1,288 @@
|
|
1
|
+
# Required for parsing:
|
2
|
+
require 'rackful_resource.rb'
|
3
|
+
require 'rackful_serializer.rb'
|
4
|
+
|
5
|
+
# Required for running
|
6
|
+
require 'rexml/rexml'
|
7
|
+
|
8
|
+
|
9
|
+
module Rackful
|
10
|
+
|
11
|
+
=begin markdown
|
12
|
+
Exception which represents an HTTP Status response.
|
13
|
+
@since 0.1.0
|
14
|
+
@abstract
|
15
|
+
=end
|
16
|
+
class HTTPStatus < RuntimeError
|
17
|
+
|
18
|
+
|
19
|
+
include Resource
|
20
|
+
attr_reader :status, :headers, :to_rackful
|
21
|
+
|
22
|
+
|
23
|
+
=begin markdown
|
24
|
+
@param message [String] XHTML
|
25
|
+
@param status [Symbol, Integer] e.g. `404` or `:not_found`
|
26
|
+
@param headers [Hash] HTTP response headers
|
27
|
+
=end
|
28
|
+
def initialize status, message = nil, info = {}
|
29
|
+
self.path = Request.current.path
|
30
|
+
@status = status_code status
|
31
|
+
raise "Wrong status: #{status}" if 0 == @status
|
32
|
+
message ||= ''
|
33
|
+
@headers = {}
|
34
|
+
@to_rackful = {}
|
35
|
+
info.each do
|
36
|
+
|k, v|
|
37
|
+
if k.kind_of? Symbol then @to_rackful[k] = v
|
38
|
+
else @headers[k] = v end
|
39
|
+
end
|
40
|
+
@to_rackful = nil if @to_rackful.empty?
|
41
|
+
begin
|
42
|
+
REXML::Document.new \
|
43
|
+
'<?xml version="1.0" encoding="UTF-8" ?>' +
|
44
|
+
"<div>#{message}</div>"
|
45
|
+
rescue
|
46
|
+
message = Rack::Utils.escape_html(message)
|
47
|
+
end
|
48
|
+
super message
|
49
|
+
if 500 <= @status
|
50
|
+
errors = Request.current.env['rack.errors']
|
51
|
+
errors.puts self.inspect
|
52
|
+
errors.puts "Headers: #{@headers.inspect}"
|
53
|
+
errors.puts "Info: #{@to_rackful.inspect}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
class XHTML < ::Rackful::XHTML
|
59
|
+
|
60
|
+
|
61
|
+
HTTP_STATUS_CODES = Rack::Utils::HTTP_STATUS_CODES
|
62
|
+
|
63
|
+
|
64
|
+
def header
|
65
|
+
<<EOS
|
66
|
+
<title>#{self.resource.status} #{HTTP_STATUS_CODES[self.resource.status]}</title>
|
67
|
+
</head><body>
|
68
|
+
<h1>HTTP/1.1 #{self.resource.status} #{HTTP_STATUS_CODES[self.resource.status]}</h1>
|
69
|
+
<div id="rackful_description">#{self.resource.message}</div>
|
70
|
+
EOS
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
def headers
|
75
|
+
self.resource.headers
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
end # class HTTPStatus::XHTML
|
80
|
+
|
81
|
+
|
82
|
+
add_serializer XHTML, 1.0
|
83
|
+
|
84
|
+
|
85
|
+
end # class Rackful::HTTPStatus
|
86
|
+
|
87
|
+
|
88
|
+
=begin markdown
|
89
|
+
@abstract Base class for HTTP status codes with only a simple text message, or
|
90
|
+
no message at all.
|
91
|
+
@since 0.1.0
|
92
|
+
=end
|
93
|
+
class HTTPSimpleStatus < HTTPStatus
|
94
|
+
|
95
|
+
def initialize message = nil
|
96
|
+
/HTTP(\d\d\d)\w+\z/ === self.class.to_s
|
97
|
+
status = $1.to_i
|
98
|
+
super( status, message )
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
# @since 0.1.0
|
105
|
+
class HTTP201Created < HTTPStatus
|
106
|
+
|
107
|
+
def initialize locations
|
108
|
+
locations = [ locations ] unless locations.kind_of? Array
|
109
|
+
locations = locations.collect { |l| l.to_path }
|
110
|
+
rf = Request.current.resource_factory
|
111
|
+
if locations.size > 1
|
112
|
+
locations = locations.collect {
|
113
|
+
|l|
|
114
|
+
resource = rf[l]
|
115
|
+
{ :location => l }.merge( resource.default_headers )
|
116
|
+
rf.uncache l if rf.respond_to? :uncache
|
117
|
+
}
|
118
|
+
super(
|
119
|
+
201, 'New resources were created:', :locations => locations
|
120
|
+
)
|
121
|
+
else
|
122
|
+
location = locations[0]
|
123
|
+
resource = rf[location]
|
124
|
+
super(
|
125
|
+
201, 'A new resource was created:', {
|
126
|
+
:location => location,
|
127
|
+
'Location' => location
|
128
|
+
}.merge( resource.default_headers )
|
129
|
+
)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
end # class HTTP201Created
|
134
|
+
|
135
|
+
|
136
|
+
# @since 0.1.0
|
137
|
+
class HTTP202Accepted < HTTPStatus
|
138
|
+
|
139
|
+
def initialize location = nil
|
140
|
+
if location
|
141
|
+
super(
|
142
|
+
202, '', {
|
143
|
+
:'Job status location:' => Path.new(locations),
|
144
|
+
'Location' => locations
|
145
|
+
}
|
146
|
+
)
|
147
|
+
else
|
148
|
+
super 202
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
end # class HTTP202Accepted
|
153
|
+
|
154
|
+
|
155
|
+
# @since 0.1.0
|
156
|
+
class HTTP301MovedPermanently < HTTPStatus
|
157
|
+
|
158
|
+
def initialize location
|
159
|
+
super(
|
160
|
+
301, '', {
|
161
|
+
:'New location:' => Path.new(location),
|
162
|
+
'Location' => location
|
163
|
+
}
|
164
|
+
)
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
# @since 0.1.0
|
171
|
+
class HTTP303SeeOther < HTTPStatus
|
172
|
+
|
173
|
+
def initialize location
|
174
|
+
super(
|
175
|
+
303, '', {
|
176
|
+
:'See:' => Path.new(location),
|
177
|
+
'Location' => location
|
178
|
+
}
|
179
|
+
)
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
183
|
+
|
184
|
+
|
185
|
+
# @since 0.1.0
|
186
|
+
class HTTP304NotModified < HTTPStatus
|
187
|
+
|
188
|
+
def initialize location
|
189
|
+
super( 304, nil, self.resource.default_headers )
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
|
194
|
+
|
195
|
+
# @since 0.1.0
|
196
|
+
class HTTP307TemporaryRedirect < HTTPStatus
|
197
|
+
|
198
|
+
def initialize location
|
199
|
+
super(
|
200
|
+
301, '', {
|
201
|
+
:'Current location:' => Path.new(location),
|
202
|
+
'Location' => location
|
203
|
+
}
|
204
|
+
)
|
205
|
+
end
|
206
|
+
|
207
|
+
end
|
208
|
+
|
209
|
+
|
210
|
+
# @since 0.1.0
|
211
|
+
class HTTP400BadRequest < HTTPSimpleStatus; end
|
212
|
+
|
213
|
+
# @since 0.1.0
|
214
|
+
class HTTP403Forbidden < HTTPSimpleStatus; end
|
215
|
+
|
216
|
+
# @since 0.1.0
|
217
|
+
class HTTP404NotFound < HTTPSimpleStatus; end
|
218
|
+
|
219
|
+
|
220
|
+
# @since 0.1.0
|
221
|
+
class HTTP405MethodNotAllowed < HTTPStatus
|
222
|
+
|
223
|
+
def initialize methods
|
224
|
+
super 405, '', 'Allow' => methods.join(', '),
|
225
|
+
:'Allowed methods:' => methods
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
|
230
|
+
|
231
|
+
# @since 0.1.0
|
232
|
+
class HTTP406NotAcceptable < HTTPStatus
|
233
|
+
|
234
|
+
def initialize content_types
|
235
|
+
super 406, '',
|
236
|
+
:'Available content-type(s):' => content_types
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|
240
|
+
|
241
|
+
|
242
|
+
# @since 0.1.0
|
243
|
+
class HTTP409Conflict < HTTPSimpleStatus; end
|
244
|
+
|
245
|
+
# @since 0.1.0
|
246
|
+
class HTTP410Gone < HTTPSimpleStatus; end
|
247
|
+
|
248
|
+
# @since 0.1.0
|
249
|
+
class HTTP411LengthRequired < HTTPSimpleStatus; end
|
250
|
+
|
251
|
+
|
252
|
+
# @since 0.1.0
|
253
|
+
class HTTP412PreconditionFailed < HTTPStatus
|
254
|
+
|
255
|
+
def initialize header = nil
|
256
|
+
info =
|
257
|
+
if header
|
258
|
+
{ header.to_sym => Request.current.env[ 'HTTP_' + header.gsub('-', '_').upcase ] }
|
259
|
+
else
|
260
|
+
{}
|
261
|
+
end
|
262
|
+
super 412, 'Failed precondition:', info
|
263
|
+
end
|
264
|
+
|
265
|
+
end
|
266
|
+
|
267
|
+
|
268
|
+
# @since 0.1.0
|
269
|
+
class HTTP415UnsupportedMediaType < HTTPStatus
|
270
|
+
|
271
|
+
def initialize media_types
|
272
|
+
super 405, '',
|
273
|
+
:'Supported media-type(s):' => media_types
|
274
|
+
end
|
275
|
+
|
276
|
+
end
|
277
|
+
|
278
|
+
|
279
|
+
# @since 0.1.0
|
280
|
+
class HTTP422UnprocessableEntity < HTTPSimpleStatus; end
|
281
|
+
|
282
|
+
# @since 0.1.0
|
283
|
+
class HTTP501NotImplemented < HTTPSimpleStatus; end
|
284
|
+
|
285
|
+
# @since 0.1.0
|
286
|
+
class HTTP503ServiceUnavailable < HTTPSimpleStatus; end
|
287
|
+
|
288
|
+
end # module Rackful
|
data/lib/rackful_path.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
# Required for parsing:
|
2
|
+
|
3
|
+
# Required for running:
|
4
|
+
require 'rack/utils'
|
5
|
+
|
6
|
+
|
7
|
+
# A String monkeypatch
|
8
|
+
# @private
|
9
|
+
class String
|
10
|
+
|
11
|
+
def to_path; Rackful::Path.new(self); end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
module Rackful
|
17
|
+
|
18
|
+
|
19
|
+
# Relative URI (a path)
|
20
|
+
class Path < String
|
21
|
+
|
22
|
+
def slashify
|
23
|
+
r = self.dup
|
24
|
+
r << '/' if '/' != r[-1,1]
|
25
|
+
r
|
26
|
+
end
|
27
|
+
|
28
|
+
def slashify!
|
29
|
+
if '/' != self[-1,1]
|
30
|
+
self << '/'
|
31
|
+
else
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def unslashify
|
37
|
+
r = self.dup
|
38
|
+
r = r.chomp( '/' ) if '/' == r[-1,1]
|
39
|
+
r
|
40
|
+
end
|
41
|
+
|
42
|
+
def unslashify!
|
43
|
+
if '/' == self[-1,1]
|
44
|
+
self.chomp! '/'
|
45
|
+
else
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# An alias for Rack::Utils.unescape
|
51
|
+
def unescape( encoding = Encoding::UTF_8 ); Rack::Utils.unescape(self, encoding); end
|
52
|
+
|
53
|
+
end # class Path
|
54
|
+
|
55
|
+
|
56
|
+
end # module Rackful
|
57
|
+
|
58
|
+
=begin comment
|
59
|
+
# Monkeypatch to this stdlib class.
|
60
|
+
class URI::Generic
|
61
|
+
|
62
|
+
# @see http://www.w3.org/TR/html401/struct/links.html#adef-rel the HTML `rel` attribute.
|
63
|
+
attr_accessor :rel
|
64
|
+
|
65
|
+
# @see http://www.w3.org/TR/html401/struct/links.html#adef-rev the HTML `rev` attribute.
|
66
|
+
attr_accessor :rev
|
67
|
+
|
68
|
+
def to_xhtml base_path, encoding = Encoding::UTF_8
|
69
|
+
retval = "<a href=\"#{self.route_from(base_path)}\"".encode encoding
|
70
|
+
retval << " rel=\"#{self.rel}\"" if self.rel
|
71
|
+
retval << " rev=\"#{self.rev}\"" if self.rev
|
72
|
+
retval << '>'
|
73
|
+
if self.relative? && ! self.query && ! self.fragment
|
74
|
+
retval << Rack::Utils.escape_html(
|
75
|
+
Rack::Utils.unescape( self.route_from(base_path).to_s, encoding )
|
76
|
+
)
|
77
|
+
else
|
78
|
+
retval << self.to_s
|
79
|
+
end
|
80
|
+
retval << '</a>'
|
81
|
+
retval
|
82
|
+
end
|
83
|
+
|
84
|
+
# @return [URI::Generic]
|
85
|
+
def slashify
|
86
|
+
r = self.dup
|
87
|
+
r.path = r.path.unslashify
|
88
|
+
r
|
89
|
+
end
|
90
|
+
|
91
|
+
# @return [self, nil]
|
92
|
+
def slashify!
|
93
|
+
self.path.slashify! && self
|
94
|
+
end
|
95
|
+
|
96
|
+
# @return [URI::Generic]
|
97
|
+
def unslashify
|
98
|
+
r = self.dup
|
99
|
+
r.path = r.path.unslashify
|
100
|
+
r
|
101
|
+
end
|
102
|
+
|
103
|
+
# @return [self, nil]
|
104
|
+
def unslashify!
|
105
|
+
self.path.unslashify! && self
|
106
|
+
end
|
107
|
+
|
108
|
+
end # class ::URI::Generic
|
109
|
+
=end comment
|
110
|
+
|
111
|
+
|
112
|
+
|
@@ -0,0 +1,268 @@
|
|
1
|
+
# Required for parsing:
|
2
|
+
require 'rack'
|
3
|
+
|
4
|
+
# Required for running:
|
5
|
+
|
6
|
+
|
7
|
+
module Rackful
|
8
|
+
|
9
|
+
=begin markdown
|
10
|
+
Subclass of {Rack::Request}, augmented for Rackful requests.
|
11
|
+
@since 0.0.1
|
12
|
+
=end
|
13
|
+
class Request < Rack::Request
|
14
|
+
|
15
|
+
|
16
|
+
=begin markdown
|
17
|
+
The resource factory for the current request.
|
18
|
+
@return [#[]]
|
19
|
+
@see Server#initialize
|
20
|
+
@since 0.0.1
|
21
|
+
=end
|
22
|
+
def resource_factory; self.env['rackful.resource_factory']; end
|
23
|
+
def base_path
|
24
|
+
self.env['rackful.base_path'] ||= begin
|
25
|
+
r = self.content_path.dup
|
26
|
+
r[%r{[^/]*\z}] = ''
|
27
|
+
r
|
28
|
+
end
|
29
|
+
end
|
30
|
+
=begin markdown
|
31
|
+
Similar to the HTTP/1.1 `Content-Location:` header. Contains the canonical path
|
32
|
+
to the requested resource, which may differ from {#path}
|
33
|
+
@return [Path]
|
34
|
+
@since 0.1.0
|
35
|
+
=end
|
36
|
+
def content_path; self.env['rackful.content_path'] ||= self.path; end
|
37
|
+
=begin markdown
|
38
|
+
Set by {Rackful::Server#call!}
|
39
|
+
@return [Path]
|
40
|
+
@since 0.1.0
|
41
|
+
=end
|
42
|
+
def content_path= bp; self.env['rackful.content_path'] = bp.to_path; end
|
43
|
+
=begin markdown
|
44
|
+
@return [Path]
|
45
|
+
@since 0.1.0
|
46
|
+
=end
|
47
|
+
def path; super.to_path; end
|
48
|
+
|
49
|
+
|
50
|
+
def initialize resource_factory, *args
|
51
|
+
super( *args )
|
52
|
+
self.env['rackful.resource_factory'] = resource_factory
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
=begin markdown
|
57
|
+
The request currently being processed in the current thread.
|
58
|
+
|
59
|
+
In a multi-threaded server, multiple requests can be handled at one time.
|
60
|
+
This method returns the request object, created (and registered) by
|
61
|
+
{Server#call!}
|
62
|
+
@return [Request]
|
63
|
+
@since 0.0.1
|
64
|
+
=end
|
65
|
+
def self.current
|
66
|
+
Thread.current[:rackful_request]
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
=begin markdown
|
71
|
+
Assert all <tt>If-*</tt> request headers.
|
72
|
+
@return [void]
|
73
|
+
@raise [HTTP304NotModified, HTTP400BadRequest, HTTP404NotFound, HTTP412PreconditionFailed]
|
74
|
+
with the following meanings:
|
75
|
+
|
76
|
+
- `304 Not Modified`
|
77
|
+
- `400 Bad Request` Couldn't parse one or more <tt>If-*</tt> headers, or a
|
78
|
+
weak validator comparison was requested for methods other than `GET` or
|
79
|
+
`HEAD`.
|
80
|
+
- `404 Not Found`
|
81
|
+
- `412 Precondition Failed`
|
82
|
+
@see http://tools.ietf.org/html/rfc2616#section-13.3.3 RFC2616, section 13.3.3
|
83
|
+
for details about weak and strong validator comparison.
|
84
|
+
@todo Implement support for the `If-Range:` header.
|
85
|
+
@since 0.0.1
|
86
|
+
=end
|
87
|
+
def assert_if_headers resource
|
88
|
+
#raise HTTP501NotImplemented, 'If-Range: request header is not supported.' \
|
89
|
+
# if @env.key? 'HTTP_IF_RANGE'
|
90
|
+
empty = resource.empty?
|
91
|
+
etag =
|
92
|
+
if ! empty && resource.respond_to?(:get_etag)
|
93
|
+
resource.get_etag
|
94
|
+
else
|
95
|
+
nil
|
96
|
+
end
|
97
|
+
last_modified =
|
98
|
+
if ! empty && resource.respond_to?(:get_last_modified)
|
99
|
+
resource.get_last_modified
|
100
|
+
else
|
101
|
+
nil
|
102
|
+
end
|
103
|
+
cond = {
|
104
|
+
:match => self.if_match,
|
105
|
+
:none_match => self.if_none_match,
|
106
|
+
:modified_since => self.if_modified_since,
|
107
|
+
:unmodified_since => self.if_unmodified_since
|
108
|
+
}
|
109
|
+
allow_weak = ['GET', 'HEAD'].include? self.request_method
|
110
|
+
if empty
|
111
|
+
if cond[:match]
|
112
|
+
raise HTTP412PreconditionFailed, 'If-Match'
|
113
|
+
elsif cond[:unmodified_since]
|
114
|
+
raise HTTP412PreconditionFailed, 'If-Unmodified-Since'
|
115
|
+
elsif cond[:modified_since]
|
116
|
+
raise HTTP404NotFound
|
117
|
+
end
|
118
|
+
else
|
119
|
+
if cond[:none_match] && self.validate_etag( etag, cond[:none_match] )
|
120
|
+
raise HTTP412PreconditionFailed, 'If-None-Match'
|
121
|
+
elsif cond[:match] && ! self.validate_etag( etag, cond[:match] )
|
122
|
+
raise HTTP412PreconditionFailed, 'If-Match'
|
123
|
+
elsif cond[:unmodified_since]
|
124
|
+
if ! last_modified || cond[:unmodified_since] < last_modified[0]
|
125
|
+
raise HTTP412PreconditionFailed, 'If-Unmodified-Since'
|
126
|
+
elsif last_modified && ! last_modified[1] && ! allow_weak &&
|
127
|
+
cond[:unmodified_since] == last_modified[0]
|
128
|
+
raise HTTP412PreconditionFailed, 'If-Unmodified-Since'
|
129
|
+
end
|
130
|
+
elsif cond[:modified_since]
|
131
|
+
if ! last_modified || cond[:modified_since] >= last_modified[0]
|
132
|
+
raise HTTP304NotModified
|
133
|
+
elsif last_modified && ! last_modified[1] && !allow_weak &&
|
134
|
+
cond[:modified_since] == last_modified[0]
|
135
|
+
raise HTTP412PreconditionFailed, 'If-Modified-Since'
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
=begin markdown
|
143
|
+
Hash of acceptable media types and their qualities.
|
144
|
+
|
145
|
+
This method parses the HTTP/1.1 `Accept:` header. If no acceptable media
|
146
|
+
types are provided, an empty Hash is returned.
|
147
|
+
@return [Hash{media_type => quality}]
|
148
|
+
@since 0.0.1
|
149
|
+
=end
|
150
|
+
def accept
|
151
|
+
@env['rackful.accept'] ||= begin
|
152
|
+
Hash[
|
153
|
+
@env['HTTP_ACCEPT'].to_s.split(',').collect do
|
154
|
+
|entry|
|
155
|
+
type, *options = entry.delete(' ').split(';')
|
156
|
+
quality = 1
|
157
|
+
options.each { |e|
|
158
|
+
quality = e[2..-1].to_f if e.start_with? 'q='
|
159
|
+
}
|
160
|
+
[type, quality]
|
161
|
+
end
|
162
|
+
]
|
163
|
+
rescue
|
164
|
+
{}
|
165
|
+
end
|
166
|
+
end # def accept
|
167
|
+
|
168
|
+
|
169
|
+
=begin markdown
|
170
|
+
@!method if_match()
|
171
|
+
Parses the HTTP/1.1 `If-Match:` header.
|
172
|
+
@return [nil, Array<String>]
|
173
|
+
@see http://tools.ietf.org/html/rfc2616#section-14.24 RFC2616, section 14.24
|
174
|
+
@see #if_none_match
|
175
|
+
@since 0.0.1
|
176
|
+
=end
|
177
|
+
def if_match none = false
|
178
|
+
header = @env["HTTP_IF_#{ none ? 'NONE_' : '' }MATCH"]
|
179
|
+
return nil unless header
|
180
|
+
envkey = "rackful.if_#{ none ? 'none_' : '' }match"
|
181
|
+
if %r{\A\s*\*\s*\z} === header
|
182
|
+
return [ '*' ]
|
183
|
+
elsif %r{\A(\s*(W/)?"([^"\\]|\\.)*"\s*,)+\z}m === ( header + ',' )
|
184
|
+
return header.scan( %r{(?:W/)?"(?:[^"\\]|\\.)*"}m )
|
185
|
+
end
|
186
|
+
raise HTTP400BadRequest, "Couldn't parse If-#{ none ? 'None-' : '' }Match: #{header}"
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
=begin markdown
|
191
|
+
Parses the HTTP/1.1 `If-None-Match:` header.
|
192
|
+
@return [nil, Array<String>]
|
193
|
+
@see http://tools.ietf.org/html/rfc2616#section-14.26 RFC2616, section 14.26
|
194
|
+
@see #if_match
|
195
|
+
@since 0.0.1
|
196
|
+
=end
|
197
|
+
def if_none_match
|
198
|
+
self.if_match true
|
199
|
+
end
|
200
|
+
|
201
|
+
|
202
|
+
=begin markdown
|
203
|
+
@!method if_modified_since()
|
204
|
+
@return [nil, Time]
|
205
|
+
@see http://tools.ietf.org/html/rfc2616#section-14.25 RFC2616, section 14.25
|
206
|
+
@see #if_unmodified_since
|
207
|
+
@since 0.0.1
|
208
|
+
=end
|
209
|
+
def if_modified_since unmodified = false
|
210
|
+
header = @env["HTTP_IF_#{ unmodified ? 'UN' : '' }MODIFIED_SINCE"]
|
211
|
+
return nil unless header
|
212
|
+
begin
|
213
|
+
header = Time.httpdate( header )
|
214
|
+
rescue ArgumentError
|
215
|
+
raise HTTP400BadRequest, "Couldn't parse If-#{ unmodified ? 'Unmodified' : 'Modified' }-Since: #{header}"
|
216
|
+
end
|
217
|
+
header
|
218
|
+
end
|
219
|
+
|
220
|
+
|
221
|
+
=begin markdown
|
222
|
+
@return [nil, Time]
|
223
|
+
@see http://tools.ietf.org/html/rfc2616#section-14.28 RFC2616, section 14.28
|
224
|
+
@see #if_modified_since
|
225
|
+
@since 0.0.1
|
226
|
+
=end
|
227
|
+
def if_unmodified_since
|
228
|
+
self.if_modified_since true
|
229
|
+
end
|
230
|
+
|
231
|
+
|
232
|
+
=begin markdown
|
233
|
+
Does any of the tags in `etags` match `etag`?
|
234
|
+
@param etag [#to_s]
|
235
|
+
@param etags [#to_a]
|
236
|
+
@example
|
237
|
+
etag = '"foo"'
|
238
|
+
etags = [ 'W/"foo"', '"bar"' ]
|
239
|
+
validate_etag etag, etags
|
240
|
+
#> true
|
241
|
+
@return [Boolean]
|
242
|
+
@see http://tools.ietf.org/html/rfc2616#section-13.3.3 RFC2616 section 13.3.3
|
243
|
+
for details about weak and strong validator comparison.
|
244
|
+
@since 0.0.1
|
245
|
+
=end
|
246
|
+
def validate_etag etag, etags
|
247
|
+
etag = etag.to_s
|
248
|
+
match = etags.to_a.detect do
|
249
|
+
|tag|
|
250
|
+
tag = tag.to_s
|
251
|
+
tag == '*' or
|
252
|
+
tag == etag or
|
253
|
+
'W/' + tag == etag or
|
254
|
+
'W/' + etag == tag
|
255
|
+
end
|
256
|
+
if match and
|
257
|
+
'*' != match and
|
258
|
+
'W/' == etag[0,2] || 'W/' == match[0,2] and
|
259
|
+
! [ 'HEAD', 'GET' ].include? self.request_method
|
260
|
+
raise HTTP400BadRequest, "Weak validators are only allowed for GET and HEAD requests."
|
261
|
+
end
|
262
|
+
!!match
|
263
|
+
end
|
264
|
+
|
265
|
+
|
266
|
+
end # class Request
|
267
|
+
|
268
|
+
end # module Rackful
|