File Coverage

blib/lib/AnyEvent/APNS.pm
Criterion Covered Total %
statement 102 119 85.7
branch 22 34 64.7
condition 2 6 33.3
subroutine 16 19 84.2
pod 2 2 100.0
total 144 180 80.0


line stmt bran cond sub pod time code
1             package AnyEvent::APNS;
2 5     5   49825 use utf8;
  5         38  
  5         29  
3 5     5   5192 use Any::Moose;
  5         199947  
  5         37  
4              
5 5     5   10670 use AnyEvent 4.80;
  5         26637  
  5         173  
6 5     5   5086 use AnyEvent::Handle;
  5         67693  
  5         184  
7 5     5   5072 use AnyEvent::Socket;
  5         40530  
  5         744  
8 5     5   5796 use AnyEvent::TLS;
  5         103776  
  5         292  
9              
10             require bytes;
11 5     5   73 use Carp qw(croak);
  5         10  
  5         401  
12 5     5   6061 use Encode;
  5         64996  
  5         547  
13 5     5   73 use Scalar::Util 'looks_like_number';
  5         12  
  5         471  
14 5     5   6733 use JSON::Any;
  5         99886  
  5         39  
15              
16             our $VERSION = '0.10';
17              
18             has certificate => (
19             is => 'rw',
20             isa => 'Str | ScalarRef',
21             required => 1,
22             );
23              
24             has private_key => (
25             is => 'rw',
26             isa => 'Str | ScalarRef',
27             required => 1,
28             );
29              
30             has sandbox => (
31             is => 'rw',
32             isa => 'Bool',
33             default => 0,
34             );
35              
36             has handler => (
37             is => 'rw',
38             isa => 'AnyEvent::Handle',
39             predicate => 'connected',
40             clearer => 'clear_handler',
41             );
42              
43             has json_driver => (
44             is => 'rw',
45             isa => 'Object',
46             lazy => 1,
47             default => sub {
48             JSON::Any->new( utf8 => 1 );
49             },
50             );
51              
52             has on_error => (
53             is => 'rw',
54             isa => 'CodeRef',
55             default => sub { sub { warn @_ } },
56             );
57              
58             has on_eof => (
59             is => 'rw',
60             isa => 'CodeRef',
61             );
62              
63             has on_connect => (
64             is => 'rw',
65             isa => 'CodeRef',
66             default => sub { sub {} },
67             );
68              
69             has on_error_response => (
70             is => 'rw',
71             isa => 'CodeRef',
72             );
73              
74             has debug_port => (
75             is => 'rw',
76             isa => 'Int',
77             predicate => 'is_debug',
78             );
79              
80             has _con_guard => (
81             is => 'rw',
82             isa => 'Object',
83             );
84              
85             has last_identifier => (
86             is => 'rw',
87             isa => 'Int',
88             default => sub { 0; }
89             );
90              
91 5     5   39292 no Any::Moose;
  5         13  
  5         50  
92              
93             sub send {
94 3     3 1 26 my $self = shift;
95 3         8 my ($token, $payload, $expiry) = @_;
96              
97 3         26 my $json = encode_utf8( $self->json_driver->encode($payload) );
98              
99             # http://developer.apple.com/library/ios/#DOCUMENTATION/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html
100             # Expiry—A fixed UNIX epoch date expressed in seconds (UTC) that identifies when the notification is no longer valid and can be discarded. The expiry value should be in network order (big endian). If the expiry value is positive, APNs tries to deliver the notification at least once. You can specify zero or a value less than zero to request that APNs not store the notification at all.
101             # default to 24 hours
102 3 50       665 $expiry = defined $expiry ? $expiry : time() + 3600 * 24;
103              
104             # Identifier—An arbitrary value that identifies this notification. This same identifier is returned in a error-response packet if APNs cannot interpret a notification.
105 3         14 my $next_identifier = $self->_increment_identifier;
106              
107 3         11 my $h = $self->handler;
108              
109 3         19 $h->push_write( pack('C', 1) ); # command
110 3         284 $h->push_write( pack('N', $next_identifier) );
111 3         175 $h->push_write( pack('N', $expiry) );
112 3         177 $h->push_write( pack('n', bytes::length($token)) ); # token length
113 3         2453 $h->push_write( $token ); # device token
114              
115             # Apple Push Notification Service refuses string values as badge number
116 3 50 33     176 if ($payload->{aps}{badge} && looks_like_number($payload->{aps}{badge})) {
117 0         0 $payload->{aps}{badge} += 0;
118             }
119              
120             # The maximum size allowed for a notification payload is 256 bytes;
121             # Apple Push Notification Service refuses any notification that exceeds this limit.
122 3 100       14 if ( (my $exceeded = bytes::length($json) - 256) > 0 ) {
123 2 100       20 if (ref $payload->{aps}{alert} eq 'HASH') {
124 1         182 $payload->{aps}{alert}{body} =
125             $self->_trim_utf8($payload->{aps}{alert}{body}, $exceeded);
126             }
127             else {
128 1         6 $payload->{aps}{alert} = $self->_trim_utf8($payload->{aps}{alert}, $exceeded);
129             }
130              
131 2         16 $json = encode_utf8( $self->json_driver->encode($payload) );
132             }
133              
134 3         87 $h->push_write( pack('n', bytes::length($json)) ); # payload length
135 3         207 $h->push_write( $json ); # payload
136              
137 3         191 return $next_identifier;
138             }
139              
140             sub _trim_utf8 {
141 2     2   6 my ($self, $string, $trim_length) = @_;
142              
143 2         8 my $string_bytes = encode_utf8($string);
144 2         13 my $trimmed = '';
145              
146 2         6 my $start_length = bytes::length($string_bytes) - $trim_length;
147 2 50       12 return $trimmed if $start_length <= 0;
148              
149 2         8 for my $len ( reverse $start_length - 6 .. $start_length ) {
150 6         7 local $@;
151 6         8 eval {
152 6         34 $trimmed = decode_utf8(substr($string_bytes, 0, $len), Encode::FB_CROAK);
153             };
154 6 100       79 last if $trimmed;
155             }
156              
157 2         10 return $trimmed;
158             }
159              
160             sub connect {
161 5     5 1 12547 my $self = shift;
162              
163 5 50 33     40 if ($self->connected && $self->handler) {
164 0         0 warn 'Already connected!';
165 0         0 return;
166             }
167              
168 5 50       31 my $host = $self->sandbox
169             ? 'gateway.sandbox.push.apple.com'
170             : 'gateway.push.apple.com';
171 5         10 my $port = 2195;
172              
173 5 50       26 if ($self->is_debug) {
174 5         12 $host = '127.0.0.1';
175 5         18 $port = $self->debug_port;
176             }
177             my $g = tcp_connect $host, $port, sub {
178 5 50   5   478 my ($fh) = @_
179             or return $self->on_error->(undef, 1, $!);
180              
181 5         11 my $tls_setting = {};
182 5 100       32 if (ref $self->certificate) {
183 2         3 $tls_setting->{cert} = ${ $self->certificate };
  2         9  
184             }
185             else {
186 3         17 $tls_setting->{cert_file} = $self->certificate;
187             }
188              
189 5 100       28 if (ref $self->private_key) {
190 2         3 $tls_setting->{key} = ${ $self->private_key };
  2         8  
191             }
192             else {
193 3         15 $tls_setting->{key_file} = $self->private_key;
194             }
195              
196             my $handle = AnyEvent::Handle->new(
197             fh => $fh,
198             on_error => sub {
199 1         180 $self->on_error->(@_);
200 1         15 $self->clear_handler;
201 1         6 $_[0]->destroy;
202             },
203 5 50       77 !$self->is_debug ? (
204             tls => 'connect',
205             tls_ctx => $tls_setting,
206             ) : (),
207             );
208 5         481 $self->handler( $handle );
209              
210 5 100       26 if ($self->on_eof) {
211             $handle->on_eof(sub {
212 1         140 $self->on_eof->(@_);
213 1         28 $self->clear_handler;
214 1         5 $_[0]->destroy;
215 1         7 });
216             }
217              
218 5 50       27 if ( $self->on_error_response ) {
219             $handle->on_read(
220             sub {
221 0         0 $self->_on_read_with_error_callback( @_ );
222             }
223 0         0 );
224             }
225             else {
226 5         35 $handle->on_read( sub { delete $_[0]->{rbuf} } );
  0         0  
227             }
228              
229 5         234 $self->on_connect->();
230 5         53 };
231              
232 5         2322 Scalar::Util::weaken($self);
233 5         52 $self->_con_guard($g);
234              
235 5         14 $self;
236             }
237              
238             sub _on_read_with_error_callback {
239 0     0   0 my ($self, $handle) = @_;
240             $handle->push_read( chunk => 1,
241             sub {
242 0     0   0 my $command = unpack( 'C', $_[1] );
243 0 0       0 if ( $command != 8 ) {
244             # something is wrong
245             # auto reconnect
246 0         0 $self->clear_handler;
247 0         0 $self->connect;
248             }
249 0         0 });
250             $handle->push_read( chunk => 5,
251             sub {
252 0     0   0 my $status = unpack( 'C', substr( $_[1], 0, 1) );
253 0         0 my $identifier = unpack( 'N', substr( $_[1], 1, 4) );
254 0         0 $self->on_error_response->( $identifier => $status );
255 0         0 });
256             }
257              
258             # 0 ... 2**32-1, 0 ... 2**32-1, 0 ...
259             sub _increment_identifier {
260 3     3   8 my ($self) = @_;
261 3         13 my $next_identifier = $self->last_identifier + 1;
262 3 50       13 if ( $next_identifier >= 2 ** 32 ) {
263             # identifier is only 4 bytes
264 0         0 $next_identifier = 0;
265             }
266 3         15 $self->last_identifier( $next_identifier );
267             }
268              
269             __PACKAGE__->meta->make_immutable;
270              
271             __END__