no POD tests, base64 encodes right stuff
[Net-Amazon-S3-Policy.git] / lib / Net / Amazon / S3 / Policy.pm
blob008755454e141885ed766cfce1d4936ce83b9b44
1 package Net::Amazon::S3::Policy;
3 use warnings;
4 use strict;
5 use version; our $VERSION = qv('0.1.4');
7 use Carp;
8 use English qw( -no_match_vars );
9 use JSON;
10 use Encode ();
11 use MIME::Base64 qw< decode_base64 >;
13 use Exporter;
14 our @ISA = qw( Exporter );
15 our @EXPORT_OK = qw( exact starts_with range );
16 our %EXPORT_TAGS = (all => \@EXPORT_OK,);
18 # Module implementation here
19 sub new {
20 my $class = shift;
21 my %args = ref($_[0]) ? %{$_[0]} : @_;
22 my $self = bless {}, $class;
24 if ($args{json}) {
25 $self->parse($args{json});
27 else {
28 $self->expiration($args{expiration}) if defined $args{expiration};
29 $self->conditions([]);
30 $self->add($_) for @{$args{conditions} || []};
33 return $self;
34 } ## end sub new
36 # Accessors
37 sub expiration {
38 my $self = shift;
39 my $previous = $self->{expiration};
40 if (@_) {
41 my $time = shift;
42 if ($time && $time =~ /\A \d+ \z/mxs) {
43 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
44 gmtime($time);
45 $time = sprintf "%04d-%02d-%02dT%02d:%02d:%02d.000Z",
46 $year + 1900,
47 $mon + 1, $mday, $hour, $min, $sec;
48 } ## end if ($time && $time =~ ...
49 $time ? ($self->{expiration} = $time) : delete $self->{expiration};
50 } ## end if (@_)
51 return $previous;
52 } ## end sub expiration
54 sub conditions {
55 my $self = shift;
56 my $previous = $self->{conditions};
58 if (@_) {
59 $self->{conditions} = (scalar(@_) == 1) ? shift : [@_];
62 return $previous;
63 } ## end sub conditions
65 { # try to understand rules
67 sub _prepend_dollar {
68 return substr($_[0], 0, 1) eq '$' ? $_[0] : '$' . $_[0];
70 my @DWIMs = (
71 qr{\A\s* (\S+?) \s* \* \s*\z}mxs => sub {
72 my $target = _prepend_dollar(shift);
73 return starts_with($target, '');
75 qr{\A\s* (\S+) \s+ eq \s+ (.*?) \s*\z}mxs => sub{
76 my $target = _prepend_dollar(shift);
77 my $value = shift;
78 return $value eq '*' ? starts_with($target, '') : exact($target, $value);
80 qr{\A\s* (\S+) \s+ (?: ^ | starts[_-]?with) \s+ (.*?) \s*\z}mxs => sub {
81 my $target = _prepend_dollar(shift);
82 my $prefix = shift;
83 return starts_with($target, $prefix);
85 qr{\A\s* (\d+) \s*<=\s* (\S+) \s*<=\s* (\d+) \s*\z}mxs => sub {
86 my ($min, $value, $max) = @_;
87 s{_}{}g for $min, $max;
89 # no "_prepend_dollar" for range conditions
90 return range($value, $min, $max);
94 sub _resolve_rule {
95 my ($string) = @_;
97 for my $i (0 .. (@DWIMs - 1) / 2) {
98 my ($regex, $callback) = @DWIMs[$i * 2, $i * 2 + 1];
99 if (my @captures = $string =~ /$regex/) {
100 my $result = $callback->(@captures);
101 return $result if defined $result;
105 croak "could not understand '$_', bailing out";
106 } ## end sub _resolve_rule
109 sub add {
110 my ($self, $condition) = @_;
111 push @{$self->conditions()},
112 ref($condition) ? $condition : _resolve_rule($condition);
113 return;
116 sub remove {
117 my ($self, $condition) = @_;
118 $condition = _resolve_rule($condition) unless ref $condition;
119 my $conditions = $self->conditions();
120 my @filtered = grep {
121 my $keep;
122 if (@$condition != @$_) { # different lengths => different
123 $keep = 1;
125 else {
126 for my $i (0 .. $#$condition) {
127 last if $keep = $condition->[$i] ne $_->[$i];
130 $keep;
131 } @$conditions;
132 $self->conditions(\@filtered);
133 return;
134 } ## end sub remove
136 sub exact {
137 shift if ref $_[0];
138 my ($target, $value) = @_;
139 return ['eq', $target, $value];
142 sub starts_with {
143 shift if ref $_[0];
144 my ($target, $value) = @_;
145 return ['starts-with', $target, $value];
148 sub range {
149 shift if ref $_[0];
150 my ($target, $min, $max) = @_;
151 return [$target, $min, $max];
154 sub json {
155 my ($self, $args) = @_;
156 my %params = %$self;
157 delete $params{expiration} unless defined $params{expiration};
158 return to_json(\%params, $args);
159 } ## end sub json
161 sub base64 {
162 return encode_base64(Encode::encode('utf-8', $_[0]->json()));
166 no warnings;
167 *stringify = \&json;
168 *json_base64 = \&base64;
169 *stringify_base64 = \&base64;
172 sub parse {
173 my ($self, $json) = @_;
175 $json = decode_base64($json)
176 unless substr($json, 0, 1) eq '{';
178 my %decoded = %{from_json($json)};
179 $self->{conditions} = [
180 map {
181 if (ref($_) eq 'ARRAY') { $_; }
182 else {
183 my ($name, $value) = %$_;
184 ['eq', '$' . $name, $value];
186 } @{$decoded{conditions}}
188 $self->{expiration} = $decoded{expiration};
190 return $self;
191 } ## end sub parse
193 sub signature {
194 my ($self, $key) = @_;
195 require Digest::HMAC_SHA1;
196 return Digest::HMAC_SHA1::hmac_sha1($self->base64(), $key);
199 sub signature_base64 {
200 my ($self, $key) = @_;
201 return encode_base64($self->signature($key));
204 # Wrapper around base64 encoder, ensuring that there's no newline
205 # to make AWS S3 happy
206 sub encode_base64 {
207 return MIME::Base64::encode_base64($_[0], '');
210 1; # Magic true value required at end of module
211 __END__
213 =encoding iso-8859-1
215 =head1 NAME
217 Net::Amazon::S3::Policy - manage Amazon S3 policies for HTTP POST forms
219 =head1 VERSION
221 This document describes Net::Amazon::S3::Policy version 0.1.3. Most likely,
222 this version number here is outdate, and you should peek the source.
225 =head1 SYNOPSIS
227 use Net::Amazon::S3::Policy;
229 # Expire in one hour
230 my $policy = Net::Amazon::S3::Policy->new(expiration => time() + 3600);
232 # Do What I Mean handling of conditions
233 # Note: single quotes, $key is not a Perl variable in this example!
234 $policy->add('$key eq path/to/somewhere');
235 # In DWIM mode, '$' are pre-pended automatically where necessary
236 $policy->add('key eq path/to/somewhere');
237 $policy->add('x-some-field starts-with some-prefix');
238 $policy->add(' 0 <= content-length-range <= 1_000_000 ');
239 $policy->add('whatever *'); # any value admitted for field 'whatever'
241 # NON-DWIM interface for conditions
242 use Net::Amazon::S3::Policy qw( :all ); # OR
243 use Net::Amazon::S3::Policy qw( exact starts_with range );
244 $policy->add(exact('$field', 'whatever spaced value ');
245 $policy->add(starts_with('$other-field', ' yadda ');
246 $policy->add(range('percentual', 0, 100));
248 # The output as JSON
249 print $policy->stringify(), "\n"; # OR
250 print $policy->json(), "\n";
252 # Where the stuff is really needed: HTML FORMs for HTTP POSTs
253 my $policy_for_form = $policy->base64();
254 my $signature_for_form = $policy->signature_base64($key);
256 # If you ever receive a policy...
257 my $received = Net::Amazon::S3::Policy->new(json => $json_text);
258 my $rec2 = Net::Amazon::S3::Policy->new();
259 $rec2->parse($json_base64); # either JSON or its Base64 encoding
262 =head1 DESCRIPTION
264 Net::Amazon::S3::Policy gives you an object-oriented interface to
265 manage policies for Amazon S3 HTTP POST uploads.
267 Amazon S3 relies upon either a REST interface (see L<Net::Amazon::S3>)
268 or a SOAP one; as an added service, it is possible to give access to
269 the upload of resources using HTTP POSTs that do not involve using
270 any of these two interfaces, but a single HTML FORM. The rules you
271 have to follow are explained in the Amazon S3 Developer Guide.
273 More or less, it boils down to the following:
275 =over
277 =item *
279 if the target bucket is not writeable by the anonymous group, you'll need
280 to set an access policy;
282 =item *
284 almost every field in the HTML FORM that will be used to build up the HTTP POST
285 message by the browser needs to be included into a I<policy>, and the policy
286 has to be sent along within the HTTP POST
288 =item *
290 together with the I<policy>, also the bucket owner's AWS ID (the public one) has to
291 be sent, together with a digital signature of the policy that has to be created
292 using the bucket owner's AWS secret key.
294 =back
296 So, you'll have to add three fields to the HTTP POST in order for it to comply
297 with Amazon's requirement when the bucket is not publicly writeable:
299 =over
301 =item C<AWSAccessKeyId>
303 given "as-is", i.e. as you copied from your account in Amazon Web Services;
305 =item C<policy>
307 given as a JSON document that is Base64 encoded;
309 =item C<signature>
311 calculated as a SHA1-HMAC of the Base64-encoded policy, using your secret
312 key as the signature key, and then encoded with Base64.
314 =back
316 This module lets you manage the build-up of a policy document, providing you
317 with tools to get the Base64 encoding of the resulting JSON policy document,
318 and to calculate the Base64 encoding of the signature. See L</Example> for
319 a complete example of how to do this.
321 In addition to I<policy synthesis>, the module is also capable of parsing
322 some policy (base64-encoded or not, but in JSON format). This shouldn't
323 be a need in general... possibly for debug reasons.
325 =head2 Example
327 For example, suppose that you have the following HTML FORM to allow selected
328 uploads to the C<somebucket> bucket (see the Amazon S3 Developer Guide for
329 details about writing the HTML FORM):
331 <form action="http://somebucket.s3.amazonaws.com/" method="post"
332 enctype="multipart/form-data">
333 <!-- inputs needed because bucket is not publicly writeable -->
334 <input type="hidden" name="AWSAccessKeyId" value="your-ID-here">
335 <input type="hidden" name="policy" value="base64-encoded-policy">
336 <input type="hidden" name="signature" value="base64-encoded-signature">
338 <!-- input needed by AWS-S3 logic: there MUST be a key -->
339 <input type="hidden" name="key" value="/restricted/${filename}">
341 <!-- inputs that you want to include in your form -->
342 <input type="hidden" name="Content-Type" value="image/jpeg">
343 <label for="colour">Colour</label>
344 <input type="text" id="colour" name="x-amz-meta-colour" value="green">
346 <!-- input needed to have something to upload. LAST IN FORM! -->
347 <input type="file" id="file" name="file">
348 </form>
350 You need to include the following elements in your policy:
352 =over
354 =item *
356 C<key>
358 =item *
360 C<Content-Type>
362 =item *
364 C<x-amz-meta-colour>
366 =back
368 Your policy can then be built like this:
370 my $policy = Net::Amazon::S3::Policy->new(
371 expiration => time() + 60 * 60, # one-hour policy
372 conditions => [
373 '$key starts-with /restricted/', # restrict to here
374 '$Content-Type starts-with image/', # accept any image format
375 '$x-amz-meta-colour *', # accept any colour
376 'bucket eq somebucket',
380 # Put this as the value for "policy",
381 # instead of "base64-encoded-policy"
382 my $policy_for_form = $policy->base64();
384 # Put this as the value for "signature",
385 # instead of "base64-encoded-signature"
386 my $signature_for_form = $policy->signature_base64($key);
388 =head1 INTERFACE
390 =begin PrivateMethods
392 =head2 encode_base64
394 =end PrivateMethods
396 =head2 Module Interface
398 =over
400 =item B<< new (%args) >>
402 =item B<< new (\%args) >>
404 constructor to create a new Net::Amazon::S3::Policy object.
406 Arguments can be passed either as a single hash reference, or
407 as a hash. Choose whatever you like most.
409 Recognised keys are:
411 =over
413 =item expiration
415 the expiration date for this policy.
417 =item conditions
419 a list of conditions to initialise the object. This should
420 point to an array with the conditions, that will be passed through
421 the L</add> method.
423 =item json
425 a piece of JSON text to parse the configuration from. The presence
426 of this parameter overrides the other two.
428 =back
430 =item B<< expiration () >>
432 =item B<< expiration ($time) >>
434 get/set the expiration time for the condition. Set to a false value
435 to remove the expiration time from the policy.
437 You should either pass an ISO8601 datetime string, or an epoch value.
438 You'll always get an ISO8601 string back.
440 =item B<< conditions () >>
442 =item B<< conditions (@conditions) >>
444 =item B<< conditions (\@conditions) >>
446 get/set the conditions in the policy. You should never need to use this
447 method, because the L</add> and L</remove> are there for you to interact
448 with this member. If you want to use this, anyway, be sure to take
449 a look to the functions in L</Convenience Condition Functions>.
451 =item B<< add ($spec) >>
453 add a specification to the list of conditions.
455 A specification can be either an ARRAY reference, or a textual one:
457 =over
459 =item *
461 if you pass an ARRAY reference, it should be something like the one
462 returned by any of the functions in L</Convenience Condition Functions>;
464 =item *
466 othewise, it can be a string with a single condition, like the following
467 examples:
469 some-field eq some-value
470 $some-other-field starts-with /path/to/somewhere/
471 10 <= numeric-value <= 1000
473 Note that the string specification is less "strict" in checking its
474 parameters; in particular, you should stick to the ARRAY reference if
475 your parameters have a space inside. You can use the following formats:
477 =over
479 =item C<< <name> eq <value> >>
481 set a name to have a given value, exactly;
483 =item C<< <name> starts-with <prefix> >>
485 =item C<< <name> starts_with <prefix> >>
487 =item C<< <name> ^ <prefix> >>
489 set the prefix that has to be matched against the value
490 for the field with the given name. If the prefix is left
491 empty, every possible value will be admitted;
493 =item C<< <name> * >>
495 admit any value for the given field, just like setting an empty
496 value for a C<starts-with> rule;
498 =item C<< <min> <= <name> <= <max> >>
500 set an allowable range for the given field.
502 =back
504 Policies for exact or starts-with matching usually refer to the form's
505 field, thus requiring to refer them as "variables" with a prepended
506 dollar sign, just like Perl scalars (more or less). Thus, if you forget
507 to put it, it will be automatically added for you. Hence, the following
508 conditions are equivalent:
510 field eq blah
511 $field eq blah
513 because both yield the following condition in JSON:
515 ["eq","$field","blah"]
517 =back
519 =item B<< remove ($spec) >>
521 remove a condition in the list of conditions. The parameter is
522 regarded exactly as in L</add>; once it is found, the list of
523 conditions will be filtered to exclude that particular
524 condition, exactly.
526 =item B<< json () >>
528 =item B<< stringify () >>
530 get a textual version of the object, in JSON format. This is the
531 base format used to interact with Amazon S3.
533 =item B<< base64 () >>
535 =item B<< json_base64 () >>
537 =item B<< stringify_base64 () >>
539 get a textual version of the object, as a Base64 encoding of the
540 JSON representation (see L</json>). This is what should to be put
541 as C<policy> field in the POST form.
544 =item B<< parse ($json_text) >>
546 parse a JSON representation of a policy and fill in the object. This
547 is the opposite of L</json>.
550 =item B<< signature ($key) >>
552 get the signature for the Base 64 encoding of the JSON representation of
553 the policy. The signature is the SHA1-HMAC digital signature, with the
554 given key.
557 =item B<< signature_base64 ($key) >>
559 get the Base64 encoding of the signature, as given by L</signature>. This
560 is the value that should be put in the C<signature> field in the POST
561 form.
563 =back
565 =head2 Convenience Condition Functions
567 The following functions can be optionally imported from the
568 module, and can be used indifferently as class/instance
569 methods or as functions.
571 =over
573 =item B<< exact ($target, $value) >>
575 produce an I<exact value> condition. This condition is an array
576 reference with the following elements:
578 =over
580 =item *
582 the C<eq> string;
584 =item *
586 the I<name> of the field;
588 =item *
590 the I<value> that the field should match exactly.
592 =back
594 =item B<< starts_with ($target, $value) >>
596 produce a I<starts-with> condition. This condition is an array
597 reference with the following elements:
599 =over
601 =item *
603 the C<starts-with> string;
605 =item *
607 the I<name> of the field;
609 =item *
611 the I<prefix> that has to be matched by the field's value
613 =back
615 =item B<< range ($target, $min, $max) >>
617 produce a I<value range> condition. This condition is an array
618 reference with the following elements:
620 =over
622 =item *
624 the I<name> of the field;
626 =item *
628 the I<minimum> value allowed for the field's value;
630 =item *
632 the I<maximum> value allowed for the field's value;
634 =back
636 =back
638 =head1 DIAGNOSTICS
641 =over
643 =item C<< could not understand '%s', bailing out >>
645 The L</add> and L</remove> function try their best to understand
646 a condition when given in string form... but you should really
647 stick to the format given in the documentation!
649 =back
652 =head1 CONFIGURATION AND ENVIRONMENT
654 Net::Amazon::S3::Policy requires no configuration files or environment variables.
657 =head1 DEPENDENCIES
659 The C<version> pragma (which has been included in Perl 5.10) and the
660 L</JSON> module.
663 =head1 INCOMPATIBILITIES
665 None reported.
668 =head1 BUGS AND LIMITATIONS
670 No bugs have been reported.
672 Please report any bugs or feature requests through http://rt.cpan.org/
675 =head1 AUTHOR
677 Flavio Poletti C<< <flavio [at] polettix [dot] it> >>
680 =head1 LICENCE AND COPYRIGHT
682 Copyright (c) 2008, Flavio Poletti C<< <flavio [at] polettix [dot] it> >>. All rights reserved.
684 This module is free software; you can redistribute it and/or
685 modify it under the same terms as Perl 5.8.x itself. See L<perlartistic>
686 and L<perlgpl>.
688 Questo modulo è software libero: potete ridistribuirlo e/o
689 modificarlo negli stessi termini di Perl 5.8.x stesso. Vedete anche
690 L<perlartistic> e L<perlgpl>.
693 =head1 DISCLAIMER OF WARRANTY
695 BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
696 FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
697 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
698 PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
699 EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
700 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
701 ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
702 YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
703 NECESSARY SERVICING, REPAIR, OR CORRECTION.
705 IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
706 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
707 REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
708 LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
709 OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
710 THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
711 RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
712 FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
713 SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
714 SUCH DAMAGES.
716 =head1 NEGAZIONE DELLA GARANZIA
718 Poiché questo software viene dato con una licenza gratuita, non
719 c'è alcuna garanzia associata ad esso, ai fini e per quanto permesso
720 dalle leggi applicabili. A meno di quanto possa essere specificato
721 altrove, il proprietario e detentore del copyright fornisce questo
722 software "così com'è" senza garanzia di alcun tipo, sia essa espressa
723 o implicita, includendo fra l'altro (senza però limitarsi a questo)
724 eventuali garanzie implicite di commerciabilità e adeguatezza per
725 uno scopo particolare. L'intero rischio riguardo alla qualità ed
726 alle prestazioni di questo software rimane a voi. Se il software
727 dovesse dimostrarsi difettoso, vi assumete tutte le responsabilità
728 ed i costi per tutti i necessari servizi, riparazioni o correzioni.
730 In nessun caso, a meno che ciò non sia richiesto dalle leggi vigenti
731 o sia regolato da un accordo scritto, alcuno dei detentori del diritto
732 di copyright, o qualunque altra parte che possa modificare, o redistribuire
733 questo software così come consentito dalla licenza di cui sopra, potrà
734 essere considerato responsabile nei vostri confronti per danni, ivi
735 inclusi danni generali, speciali, incidentali o conseguenziali, derivanti
736 dall'utilizzo o dall'incapacità di utilizzo di questo software. Ciò
737 include, a puro titolo di esempio e senza limitarsi ad essi, la perdita
738 di dati, l'alterazione involontaria o indesiderata di dati, le perdite
739 sostenute da voi o da terze parti o un fallimento del software ad
740 operare con un qualsivoglia altro software. Tale negazione di garanzia
741 rimane in essere anche se i dententori del copyright, o qualsiasi altra
742 parte, è stata avvisata della possibilità di tali danneggiamenti.
744 Se decidete di utilizzare questo software, lo fate a vostro rischio
745 e pericolo. Se pensate che i termini di questa negazione di garanzia
746 non si confacciano alle vostre esigenze, o al vostro modo di
747 considerare un software, o ancora al modo in cui avete sempre trattato
748 software di terze parti, non usatelo. Se lo usate, accettate espressamente
749 questa negazione di garanzia e la piena responsabilità per qualsiasi
750 tipo di danno, di qualsiasi natura, possa derivarne.
752 =head1 SEE ALSO
754 L<Net::Amazon::S3>.
756 =cut