rackful 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|