File Coverage

blib/lib/Business/Stripe/Webhook.pm
Criterion Covered Total %
statement 108 129 83.7
branch 43 54 79.6
condition 4 9 44.4
subroutine 14 15 93.3
pod 7 7 100.0
total 176 214 82.2


line stmt bran cond sub pod time code
1             package Business::Stripe::Webhook;
2            
3 9     9   1300369 use JSON::PP;
  9         144742  
  9         997  
4 9     9   4748 use Digest::SHA qw(hmac_sha256_hex);
  9         35333  
  9         9771  
5 9     9   9261 use Time::Piece;
  9         139854  
  9         50  
6 9     9   8256 use HTTP::Tiny;
  9         603091  
  9         525  
7            
8 9     9   82 use strict;
  9         19  
  9         285  
9 9     9   43 use warnings;
  9         16  
  9         16598  
10            
11             our $VERSION = '1.14';
12             $VERSION = eval $VERSION;
13            
14             sub new {
15 20     20 1 1546445 my $class = shift;
16 20         162 my %vars = @_;
17            
18 20 100 66     121 if (exists $vars{'error'} && ref $vars{'error'} eq 'CODE') {
19 1         21 $vars{'error_callback'} = $vars{'error'};
20             }
21 20         87 $vars{'error'} = '';
22            
23 20         122 $vars{'reply'} = {
24             'status' => 'noaction',
25             'sent_to' => [ ],
26             'sent_to_all' => 'false',
27             };
28            
29 20 100       81 if (exists $vars{'payload'}) {
30 17         40 $vars{'webhook'} = eval { decode_json($vars{'payload'});};
  17         111  
31 17 100       163634 $vars{'error'} = 'Missing payload data' unless $vars{'webhook'};
32             } else {
33             # Obtaining payload from STDIN only
34             # exists for backward compatability
35             # This option is deprecated and will
36             # be removed in a future version
37 3 100       11 if (exists $ENV{'GATEWAY_INTERFACE'}) {
38 2         59 read(STDIN, $vars{'payload'}, $ENV{'CONTENT_LENGTH'});
39 2 100       17 $vars{'webhook'} = decode_json($vars{'payload'}) if $vars{'payload'};
40 2 100       63445 $vars{'error'} = 'No payload data' unless $vars{'webhook'};
41             } else {
42 1         4 $vars{'error'} = 'Looks like this is not a web request!';
43             }
44             }
45            
46 20         134 return bless \%vars, $class;
47             }
48            
49             # Returns true if last operation was success
50             sub success {
51 12     12 1 746 my $self = shift;
52 12         87 return !$self->{'error'};
53             }
54            
55             # Returns error if last operation failed
56             sub error {
57 7     7 1 18 my $self = shift;
58 7         39 return $self->{'error'};
59             }
60            
61             # Deal with webhook calls
62             sub process {
63 17     17 1 234 my $self = shift;
64            
65 17         72 $self->{'error'} = '';
66            
67 17 50       83 if (!defined $self->{'payload'}) {
68 0         0 $self->_error("No payload to process");
69 0         0 return undef;
70             }
71            
72 17 100       71 if ($self->{'signing_secret'}) {
73 10 100       56 if (!$ENV{'HTTP_STRIPE_SIGNATURE'}) {
74 1         8 $self->_error('Stripe-Signature HTTP heading missing - the request is not from Stripe');
75 1         42 return undef;
76             }
77 9         50 my $sig = $self->check_signature;
78 9 100       37 return undef unless defined $sig;
79 6 100       22 if (!$sig) {
80 5         26 $self->_error('Invalid Stripe Signature');
81 5         158 return undef;
82             }
83             }
84            
85 8         32 my $hook_type = $self->{'webhook'}->{'type'};
86            
87 8 50       38 if (!$hook_type) {
88 0         0 $self->_error("Invalid webhook payload");
89 0         0 return undef;
90             }
91            
92 8         45 $hook_type =~ s/\./-/g;
93 8 100       47 if (exists $self->{$hook_type}) {
94 4         11 push @{$self->{'reply'}->{'sent_to'}}, $hook_type;
  4         16  
95 4         10 &{$self->{$hook_type}}($self->{'webhook'});
  4         24  
96             }
97            
98 8 100       2558 if (exists $self->{'all-webhooks'}) {
99 1         5 $self->{'reply'}->{'sent_to_all'} = 'true';
100 1         2 push @{$self->{'reply'}->{'sent_to'}}, 'all-webhooks';
  1         9  
101 1         3 &{$self->{'all-webhooks'}}($self->{'webhook'});
  1         5  
102             }
103            
104 8         35 $self->{'reply'}->{'type'} = $self->{'webhook'}->{'type'};
105            
106 8         40 return $self->{'reply'};
107             }
108            
109             # Check for correct Stripe Signature
110             sub check_signature {
111 9     9 1 22 my $self = shift;
112            
113 9         34 $self->{'error'} = '';
114            
115 9 50       43 if (!$self->{'signing_secret'}) {
116 0         0 $self->_warning('No signing secret has been supplied');
117 0         0 return undef;
118             }
119 9 50       38 if (!$ENV{'HTTP_STRIPE_SIGNATURE'}) {
120 0         0 $self->_warning('Stripe-Signature HTTP heading missing');
121 0         0 return undef;
122             }
123            
124 9         120 my %sig_head = ($ENV{'HTTP_STRIPE_SIGNATURE'} . ',') =~ /(\S+?)=(\S+?),/g;
125              
126 9 100       38 if (!defined $sig_head{'t'}) {
127 1         5 $self->_error("No t Parameter");
128 1         29 return undef;
129             }
130              
131 8         33 my $signed_payload = $sig_head{'t'} . '.' . $self->{'payload'};
132              
133 8 100       48 if (!defined $sig_head{'v1'}) {
134 1         5 $self->_error("No v1 Parameter");
135 1         42 return undef;
136             }
137              
138 7 100 66     40 if (defined $self->{'tolerance'} && abs(time - $sig_head{'t'}) > $self->{'tolerance'}) {
139 1         5 $self->_error("Timestamp outside tolerance");
140 1         32 return undef;
141             }
142            
143 6 100       253 if (hmac_sha256_hex($signed_payload, $self->{'signing_secret'}) eq $sig_head{'v1'}) {
144 1         5 return 1;
145             }
146 5         20 return 0;
147             }
148            
149             # Send reply to Stripe
150             sub reply {
151 4     4 1 2036 my $self = shift;
152 4         33 my %keys = @_;
153            
154 4         34 $self->{'reply'}->{'timestamp'} = localtime->datetime;
155 4 100       638 if ($self->{'error'}) {
156 1         4 $self->{'reply'}->{'error'} = $self->{'error'};
157 1         3 $self->{'reply'}->{'status'} = 'failed';
158             }
159            
160 4         14 foreach my $key(keys %keys) {
161 0         0 $self->{'reply'}->{$key} = $keys{$key};
162             }
163            
164 4         16 print "Content-type: application/json\n\n";
165 4         21 print encode_json $self->{'reply'};
166 4         1326 return;
167             }
168            
169             # Retrieve subscription object from Stripe
170             sub get_subscription {
171 2     2 1 8386 my ($self, $subscription, $secret) = @_;
172            
173 2         13 $self->{'error'} = '';
174            
175 2 50       9 if (!$subscription) {
176 0         0 $self->{'error'} = 'Subscription missing';
177 0         0 $self->_error('Subscription missing');
178 0         0 return undef;
179             }
180            
181 2 50       8 $self->{'api_secret'} = $secret if defined $secret;
182            
183 2 50       10 if (!$self->{'api_secret'}) {
184 0         0 $self->{'error'} = 'No Secret Key supplied to fetch subscription';
185 0         0 return undef;
186             }
187            
188             my $headers = {
189             'headers' => {
190 2         29 'Authorization' => 'Bearer ' . $self->{'api_secret'},
191             'Stripe-Version' => '2022-11-15',
192             },
193             'agent' => "Perl-Business::Stripe::Webhook-v$VERSION",
194             };
195            
196 2         18 my $http = HTTP::Tiny->new;
197 2         367 return $http->get("https://api.stripe.com/v1/subscriptions/$subscription", $headers);
198             }
199            
200             sub _error {
201 9     9   32 my ($self, $message) = @_;
202            
203 9         22 $self->{'error'} = $message;
204 9 100       34 if (defined $self->{'error_callback'}) {
205 1         2 &{$self->{'error_callback'}}($message);
  1         4  
206             } else {
207 8         115 STDERR->print("Stripe Webhook Error: $message\n");
208             }
209             }
210            
211             sub _warning {
212 0     0     my ($self, $message) = @_;
213            
214 0 0 0       return if $self->{'warning'} and $self->{'warning'} =~ /^nowarn/i;
215 0           $self->{'error'} = $message;
216 0 0         if (defined $self->{'warning'}) {
217 0           &{$self->{'warning'}}($message);
  0            
218             } else {
219 0           STDERR->print("Stripe Webhook Warning: $message\n");
220             }
221             }
222            
223            
224             __END__