• Stars
    star
    369
  • Rank 115,675 (Top 3 %)
  • Language
    Erlang
  • Created about 14 years ago
  • Updated 3 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Apple Push Notification Server for Erlang

Apns4erl v2 Build Statuscodecov

This lib is intended to allow you to write an APNs provider for Apple Push Notificaion services (APNs) over HTTP2 in Erlang.

Copyright (c) 2017 Erlang Solutions Ltd. [email protected], released under the Apache 2 license

You can find the v1 here?

Note: Apns4erl v2 is still under development. Currently it supports push notifications with certificate and authentication token.

Contact Us

If you find any bugs or have a problem while using Apns4erl, please open an issue in this repo (or a pull request :)).

Requirements

  • You must have installed an updated Openssl version or, at least, be sure it supports TLS 1.2+. New APNs server only supports connections over TLS 1.2+.
  • Erlang R19+

Important Links

How to use it?

First we have to fill our config data. There are two ways for do this, one is filling a config file. This is an example you can find at test/test.config:

{
  apns,
  [ {apple_host,       "api.development.push.apple.com"}
  , {apple_port,       443}
  , {certfile,         "priv/apns-dev-cert.pem"}
  , {keyfile,          "priv/apns-dev-key-noenc.pem"}
  , {token_keyfile,    "priv/APNsAuthKey_KEYID12345.p8"}
  , {timeout,          10000}

  %% APNs Headers

  , {apns_id,          undefined}
  , {apns_expiration,  0}
  , {apns_priority,    10}
  , {apns_topic,       "com.example.myapp"}
  , {apns_collapse_id, undefined}
  , {apns_push_type,   "alert"}

  %% Feedback
  , {feedback_host,    "feedback.push.apple.com"}
  , {feedback_port,    2195}
  ]
  ]
}

The other way is send all that info as a parameter to apns:connect/1 function encapsulated in a apns_connection:connection() structure:

#{ name       := name()
 , apple_host := host()
 , apple_port := inet:port_number()
 , certfile   => path()
 , keyfile    => path()
 , timeout    => integer()
 , type       := type()
 }.

APNs allows two connection types, one is using Provider Certificates. The first certificate option is to supply cert paths in certfile and keyfile. Alternatively, you can supply a cert binary in certdata and a keydata()-type tuple (see: https://github.com/inaka/apns4erl/blob/master/src/apns_connection.erl#L64) in keydata. Certs are the Provider Certificates and the keys are the Private Key both provided by Apple. We need them in .pem format, here is an example of how to convert them, check the certificates section.

The other way to connect against APNs is using Provider Authentication Tokens, for this choice you must fill the field token_keyfile. This is a path to the Authentication Key provided by Apple. This is in .p8 format and it doesn't need conversion.

This key will be needed in order to generate a token which will be used every time we try to push a notification. In connection's time it is not needed.

Run

apns4erl can be included as a dependency and started from yourapp.app.src. You also can run it on the shell for testing.

> rebar3 compile
> erl -pa _build/default/lib/*/ebin -config test/test.config

Don't forget your config file if you want to use apns:connect/2.

1> apns:start().
ok

Create connections

After running apns4erl app we can start creating connections. As we mentioned before there are two types of connections. Both are created using the functions apns:connect/1 and apns:connect/2.

  • apns:connect/1: This function accepts as a parameter an apns_connection:connection() structure.

    #{ name       := name()
     , apple_host := host()
     , apple_port := inet:port_number()
     , certdata   => binary()
     , certfile   => path()
     , keydata    => keydata()
     , keyfile    => path()
     , timeout    => integer()
     , type       := type()
     }.

    where the type field indicates if is certdata, cert, or token.

  • apns:connect/2: The first argument is the type and the second one is the connection's name. In order to use it successfully we have to fill the config file before, as explained in how to use it? section.

Example:

1> apns:connect(cert, my_first_connection).
{ok,<0.87.0>}
2> apns:connect(#{name => another_cert, apple_host => "api.push.apple.com", apple_port => 443,
certfile => "priv/cert.pem", keyfile => "priv/key.pem", type => cert}).
3> apns:connect(token, my_second_connection).
{ok,<0.95.0>}

Note cert and token define the type we want.

apns:connect/2 returns the connection pid.

Create Connections without name

In some scenarios we don't want to assign names to the connections instead we want working just with the pid (working with a pool of connections for example). If that is the case we use the same apns:connect/1 and apns:connect/2 functions but instead of a connection name we put undefined:

1> apns:connect(cert, undefined).
{ok,<0.127.0>}
2> apns:connect(#{name => undefined, apple_host => "api.push.apple.com", apple_port => 443,
certfile => "priv/cert2.pem", keyfile => "priv/key2-noenc.pem", type => cert}).
{ok,<0.130.0>}
3> apns:connect(token, my_second_connection).
{ok,<0.132.0>}

Push Notifications over Provider Certificate connections

In order to send Notifications over Provider Certificate connection we will use apns:push_notification/3,4.

We will need the connection, a notification, the device ID and some http2 headers. The connection is the atom we used when we executed apns:connect/2 for setting a name or its pid, the device ID is provided by Apple, the notification is a map with the data we want to send, that map will be encoded to json later and the http2 headers can be explicitly sent as a parameter using apns:push_notification/4 or can be defined at the config file, in that case we would use apns:push_notification/3.

This is the headers format:

-type headers()   :: #{ apns_id          => binary()
                      , apns_expiration  => binary()
                      , apns_priority    => binary()
                      , apns_topic       => binary()
                      , apns_collapse_id => binary()
                      , apns_push_type   => binary()
                      , apns_auth_token  => binary()
                      }.

All of them are defined by Apple here

Lets send a Notification.

1> {ok, Pid} = apns:connect(cert, my_first_connection).
{ok,<0.85.0>}
2> DeviceId = <<"a0dc63fb059cb9c13b03e5c974af3dd33d67fed4147da8c5ada0626439e18935">>.
<<"a0dc63fb059cb9c13b03e5c974af3dd33d67fed4147da8c5ada0626439e18935">>
3> Notification = #{aps => #{alert => <<"you have a message">>}}.
#{aps => #{alert => <<"you have a message">>}}
4> apns:push_notification(my_first_connection, DeviceId, Notification).
{200,
 [{<<"apns-id">>,<<"EFDE0D9D-F60C-30F4-3FF1-86F3B90BE434">>}],
 no_body}
5> apns:push_notification(Pid, DeviceId, Notification).
{200,
 [{<<"apns-id">>,<<"EFDE0D9D-F60C-30F4-3FF1-86F3B90BE654">>}],
 no_body}

The result is the response itself, its format is:

-type response()  :: { integer()          % HTTP2 Code
                     , [term()]           % Response Headers
                     , [term()] | no_body % Response Body
                     } | timeout.

And that's all.

Push Notifications over Provider Authentication Tokens connections

This is the other way APNs allows us to send notifications. In this case we don't need a certificate but we will need a p8 file with the private key we will use to sign the token. Lets assume we've got the file APNsAuthKey_KEYID12345.p8 from Apple. We then have to fill the config file key token_keyfile with the path to that file.

We will need a kid value, this is the key identifier. In our case is the last 10 chars of the file name (KEYID123456). We will need also the iss value, this is the Team Id, that can be checked on your Apple's Developer account, in our case it will be THEATEAM. And that's it.

You can find more info here

In order to push a notification we will use apns:push_notification_token/4,5. We will need the same attributes we used sending a notification over Provider Certificate connections plus a signed token. This token has a 1 hour life, so that means we can generate one token and use it many times till it expires. Lets try.

Create the token:

6> TeamId = <<"THEATEAM">>.
<<"THEATEAM">>
7> KeyID = <<"KEYID123456">>.
<<"KEYID123456">>
8> Token = apns:generate_token(TeamId, KeyID).
<<"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IktFWUlEMTIzNDU2In0.eyJpc3MiOiJUSEVBVEVBTSIsImlhdCI6MTQ4NjE0OTMzNH0.MEQC"...>>

Now push the notification:

12> DeviceId = <<"a0dc63fb059cb9c13b03e5c974af3dd33d67fed4147da8c5ada0626439e18935">>.
<<"a0dc63fb059cb9c13b03e5c974af3dd33d67fed4147da8c5ada0626439e18935">>
13> Notification = #{aps => #{alert => <<"you have a message">>}}.
#{aps => #{alert => <<"you have a message">>}}
14> apns:push_notification_token(my_second_connection, Token, DeviceId, Notification).
{200,
 [{<<"apns-id">>,<<"EBC03BF9-A784-FDED-34F7-5A8D859DA977">>}],
 no_body}

We can use this token for an entire hour, after that we will receive something like this:

16> apns:push_notification_token(my_second_connection, Token, DeviceId, Notification).
{403,
 [{<<"apns-id">>,<<"03FF9497-8A6B-FFD6-B32B-160ACEDE35F0">>}],
 [{<<"reason">>,<<"ExpiredProviderToken">>}]}

Pushing notifications

NOTE in order to push notifications, in both ways, we must call apns:push_notification/3,4 and apns:push_notification_token/4,5 from the same process which created the connection. If we try to do it from a different one we will get an error {error, not_connection_owner}.

Reconnection

If network goes down or something unexpected happens the gun connection with APNs will go down. In that case apns4erl will send a message {reconnecting, ServerPid} to the client process, that means apns4erl lost the connection and it is trying to reconnect. Once the connection has been recover a {connection_up, ServerPid} message will be send.

We implemented an Exponential Backoff strategy. We can set the ceiling time adding the backoff_ceiling variable on the config file. By default it is set to 10 (seconds).

Close connections

Apple recommends us to keep our connections open and avoid opening and closing very often. You can check the Best Practices for Managing Connections section.

But when closing a connection makes sense apns4erl gives us the function apns:close_connection/1 where the parameter is the connection's name or the connection's pid. After using it the name will be available for new connections again (if it was different than undefined).

Feedback

apns4erl also allows us to get feedback from APNs service. It does it thru the binary API.

In order to get feedback we would need a Provider Certificate. apns4erl provides us two functions, apns:get_feedback/0 and apns:get_feedback/1 which require some Feedback's information like url, port, timeout... We can set that info in our config file and use apns:get_feedback/0. We can also send all that configuration as a parameter to apns:get_feedback/1 where the config structure must looks like this:

#{ host     := string()
 , port     := pos_integer()
 , certfile := string()
 , keyfile  => string()
 , timeout  := pos_integer()
 }.

The response for both functions will be a list of feedback()

-type feedback() :: {calendar:datetime(), string()}.

Where the first element in the tuple is the date when the device uninstalled the app and the second element is the Device Id.

Changelog Generation

If you want to generate a new release of this project, you'll need to update the CHANGELOG.md file. We generally do it using github_changelog_generator. This project needs a special option passed to it, tho: --exclude-tags-regex '1*'. Otherwise, it will fail since version 2 releases started from a clean HEAD and therefore have nothing in common with the ones for version 1.

More Repositories

1

erlang_guidelines

Inaka's Erlang Coding Guidelines
Erlang
619
star
2

EventSource

A simple Swift client library for the Server Sent Events (SSE)
Swift
483
star
3

galgo

When you want your logs to be displayed on screen
Java
427
star
4

elvis

Erlang Style Reviewer
Erlang
424
star
5

TinyTask

A Tiny Task Library
Java
324
star
6

worker_pool

Erlang worker pool
Erlang
274
star
7

sumo_db

Erlang Persistency Framework
Erlang
173
star
8

shotgun

For the times you need more than just a gun.
Erlang
166
star
9

Dayron

A repository `similar` to Ecto.Repo that maps to an underlying http client, sending requests to an external rest api instead of a database
Elixir
159
star
10

cowboy_swagger

Swagger integration for Cowboy (built on trails)
Erlang
120
star
11

gold_fever

A Treasure Hunt for Erlangers
Erlang
86
star
12

Jayme

Abstraction layer that eases RESTful interconnections in Swift
Swift
81
star
13

guidelines

General Inaka Guidelines
75
star
14

cowboy-trails

A couple of improvements over Cowboy Routes
Erlang
71
star
15

jem.js

Just Erlang Maps for Javascript
JavaScript
69
star
16

sheldon

Very Simple Erlang Spell Checker
Erlang
62
star
17

elvis_core

The core of an Erlang linter
Erlang
61
star
18

niffy

Inline C code in Erlang modules to build NIFs
Erlang
60
star
19

sumo_rest

Generic cowboy handlers to work with Sumo
Erlang
59
star
20

serpents

Multi-Player Game on top of HDP protocol
Erlang
56
star
21

xref_runner

Erlang Xref Runner (inspired in rebar xref)
Erlang
50
star
22

erlang-github

Github API client
Erlang
46
star
23

lasse

SSE handler for Cowboy
Erlang
45
star
24

beam_olympics

Let's find the fastest beamer!
Erlang
39
star
25

fiar

Four in a Row - A game to learn Erlang
Erlang
36
star
26

zipper

Generic Zipper implementation in Erlang
Erlang
34
star
27

ios-xmpp-sample

Blog post sample project.
Swift
33
star
28

kotlillon

Android Kotlin Examples
Kotlin
33
star
29

katana-test

Meta Testing Utilities for common_test
Erlang
32
star
30

match_stream

A sample project to show in our scale blog post
JavaScript
30
star
31

phoenix_passwordless_login

Phoenix Passwordless Login
Elixir
29
star
32

KillerTask

Android AsyncTask wrapper library, written in Kotlin
Kotlin
26
star
33

canillita

Simple Paperboy-themed PubSub
Erlang
26
star
34

lewis

Rock your Android
Java
22
star
35

tirerl

Erlang interface to Elastic Search
Erlang
19
star
36

itweet

Twitter Stream API on ibrowse
Erlang
18
star
37

katana-code

Code Utilities for Erlang
Erlang
17
star
38

pusherman

queuing system for push notifications
Erlang
17
star
39

galgo-ios

When you want your logs to be displayed on screen
Objective-C
16
star
40

lsl

NIM in Erlang
Erlang
15
star
41

credo_server

Credo Server
Elixir
15
star
42

FadeButton

Fading effects for UIButtons made simple
Swift
15
star
43

Jolly

Jolly Chimp that keeps track of our Github Repos
Swift
12
star
44

rpsls

Rock Paper Scissors Lizzard Spock World Championship in Erlang
Erlang
12
star
45

ikbot

An elixir based customizable hipchat bot
Elixir
12
star
46

nconf

Nested Configuration Manager for Erlang Applications
Erlang
12
star
47

pushito

APNS over HTTP/2
Elixir
11
star
48

rest_guidelines

REST API Design Guidelines
11
star
49

fetjaba

From Erlang To Java and Back Again
Erlang
9
star
50

sumo_db_pgsql

PostgreSQL adapter for sumo_db.
Erlang
9
star
51

jinterface_stdlib

Erlang stdlib implementation on Java, based on JInterface
Java
9
star
52

IKCapture

Snapchat-Like Image Capture Library
Objective-C
8
star
53

MediaPickerController

Neat API for presenting the classical action sheet for picking an image or video from the device or camera.
Swift
8
star
54

PictureViewMaster

Interactive image projector.
Swift
8
star
55

toy_kv

A simple and reduced Key-Value Store written in Erlang
Erlang
7
star
56

ColorPicker

Color Picker for Swift
Swift
7
star
57

swift_guidelines

Inaka's Swift Coding Guidelines
7
star
58

spellingci

Spelling CI Server
Erlang
7
star
59

talks

Sources and pdfs of our talks and speeches
TeX
6
star
60

bookmarks

A collection of bookmarks for Inakos
6
star
61

IKJayma

RESTful API abstraction for Server Interconnection
Objective-C
6
star
62

hexer

Hex.pm integration in escript format.
Erlang
6
star
63

hexer.mk

erlang.mk plugin for hexer
Makefile
5
star
64

android_guidelines

Inaka's Android Development Guidelines
5
star
65

elvis.mk

3rd party erlang.mk plug-in for Elvis
Shell
5
star
66

plixir

Poker + Elixir + Phoenix
CSS
5
star
67

ios_guidelines

Inaka's iOS Coding Guidelines
Objective-C
4
star
68

sumo_db_elasticsearch

ElasticSearch adapter for sumo_db
Erlang
4
star
69

tele_sign

Node.js library to send messages through http://www.telesign.com/
JavaScript
4
star
70

inaka.github.io

Inaka's Open Source Projects
HTML
3
star
71

sumo_db_riak

Riak adapter for sumo_db
Erlang
3
star
72

android-excercises

Quick test for Android candidates
3
star
73

sumo_db_mongo

MongoDB adapter for sumo_db
Erlang
2
star
74

Otec

A swift app to showcase our best open-source libraries
Swift
2
star
75

gold_fever-solver

A solver for the http://github.com/inaka/gold_fever game
Erlang
2
star
76

inaka.mk

erlang.mk extras that we generally use in all of our projects
Makefile
2
star
77

g2x

Graffle to XCode
Objective-C
2
star
78

beam_olympics-extended

Internal repo to keep secret beam_olympics tasks from the public view
Erlang
2
star
79

beam_olympics-solver

Solutions for beam_olympics
Elixir
2
star
80

emarkdown

Based on https://github.com/devinus/markdown - but for Erlang :)
C
2
star
81

sumo_db_mysql

MySQL adapter for sumo_db
Erlang
2
star
82

ruby_guidelines

Our own guidelines when it comes to ruby development
1
star
83

pokedex

Dumb repo to prove what we can do with sumo{_db|rest_}
Erlang
1
star
84

homebrew-formulas

Homebrew formulas for some of our tools
Ruby
1
star
85

updike

Run, rabbit, run
1
star
86

ios-scripts

Helper scripts that you can use in your iOS apps
Shell
1
star
87

INSocketListener

SSE Socket Listener for Objective-C
Objective-C
1
star