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