File Coverage

blib/lib/Dancer2/Plugin/HTTP/ConditionalRequest.pm
Criterion Covered Total %
statement 15 35 42.8
branch n/a
condition n/a
subroutine 5 12 41.6
pod n/a
total 20 47 42.5


line stmt bran cond sub pod time code
1             package Dancer2::Plugin::HTTP::ConditionalRequest;
2              
3             =head1 NAME
4              
5             Dancer2::Plugin::HTTP::ConditionalRequest - RFC 7232 compliant
6              
7             =head1 VERSION
8              
9             Version 0.01
10              
11             =cut
12              
13             our $VERSION = '0.03';
14              
15 1     1   21518 use warnings;
  1         2  
  1         32  
16 1     1   5 use strict;
  1         1  
  1         23  
17              
18 1     1   5 use Carp;
  1         5  
  1         79  
19 1     1   814 use Dancer2::Plugin;
  1         111332  
  1         7  
20              
21 1     1   1039 use DateTime::Format::HTTP;
  1         117428  
  1         1249  
22              
23              
24             =head1 SYNOPSIS
25              
26             Conditionally handling HTTP request based on eTag or Modification-Date,
27             according to RFC 7232
28              
29             HTTP Conditional Requests are used for telling servers that they only have to
30             perform the method if the preconditions are met. Such requests are either used
31             by caches to (re)validate the cached response with the origin server - or -
32             to prevent lost-updates with unsafe-methods in a stateless api (like REST).
33            
34             any '/my_resource/:id' => sub {
35             ...
36             # check stuff
37             # - compute eTag from MD5
38             # - use an external table
39             # - find a last modification date
40             ...
41            
42             http_conditional {
43             etag => '2d5730a4c92b1061',
44             last_modified => "Tue, 15 Nov 1994 12:45:26 GMT", # HTTP Date
45             required => false,
46             } => sub {
47             ...
48             # do the real stuff, like updating
49             ...
50             }
51             };
52              
53             =head1 RFC_7232 HTTP: Conditional Requests... explained
54              
55             As mentioned in the previous section, Conditional Requests are for two purposes
56             mainly:
57              
58             =head2 Caching
59              
60             For GET and HEAD methods, the caching-server passes the validators to the origin
61             server to see if it can still use the cached version or not - and if not, get a
62             fresh version.
63              
64             Keep in mind that the Dancer2 server is probably designed to be that originating
65             server and should provide either the 304 (Not Modified) status or a fresh
66             representation off the requested resource. At this stage (and in compliance with
67             the RFC) there is nothing to deal with the caching of the responses at all.
68              
69             This plugin does not do any caching, it's only purpose is to respond correctly
70             to conditional requests. Neither does this plugin set any caching-directives
71             that are part of RFC_7234 (Caching), and for which there is a seperate plugin.
72              
73             =head2 Lost-Updates
74              
75             For REST api's it is important to understand that it is a Stateless interface.
76             This means that there is no such thing as record locking on the server. Would
77             one desire to edit a resource, the only way to check that one is not accidently
78             overwritin someone elses changes, is comparing it with weak or strong
79             validators, like date/time of a last modification - or a unique version
80             identifier, known as a eTag.
81              
82             =head2 Strong and weak validators
83              
84             ETags are stronger validators than the Date Last-Modified. In the above
85             described example, it has two validators provided that can be used to check the
86             conditional request. If the client did set an eTag conditional in 'If-Matched'
87             or 'If-None-Matched', it will try to match that. If not, it will try to match
88             against the Date Last-Modified with either the 'If-Modified-Since' or
89             'If-Unmodified-Since'.
90              
91             =head2 Required or not
92              
93             The optional 'required' turns the API into a strict mode. Running under 'strict'
94             ensures that the client will provided either the eTag or Date-Modified validator
95             for un-safe requests. If not provided when required, it will return a response
96             with status 428 (Precondition Required) (RFC 6585).
97              
98             When set to false, it allows a client to sent of a request without the headers
99             for the conditional requests and as such have bypassed all the checks end up in
100             the last validation step and continue with the requested operation.
101              
102             =head2 Safe and unsafe methods
103              
104             Sending these validators with a GET request is used for caching and respond with
105             a status of 304 (Not Modified) when the client has a 'fresh' version. Remember
106             though to send of current caching-information too (according to the RFC 7232).
107              
108             When used with 'unsafe' methods that will cause updates, these validators can
109             prevent 'lost updates' and will respond with 412 (Precondition Failed) when
110             there might have happened an intermediate update.
111              
112             =head2 Generating eTags and Dates Last-Modified
113              
114             Unfortunately, for a any method one might have to retrieve and process the
115             resource data before being capable of generating a eTag. Or one might have to go
116             through a few pieces of underlying data structures to find that
117             last-modification date.
118              
119             For a GET method one can then skip the 'post-processing' like serialisation and
120             one does no longer have to send the data but only the status message 304
121             (Not Modified).
122              
123             =head2 More reading
124              
125             There is a lot of additional information in RFC-7232 about generating and
126             retrieving eTags or last-modification-dates. Please read-up in the RFC about
127             those topics.
128              
129             =cut
130              
131             =head1 Dancer2 Keywords
132              
133             =head2 http_conditional
134              
135             This keyword takes as last argument the CODEREF of the part that should be ran
136             if the pre-conditions are met.
137              
138             A optional hashref takes the options
139              
140             =over
141              
142             =item etag
143              
144             a string that 'uniquely' identifies the current version of the resource.
145              
146             =item last_modified
147              
148             a HTTP Date compliant string of the date/time this resource was last updated.
149              
150             or
151              
152             a DateTime object.
153              
154             A suitable string can be created from a UNIX timestamp using
155             L, or from a L
156             object using C from L.
157              
158             =item required
159              
160             if set to true, it enforces clients that request a unsafe method to privide one
161             or both validators.
162              
163             =back
164              
165             If used with either a GET or a HEAD method, the validators mentioned in the
166             options are returned in the appropriate HTTP Header Fields.
167              
168             =cut
169              
170             register http_conditional => sub {
171             my $dsl = shift;
172             my $coderef = pop;
173             my $args = shift;
174            
175             unless ( $coderef && ref $coderef eq 'CODE' ) {
176             return sub {
177             warn "http_conditional: missing CODE-REF";
178             };
179             };
180            
181             # To understand the flow of the desicions the original text of the RFC has
182             # been included so one can see that it does what the RFC says, not what the
183             # evelopper thinks it should do.
184            
185             # Additional checks for argument validation have been added.
186            
187             goto STEP_1 if not $args->{required};
188            
189             # RFC-6585 - Status 428 (Precondition Required)
190             #
191             # For a GET, it would be totaly safe to return a fresh response,
192             # however, for unsafe methods it could be required that the client
193             # does provide the eTag or DateModified validators.
194             #
195             # setting the pluging config with something like: required => 1
196             # might be a nice way to handle it for the entire app, turning it
197             # into a strict modus.
198            
199             if ($dsl->http_method_is_nonsafe) {
200             # warn "http_conditional: http_method_is_nonsafe";
201             return $dsl->_http_status_precondition_required_etag
202             if ( $args->{etag}
203             and not $dsl->request->header('If-Match') );
204             return $dsl->_http_status_precondition_required_last_modified
205             if ( $args->{last_modified}
206             and not $dsl->request->header('If-Unmodified-Since') );
207             } else {
208             # warn "http_conditional: http_method_is_safe";
209             };
210            
211              
212             # RFC 7232 Hypertext Transfer Protocol (HTTP/1.1): Conditional Requests
213             #
214             # Section 6. Precedence
215             #
216             # When more than one conditional request header field is present in a
217             # request, the order in which the fields are evaluated becomes
218             # important. In practice, the fields defined in this document are
219             # consistently implemented in a single, logical order, since "lost
220             # update" preconditions have more strict requirements than cache
221             # validation, a validated cache is more efficient than a partial
222             # response, and entity tags are presumed to be more accurate than date
223             # validators.
224            
225             STEP_1:
226             # When recipient is the origin server and If-Match is present,
227             # evaluate the If-Match precondition:
228            
229             # if true, continue to step 3
230            
231             # if false, respond 412 (Precondition Failed) unless it can be
232             # determined that the state-changing request has already
233             # succeeded (see Section 3.1)
234            
235             if ( defined ($dsl->request->header('If-Match')) ) {
236            
237             # check arguments and http-headers
238             return $dsl->_http_status_precondition_failed_no_etag
239             if not exists $args->{etag};
240            
241            
242             # RFC 7232
243             if ( $dsl->request->header('If-Match') eq $args->{etag} ) {
244             goto STEP_3;
245             } else {
246             $dsl->status(412); # Precondition Failed
247             return;
248             }
249             }
250            
251             STEP_2:
252             # When recipient is the origin server, If-Match is not present, and
253             # If-Unmodified-Since is present, evaluate the If-Unmodified-Since
254             # precondition:
255            
256             # if true, continue to step 3
257            
258             # if false, respond 412 (Precondition Failed) unless it can be
259             # determined that the state-changing request has already
260             # succeeded (see Section 3.4)
261            
262             if ( defined ($dsl->request->header('If-Unmodified-Since')) ) {
263            
264             # check arguments and http-headers
265             return $dsl->_http_status_precondition_failed_no_date
266             if not exists $args->{last_modified};
267            
268             my $rqst_date = DateTime::Format::HTTP->parse_datetime(
269             $dsl->request->header('If-Unmodified-Since')
270             );
271             return $dsl->_http_status_bad_request_if_unmodified_since
272             if not defined $rqst_date;
273            
274             my $last_date = $args->{last_modified}->isa('DateTime')
275             ? $args->{last_modified}
276             : DateTime::Format::HTTP->parse_datetime($args->{last_modified});
277             return $dsl->_http_status_server_error_bad_last_modified
278             if not defined $last_date;
279            
280            
281             # RFC 7232
282             if ( $rqst_date > $last_date ) {
283             goto STEP_3;
284             } else {
285             $dsl->status(412); # Precondition Failed
286             return;
287             }
288             }
289              
290             STEP_3:
291             # When If-None-Match is present, evaluate the If-None-Match
292             # precondition:
293            
294             # if true, continue to step 5
295            
296             # if false for GET/HEAD, respond 304 (Not Modified)
297            
298             # if false for other methods, respond 412 (Precondition Failed)
299            
300             if ( defined ($dsl->request->header('If-None-Match')) ) {
301            
302             # check arguments and http-headers
303             return $dsl->_http_status_precondition_failed_no_etag
304             if not exists $args->{etag};
305            
306            
307             # RFC 7232
308             if ( $dsl->request->header('If-None-Match') ne $args->{etag} ) {
309             goto STEP_5;
310             } else {
311             if (
312             $dsl->request->method eq 'GET'
313             or
314             $dsl->request->method eq 'HEAD'
315             ) {
316             $dsl->status(304); # Not Modified
317             return;
318             } else {
319             $dsl->status(412); # Precondition Failed
320             return;
321             }
322             }
323             }
324              
325             STEP_4:
326             # When the method is GET or HEAD, If-None-Match is not present, and
327             # If-Modified-Since is present, evaluate the If-Modified-Since
328             # precondition:
329            
330             # if true, continue to step 5
331            
332             # if false, respond 304 (Not Modified)
333              
334             if (
335             ($dsl->request->method eq 'GET' or $dsl->request->method eq 'HEAD')
336             and
337             not defined($dsl->request->header('If-None-Match'))
338             and
339             defined($dsl->request->header('If-Modified-Since'))
340             ) {
341            
342             # check arguments and http-headers
343             return $dsl->_http_status_precondition_failed_no_date
344             if not exists $args->{last_modified};
345            
346             my $rqst_date = DateTime::Format::HTTP->parse_datetime(
347             $dsl->request->header('If-Modified-Since')
348             );
349             return $dsl->_http_status_bad_request_if_modified_since
350             if not defined $rqst_date;
351            
352             my $last_date = $args->{last_modified}->isa('DateTime')
353             ? $args->{last_modified}
354             : DateTime::Format::HTTP->parse_datetime($args->{last_modified});
355             return $dsl->_http_status_server_error_bad_last_modified
356             if not defined $last_date;
357            
358            
359             # RFC 7232
360             if ( $rqst_date < $last_date ) {
361             goto STEP_5;
362             } else {
363             $dsl->status(304); # Not Modified
364             return;
365             }
366             }
367            
368             STEP_5:
369             # When the method is GET and both Range and If-Range are present,
370             # evaluate the If-Range precondition:
371            
372             # if the validator matches and the Range specification is
373             # applicable to the selected representation, respond 206
374             # (Partial Content) [RFC7233]
375            
376             undef; # TODO (BTW, up till perl 5.13, this would break because of labels
377            
378             STEP_6:
379             # Otherwise,
380            
381             # all conditions are met, so perform the requested action and
382             # respond according to its success or failure.
383            
384             # set HTTP Header-fields for GET / HEAD requests
385             if (
386             ($dsl->request->method eq 'GET' or $dsl->request->method eq 'HEAD')
387             ) {
388             if ( exists($args->{etag}) ) {
389             $dsl->header('ETag' => $args->{etag})
390             }
391             if ( exists($args->{last_modified}) ) {
392             my $last_date = $args->{last_modified}->isa('DateTime')
393             ? $args->{last_modified}
394             : DateTime::Format::HTTP->parse_datetime($args->{last_modified});
395             return $dsl->_http_status_server_error_bad_last_modified
396             if not defined $last_date;
397             $dsl->header('Last-Modified' =>
398             DateTime::Format::HTTP->format_datetime($last_date) )
399             }
400             }
401            
402             # RFC 7232
403             return $coderef->($dsl);
404            
405             };
406              
407             # RFC 7231 HTTP/1.1 Semantics and Content
408             # section 4.2.1 Common Method Properties - Safe Methods#
409             # http://tools.ietf.org/html/rfc7231#section-4.2.1
410             # there is a patch for Dancer2 it self
411             register http_method_is_safe => sub {
412             return (
413             $_[0]->request->method eq 'GET' ||
414             $_[0]->request->method eq 'HEAD' ||
415             $_[0]->request->method eq 'OPTIONS' ||
416             $_[0]->request->method eq 'TRACE'
417             );
418             };
419              
420             register http_method_is_nonsafe => sub {
421             return not $_[0]->http_method_is_safe();
422             };
423              
424             sub _http_status_bad_request_if_modified_since {
425 0     0     warn "http_conditional: bad formatted date 'If-Modified-Since'";
426 0           $_[0]->status(400); # Bad Request
427 0           return;
428             }
429              
430             sub _http_status_bad_request_if_unmodified_since {
431 0     0     warn "http_conditional: bad formatted date 'If-Unmodified-Since'";
432 0           $_[0]->status(400); # Bad Request
433 0           return;
434             }
435              
436             sub _http_status_precondition_failed_no_date {
437 0     0     warn "http_conditional: not provided 'last_modified'";
438 0           $_[0]->status(412); # Precondition Failed
439 0           return;
440             }
441              
442             sub _http_status_precondition_failed_no_etag {
443 0     0     warn "http_conditional: not provided 'eTag'";
444 0           $_[0]->status(412); # Precondition Failed
445 0           return;
446             }
447              
448             sub _http_status_precondition_required_etag {
449 0     0     warn "http_conditional: Precondition Required 'ETag'";
450 0           $_[0]->status(428); # Precondition Required
451 0           return;
452             }
453              
454             sub _http_status_precondition_required_last_modified {
455 0     0     warn "http_conditional: Precondition Required 'Date Last-Modified'";
456 0           $_[0]->status(428); # Precondition Required
457 0           return;
458             }
459              
460             sub _http_status_server_error_bad_last_modified {
461 0     0     $_[0]->status(500); # Precondition Failed
462 0           return "http_conditional: bad formatted date 'last_modified'";
463             }
464              
465             on_plugin_import {
466             my $dsl = shift;
467             my $app = $dsl->app;
468             };
469              
470             register_plugin;
471              
472             =head1 AUTHOR
473              
474             Theo van Hoesel, C<< >>
475              
476             =head1 BUGS
477              
478             Please report any bugs or feature requests to
479             C, or through the web
480             interface at
481             L.
482             I will be notified, and then you'll automatically be notified of progress on
483             your bug as I make changes.
484              
485              
486              
487             =head1 SUPPORT
488              
489             You can find documentation for this module with the perldoc command.
490              
491             perldoc Dancer2::Plugin::HTTP::ConditionalRequest
492              
493              
494             You can also look for information at:
495              
496             =over 4
497              
498             =item * RT: CPAN's request tracker (report bugs here)
499              
500             L
501              
502             =item * AnnoCPAN: Annotated CPAN documentation
503              
504             L
505              
506             =item * CPAN Ratings
507              
508             L
509              
510             =item * Search CPAN
511              
512             L
513              
514             =back
515              
516              
517             =head1 ACKNOWLEDGEMENTS
518              
519              
520             =head1 LICENSE AND COPYRIGHT
521              
522             Copyright 2015 Theo van Hoesel.
523              
524             This program is free software; you can redistribute it and/or modify it
525             under the terms of the the Artistic License (2.0). You may obtain a
526             copy of the full license at:
527              
528             L
529              
530             Any use, modification, and distribution of the Standard or Modified
531             Versions is governed by this Artistic License. By using, modifying or
532             distributing the Package, you accept this license. Do not use, modify,
533             or distribute the Package, if you do not accept this license.
534              
535             If your Modified Version has been derived from a Modified Version made
536             by someone other than you, you are nevertheless required to ensure that
537             your Modified Version complies with the requirements of this license.
538              
539             This license does not grant you the right to use any trademark, service
540             mark, tradename, or logo of the Copyright Holder.
541              
542             This license includes the non-exclusive, worldwide, free-of-charge
543             patent license to make, have made, use, offer to sell, sell, import and
544             otherwise transfer the Package with respect to any patent claims
545             licensable by the Copyright Holder that are necessarily infringed by the
546             Package. If you institute patent litigation (including a cross-claim or
547             counterclaim) against any party alleging that the Package constitutes
548             direct or contributory patent infringement, then this Artistic License
549             to you shall terminate on the date that such litigation is filed.
550              
551             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
552             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
553             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
554             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
555             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
556             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
557             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
558             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
559              
560             =cut
561              
562             1;