File Coverage

blib/lib/Plack/Middleware/AppStoreReceipt.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package Plack::Middleware::AppStoreReceipt;
2             # ABSTRACT: The Plack::Middleware that verifies Apple App Store Receipts for In-App Purchases
3              
4             our $VERSION = '0.03';
5              
6 1     1   40433 use warnings;
  1         2  
  1         31  
7 1     1   5 use strict;
  1         1  
  1         32  
8 1     1   5 use parent qw(Plack::Middleware);
  1         7  
  1         7  
9 1     1   17425 use Plack::Request;
  1         56824  
  1         34  
10 1     1   9 use Plack::Util::Accessor qw(route method path receipt_data allow_sandbox shared_secret);
  1         29  
  1         12  
11 1     1   1064 use JSON;
  1         14891  
  1         43  
12 1     1   1045 use Try::Tiny;
  1         1373  
  1         62  
13 1     1   1643 use Furl;
  0            
  0            
14              
15             sub prepare_app {
16             my $self = shift;
17              
18             $self->allow_sandbox( $self->allow_sandbox || 0 );
19              
20             if ( !$self->route ) {
21             $self->method( 'POST' ) if !$self->method;
22             $self->path( '/receipts/validate' ) if !$self->path;
23             } else {
24             while (my ($key, $value) = each %{$self->route}) {
25             $self->method( uc $key );
26             $self->path( $value );
27             }
28             }
29             }
30              
31             sub call {
32             my ( $self, $env ) = @_;
33              
34             my $vpath = ($env->{'PATH_INFO'} eq $self->path);
35             return [
36             405,
37             [
38             'Allow' => $self->method,
39             'Content-Type' => 'text/plain',
40             ],
41             ['Method not allowed'],
42             ] if $vpath && $env->{'REQUEST_METHOD'} ne $self->method;
43              
44             my $res = try { $self->app->($env) };
45              
46             $res = $self->_verify_receipt($env) if $vpath;
47              
48             return $res;
49             };
50              
51             sub _verify_receipt {
52             my ( $self, $env ) = @_;
53              
54             my $plack_req = Plack::Request->new($env);
55             my $receipt_data_param = $plack_req->param('receipt_data') || $plack_req->param($self->receipt_data);
56             my %params = ("receipt-data" => $receipt_data_param);
57             $params{password} = $self->shared_secret if $self->shared_secret;
58             my $receipt_data = encode_json (\%params);
59              
60             my $res;
61             $res = $self->_post_receipt_to_itunes( 'production', $receipt_data, $env );
62             if ( $res->[0] == 200 && $self->_is_sandboxed( $self->_parse_success_response( $res ) ) ) {
63             #should request to sandbox url since it's sandbox receipt!!
64             print "Retrying to post to sandbox environment...\n";
65             $res = $self->_post_receipt_to_itunes( 'sandbox', $receipt_data, $env );
66             }
67             return $res;
68             }
69              
70             sub _post_receipt_to_itunes {
71             my ( $self, $itunes_env, $receipt_data, $env ) = @_;
72              
73             die "sandbox request is not allowed" if $itunes_env eq 'sandbox' && !$self->allow_sandbox;
74              
75             my $endpoint = {
76             'production' => 'https://buy.itunes.apple.com/verifyReceipt',
77             'sandbox' => 'https://sandbox.itunes.apple.com/verifyReceipt',
78             };
79              
80             my $furl = Furl->new(
81             agent => 'Furl/2.15',
82             timeout => 10,
83             );
84              
85             my $res = $furl->post(
86             $endpoint->{$itunes_env}, # URL
87             ['Content-Type' => 'application/json'], # headers
88             $receipt_data, # form data (HashRef/FileHandle are also okay)
89             );
90              
91             return [200, ['Content-Type' => 'application/json'], [$res->content]] if $res->is_success;
92             return [500, ['Content-Type' => 'text/plain' ], ["error: ".$res->status_line."\n"]];
93             }
94              
95             sub _parse_success_response {
96             my ( $self, $res ) = @_;
97             return decode_json $res->[2]->[0];
98             }
99              
100             sub _is_sandboxed {
101             my ( $self, $json ) = @_;
102             return ( $json->{'status'} == 21007 ); #should be sandboxed!
103             }
104              
105             1;
106              
107              
108             __END__