File Coverage

blib/lib/Net/APNS/Simple.pm
Criterion Covered Total %
statement 57 91 62.6
branch 6 24 25.0
condition 3 20 15.0
subroutine 15 18 83.3
pod 2 3 66.6
total 83 156 53.2


line stmt bran cond sub pod time code
1             package Net::APNS::Simple;
2 3     3   142804 use 5.008001;
  3         30  
3 3     3   16 use strict;
  3         5  
  3         78  
4 3     3   15 use warnings;
  3         5  
  3         103  
5 3     3   19 use Carp ();
  3         6  
  3         75  
6 3     3   2074 use JSON;
  3         34473  
  3         17  
7 3     3   2069 use Moo;
  3         34839  
  3         15  
8 3     3   6304 use Protocol::HTTP2::Client;
  3         121641  
  3         118  
9 3     3   1564 use IO::Select;
  3         5294  
  3         167  
10 3     3   2600 use IO::Socket::SSL qw();
  3         265234  
  3         4003  
11              
12             our $VERSION = "0.07";
13              
14             has [qw/auth_key key_id team_id bundle_id development/] => (
15             is => 'rw',
16             );
17              
18             has [qw/cert_file key_file passwd_cb/] => (
19             is => 'rw',
20             );
21              
22             has [qw/proxy/] => (
23             is => 'rw',
24             default => $ENV{https_proxy},
25             );
26              
27             has [qw/apns_id apns_expiration apns_collapse_id apns_push_type/] => (
28             is => 'rw',
29             );
30              
31             has apns_priority => (
32             is => 'rw',
33             default => 10,
34             );
35              
36 2     2 0 7469 sub algorithm {'ES256'}
37              
38             sub _host {
39 3     3   8 my ($self) = @_;
40 3 100       37 return 'api.' . ($self->development ? 'sandbox.' : '') . 'push.apple.com'
41             }
42              
43 2     2   22 sub _port {443}
44              
45             sub _socket {
46 0     0   0 my ($self) = @_;
47 0 0 0     0 if (!$self->{_socket} || !$self->{_socket}->opened){
48 0         0 my %ssl_opts = (
49             SSL_alpn_protocols => ['h2'],
50             );
51 0         0 for (qw/cert_file key_file passwd_cb/) {
52 0 0       0 $ssl_opts{"SSL_$_"} = $self->{$_} if defined $self->{$_};
53             }
54              
55 0         0 my ($host,$port) = ($self->_host, $self->_port);
56              
57 0         0 my $socket;
58 0 0       0 if ( my $proxy = $self->proxy ) {
59 0 0       0 $proxy =~ s|^http://|| or die "Invalid proxy $proxy - only http proxy is supported!\n";
60 0         0 require Net::HTTP;
61 0   0     0 $socket = Net::HTTP->new(PeerAddr => $proxy) || die $@;
62 0         0 $socket->write_request(
63             CONNECT => "$host:$port",
64             Host => "$host:$port",
65             Connection => "Keep-Alive",
66             'Proxy-Connection' => "Keep-Alive",
67             );
68 0         0 my ($code, $mess, %h) = $socket->read_response_headers;
69 0 0       0 $code eq '200' or die "Proxy error: $code $mess";
70              
71 0 0 0     0 IO::Socket::SSL->start_SSL(
72             $socket,
73             # explicitly set hostname we should use for SNI
74             SSL_hostname => $host,
75             %ssl_opts,
76             ) or die $! || $IO::Socket::SSL::SSL_ERROR;
77             }
78             else {
79             # TLS transport socket
80 0 0 0     0 $socket = IO::Socket::SSL->new(
81             PeerHost => $host,
82             PeerPort => $port,
83             %ssl_opts,
84             ) or die $! || $IO::Socket::SSL::SSL_ERROR;
85             }
86 0         0 $self->{_socket} = $socket;
87              
88             # non blocking
89 0         0 $self->{_socket}->blocking(0);
90             }
91 0         0 return $self->{_socket};
92             }
93              
94             sub _client {
95 1     1   4 my ($self) = @_;
96 1   33     16 $self->{_client} ||= Protocol::HTTP2::Client->new(keepalive => 1);
97 1         113 return $self->{_client};
98             }
99              
100             sub prepare {
101 1     1 1 2636 my ($self, $device_token, $payload, $cb) = @_;
102 1         10 my @headers = (
103             'apns-topic' => $self->bundle_id,
104             );
105              
106 1         4 for (qw/apns_id apns_priority apns_expiration apns_collapse_id apns_push_type/) {
107 5         21 my $v = $self->$_;
108 5 100       13 next unless defined $v;
109 2         4 my $k = $_;
110 2         7 $k =~ s/_/-/g;
111 2         6 push @headers, $k => $v;
112             }
113              
114 1 50 33     13 if ($self->team_id and $self->auth_key and $self->key_id) {
      33        
115 1         670 require Crypt::PK::ECC;
116             # require for treat pkcs#8 private key
117 1         16651 Crypt::PK::ECC->VERSION(0.059);
118 1         650 require Crypt::JWT;
119 1         29736 my $claims = {
120             iss => $self->team_id,
121             iat => time,
122             };
123 1         10 my $jwt = Crypt::JWT::encode_jwt(
124             payload => $claims,
125             key => [$self->auth_key],
126             alg => $self->algorithm,
127             extra_headers => {
128             kid => $self->key_id,
129             },
130             );
131 1         9414 push @headers, authorization => sprintf('bearer %s', $jwt);
132             }
133 1         5 my $path = sprintf '/3/device/%s', $device_token;
134 1         3 push @{$self->{_request}}, {
  1         8  
135             ':scheme' => 'https',
136             ':authority' => join(":", $self->_host, $self->_port),
137             ':path' => $path,
138             ':method' => 'POST',
139             headers => \@headers,
140             data => JSON::encode_json($payload),
141             on_done => $cb,
142             };
143 1         10 return $self;
144             }
145              
146             sub _make_client_request_single {
147 1     1   3 my ($self) = @_;
148 1 50       2 if (my $req = shift @{$self->{_request}}){
  1         5  
149 1         3 my $done_cb = delete $req->{on_done};
150             $self->_client->request(
151             %$req,
152             on_done => sub {
153 0 0   0     ref $done_cb eq 'CODE'
154             and $done_cb->(@_);
155 0           $self->_make_client_request_single();
156             },
157 1         4 );
158             }
159             else {
160 0           $self->_client->close;
161             }
162             }
163              
164             sub notify {
165 0     0 1   my ($self) = @_;
166             # request one by one as APNS server returns SETTINGS_MAX_CONCURRENT_STREAMS = 1
167 0           $self->_make_client_request_single();
168 0           my $io = IO::Select->new($self->_socket);
169             # send/recv frames until request is done
170 0           while ( !$self->_client->shutdown ) {
171 0           $io->can_write;
172 0           while ( my $frame = $self->_client->next_frame ) {
173 0           syswrite $self->_socket, $frame;
174             }
175 0           $io->can_read;
176 0           while ( sysread $self->_socket, my $data, 4096 ) {
177 0           $self->_client->feed($data);
178             }
179             }
180 0           undef $self->{_client};
181 0           $self->_socket->close(SSL_ctx_free => 1);
182             }
183              
184             1;
185             __END__