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   114625 use 5.008001;
  3         27  
3 3     3   14 use strict;
  3         3  
  3         50  
4 3     3   11 use warnings;
  3         4  
  3         79  
5 3     3   14 use Carp ();
  3         3  
  3         59  
6 3     3   1767 use JSON;
  3         28196  
  3         13  
7 3     3   1798 use Moo;
  3         28389  
  3         12  
8 3     3   5022 use Protocol::HTTP2::Client;
  3         99087  
  3         107  
9 3     3   1339 use IO::Select;
  3         4153  
  3         147  
10 3     3   2134 use IO::Socket::SSL qw();
  3         216180  
  3         2943  
11              
12             our $VERSION = "0.06";
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 5979 sub algorithm {'ES256'}
37              
38             sub _host {
39 3     3   9 my ($self) = @_;
40 3 100       32 return 'api.' . ($self->development ? 'sandbox.' : '') . 'push.apple.com'
41             }
42              
43 2     2   19 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             # openssl 1.0.1 support only NPN
50             SSL_npn_protocols => ['h2'],
51             # openssl 1.0.2 also have ALPN
52             SSL_alpn_protocols => ['h2'],
53             SSL_version => 'TLSv1_2',
54             );
55 0         0 for (qw/cert_file key_file passwd_cb/) {
56 0 0       0 $ssl_opts{"SSL_$_"} = $self->{$_} if defined $self->{$_};
57             }
58              
59 0         0 my ($host,$port) = ($self->_host, $self->_port);
60              
61 0         0 my $socket;
62 0 0       0 if ( my $proxy = $self->proxy ) {
63 0 0       0 $proxy =~ s|^http://|| or die "Invalid proxy $proxy - only http proxy is supported!\n";
64 0         0 require Net::HTTP;
65 0   0     0 $socket = Net::HTTP->new(PeerAddr => $proxy) || die $@;
66 0         0 $socket->write_request(
67             CONNECT => "$host:$port",
68             Host => "$host:$port",
69             Connection => "Keep-Alive",
70             'Proxy-Connection' => "Keep-Alive",
71             );
72 0         0 my ($code, $mess, %h) = $socket->read_response_headers;
73 0 0       0 $code eq '200' or die "Proxy error: $code $mess";
74              
75 0 0 0     0 IO::Socket::SSL->start_SSL(
76             $socket,
77             # explicitly set hostname we should use for SNI
78             SSL_hostname => $host,
79             %ssl_opts,
80             ) or die $! || $IO::Socket::SSL::SSL_ERROR;
81             }
82             else {
83             # TLS transport socket
84 0 0 0     0 $socket = IO::Socket::SSL->new(
85             PeerHost => $host,
86             PeerPort => $port,
87             %ssl_opts,
88             ) or die $! || $IO::Socket::SSL::SSL_ERROR;
89             }
90 0         0 $self->{_socket} = $socket;
91              
92             # non blocking
93 0         0 $self->{_socket}->blocking(0);
94             }
95 0         0 return $self->{_socket};
96             }
97              
98             sub _client {
99 1     1   2 my ($self) = @_;
100 1   33     15 $self->{_client} ||= Protocol::HTTP2::Client->new(keepalive => 1);
101 1         98 return $self->{_client};
102             }
103              
104             sub prepare {
105 1     1 1 2344 my ($self, $device_token, $payload, $cb) = @_;
106 1         9 my @headers = (
107             'apns-topic' => $self->bundle_id,
108             );
109              
110 1         3 for (qw/apns_id apns_priority apns_expiration apns_collapse_id apns_push_type/) {
111 5         20 my $v = $self->$_;
112 5 100       11 next unless defined $v;
113 2         3 my $k = $_;
114 2         6 $k =~ s/_/-/g;
115 2         5 push @headers, $k => $v;
116             }
117              
118 1 50 33     12 if ($self->team_id and $self->auth_key and $self->key_id) {
      33        
119 1         569 require Crypt::PK::ECC;
120             # require for treat pkcs#8 private key
121 1         15844 Crypt::PK::ECC->VERSION(0.059);
122 1         570 require Crypt::JWT;
123 1         24374 my $claims = {
124             iss => $self->team_id,
125             iat => time,
126             };
127 1         13 my $jwt = Crypt::JWT::encode_jwt(
128             payload => $claims,
129             key => [$self->auth_key],
130             alg => $self->algorithm,
131             extra_headers => {
132             kid => $self->key_id,
133             },
134             );
135 1         7775 push @headers, authorization => sprintf('bearer %s', $jwt);
136             }
137 1         5 my $path = sprintf '/3/device/%s', $device_token;
138 1         2 push @{$self->{_request}}, {
  1         8  
139             ':scheme' => 'https',
140             ':authority' => join(":", $self->_host, $self->_port),
141             ':path' => $path,
142             ':method' => 'POST',
143             headers => \@headers,
144             data => JSON::encode_json($payload),
145             on_done => $cb,
146             };
147 1         9 return $self;
148             }
149              
150             sub _make_client_request_single {
151 1     1   3 my ($self) = @_;
152 1 50       2 if (my $req = shift @{$self->{_request}}){
  1         4  
153 1         3 my $done_cb = delete $req->{on_done};
154             $self->_client->request(
155             %$req,
156             on_done => sub {
157 0 0   0     ref $done_cb eq 'CODE'
158             and $done_cb->(@_);
159 0           $self->_make_client_request_single();
160             },
161 1         4 );
162             }
163             else {
164 0           $self->_client->close;
165             }
166             }
167              
168             sub notify {
169 0     0 1   my ($self) = @_;
170             # request one by one as APNS server returns SETTINGS_MAX_CONCURRENT_STREAMS = 1
171 0           $self->_make_client_request_single();
172 0           my $io = IO::Select->new($self->_socket);
173             # send/recv frames until request is done
174 0           while ( !$self->_client->shutdown ) {
175 0           $io->can_write;
176 0           while ( my $frame = $self->_client->next_frame ) {
177 0           syswrite $self->_socket, $frame;
178             }
179 0           $io->can_read;
180 0           while ( sysread $self->_socket, my $data, 4096 ) {
181 0           $self->_client->feed($data);
182             }
183             }
184 0           undef $self->{_client};
185 0           $self->_socket->close(SSL_ctx_free => 1);
186             }
187              
188             1;
189             __END__