Net-APNS-Simple

 view release on metacpan or  search on metacpan

lib/Net/APNS/Simple.pm  view on Meta::CPAN

package Net::APNS::Simple;
use 5.008001;
use strict;
use warnings;
use Carp ();
use JSON;
use Moo;
use Protocol::HTTP2::Client;
use IO::Select;
use IO::Socket::SSL qw();

our $VERSION = "0.07";

has [qw/auth_key key_id team_id bundle_id development/] => (
    is => 'rw',
);

has [qw/cert_file key_file passwd_cb/] => (
    is => 'rw',
);

has [qw/proxy/] => (
    is => 'rw',
    default => $ENV{https_proxy},
);

has [qw/apns_id apns_expiration apns_collapse_id apns_push_type/] => (
    is => 'rw',
);

has apns_priority => (
    is => 'rw',
    default => 10,
);

sub algorithm {'ES256'}

sub _host {
    my ($self) = @_;
    return 'api.' . ($self->development ? 'sandbox.' : '') . 'push.apple.com'
}

sub _port {443}

sub _socket {
    my ($self) = @_;
    if (!$self->{_socket} || !$self->{_socket}->opened){
        my %ssl_opts = (
             SSL_alpn_protocols => ['h2'],
        );
        for (qw/cert_file key_file passwd_cb/) {
            $ssl_opts{"SSL_$_"} = $self->{$_} if defined $self->{$_};
        }

        my ($host,$port) = ($self->_host, $self->_port);

        my $socket;
        if ( my $proxy = $self->proxy ) {
            $proxy =~ s|^http://|| or die "Invalid proxy $proxy - only http proxy is supported!\n";
            require Net::HTTP;
            $socket = Net::HTTP->new(PeerAddr => $proxy) || die $@;
            $socket->write_request(
                CONNECT => "$host:$port",
                Host => "$host:$port",
                Connection => "Keep-Alive",
                'Proxy-Connection' => "Keep-Alive",
            );
            my ($code, $mess, %h) = $socket->read_response_headers;
            $code eq '200' or die "Proxy error: $code $mess";

            IO::Socket::SSL->start_SSL(
                $socket,
                # explicitly set hostname we should use for SNI
                SSL_hostname => $host,
                %ssl_opts,
            ) or die $! || $IO::Socket::SSL::SSL_ERROR;
        }
        else {
            # TLS transport socket
            $socket = IO::Socket::SSL->new(
                PeerHost => $host,
                PeerPort => $port,
                %ssl_opts,
            ) or die $! || $IO::Socket::SSL::SSL_ERROR;
        }
        $self->{_socket} = $socket;

        # non blocking
        $self->{_socket}->blocking(0);
    }
    return $self->{_socket};
}

sub _client {
    my ($self) = @_;
    $self->{_client} ||= Protocol::HTTP2::Client->new(keepalive => 1);
    return $self->{_client};
}

sub prepare {
    my ($self, $device_token, $payload, $cb) = @_;
    my @headers = (
        'apns-topic' => $self->bundle_id,
    );

    for (qw/apns_id apns_priority apns_expiration apns_collapse_id apns_push_type/) {
        my $v = $self->$_;
        next unless defined $v;
        my $k = $_;
        $k =~ s/_/-/g;
        push @headers, $k => $v;
    }

    if ($self->team_id and $self->auth_key and $self->key_id) {
        require Crypt::PK::ECC;
        # require for treat pkcs#8 private key
        Crypt::PK::ECC->VERSION(0.059);
        require Crypt::JWT;
        my $claims = {
            iss => $self->team_id,
            iat => time,
        };
        my $jwt = Crypt::JWT::encode_jwt(
            payload => $claims,
            key => [$self->auth_key],
            alg => $self->algorithm,
            extra_headers => {
                kid => $self->key_id,
            },
        );
        push @headers, authorization => sprintf('bearer %s', $jwt);
    }
    my $path = sprintf '/3/device/%s', $device_token;
    push @{$self->{_request}}, {
        ':scheme' => 'https',
        ':authority' => join(":", $self->_host, $self->_port),
        ':path' => $path,
        ':method' => 'POST',
        headers => \@headers,
        data => JSON::encode_json($payload),
        on_done => $cb,
    };
    return $self;
}



( run in 1.381 second using v1.01-cache-2.11-cpan-39bf76dae61 )