Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Simple gateway using ZeroMQ to APNS (Apple Push Notification Service) and GCM (Google Cloud Messaging).

branch: master

Fetching latest commit…

Cannot retrieve the latest commit at this time

README.md

I. DESCRIPTION

Push2mob is a daemon acting as a common gateway interface for Apple Push Notification Service (APNS) and Google Cloud Messaging (GCM).

It provides asynchronous notification and feedback services using SQLite as persistent storage, so it can be stopped at any time without losing informations.

It is controlled through one ZeroMQ REQ/REP (a.k.a. "ping-pong") socket for each service with an extremely simple communication protocol.

II. DESIGN RATIONALE

Commands are asynchronous primarily because this is the ways APNS works. GCM is synchronous but we can easily mimic an asynchronous behaviour, while the other way around is not possible. Push2mob will ensure to honor GCM feedback while the client application has not retrieved it.

Besides, asynchronous pushes allow the client application to return immediately without waiting for all notifications to be sent; push2mob will just do it right (there is not many ways to stray anyhow). This is especially relevant for APNS where every single device must be addressed individually.

ZeroMQ protocol has been chosen because it allows very simple network programming. It abstracts away all the socket dance one has to do when writing a networked application. Additionally, this means any language can be used as long as ZeroMQ bindings exist for it (all major scripting and compiled languages are supported and more).

The ZeroMQ REQ/REP semantic enforces an even simpler programming model: the client application sends request and then receives a reply. Any other sequence (e.g. sending two messages in a row) results in an error.

III. PROTOCOL

Both APNS and GCM have only two commands, send and feedback, though their arguments may differ a little, depending on the capabilities each one provides.

Protocol is described using the very simple Wirth syntax notation.

III.1. APNS

For a better grasp of the whole picture, please read the APNS documentation

III.1.1. send command

III.1.1.1. Request

REQUEST = "send" expiry count devicetoken { devicetoken } payload
devicetoken = dt_hex | dt_base64
  • expiry is how long the notification is valid. It may be an absolute value representing is a UNIX epoch date or, if it is prefixed with a + character, the following number is added to the current UNIX time to compute the expiry date. An expiry in the past will make APNS try only once before discarding it. Note that the notification may be enqueued for some time (usually a few seconds) in the daemon before being send to APNS, depending on its load.
  • count is the number of following device tokens. Device tokens may be specified in hexadecimal or Base64.
  • dt_hex is a 32-bytes device token encoded in hexadecimal.
  • dt_base64 is a 32-bytes device token encoded in Base64.
  • payload is what must be sent to the device. The format must be valid JSON and must not exceed 256 bytes (enforced by APNS). No further check is done. This is the only argument that can contain spaces.

III.1.1.2. Reply

RESPONSE = rep_ok | rep_err
rep_ok = "OK" id { id }
rep_err = "ERROR" errormsg
  • id is the notification identifier. It is unique for each device token.
  • errormsg is an error message describing the problem.

If an error happens while trying to send a notification to multiple devices, nothing will be pushed at all.

III.1.1.3. Example

Send a notification to one device, using Base64 encoded device token:

REQ> send 1343727824 1 oplo1dgXSxYT5jGmD/L3XjVSRHCT1EkMLBk+/xp5HAY= {"aps":{"alert":"hello","aid":"example"}}
REP> OK 34

Send a notification to two devices, using hex encoded device tokens:

REQ> send +604800 2 DDE37D652A87B72516D51117B45980852A22CEFAB57ABF84ED11F937ED621007 7B168DB2D8F3EBAE6A0235AFF7CEDAC279ECBA6DDE83099816383E94B3B2020C {"aps":{"alert":"hello","aid":"example"}}
REP> OK 35 36

Send a notification using an invalid device token format:

REQ> send +2419200 1 DEADBEEF {"aps":{"alert":"hello","aid":"example"}}
REP> ERROR Wrong device token length (6 != 32): DEADBEEF
REQ> send 1343730023 1 oplo1dgXSxYT5jGmD/L3XjVSRHCT1EkMLBk-/xp5HAD= {"aps":{"alert":"hello","aid":"example"}}
REP> ERROR Wrong Base64 encoding for device token: oplo1dgXSxYT5jGmD/L3XjVSRHCT1EkMLBk-/xp5HAD=

Send a notification using an invalid payload:

REQ> send 1343727824 1 oplo1dgXSxYT5jmD/L3XjVSRHCT1EkMLBk+/xp5HAY= hello
REP> ERROR Invalid JSON payload: hello

III.1.2. feedback command

III.1.2.1. Request

REQ = "feedback"

Simple, isn't it?

III.1.2.2. Reply

REP = "OK" { feedback }
feedback = timestamp:devicetoken
  • timestamp is a timestamp in the UNIX format provided by APNS, indicating when the APNS determined that the application no longer exists on the device. If the timestamp is 0, this means that the APNS thinks it is an invalid token and you must stop using it and possibly investigate why it is there. Otherwise you have to compare this timestamp to the one recorded in your token database in order to determine if the application on the device has re-registered since then. If it hasn't, you must stop sending push notifications to the device.
  • devicetoken is the affected 32-bytes device token encoded either in hexadecimal or in Base64, depending on your device_token_format parameter in the configuration file.

III.1.2.3. Example

No feedback:

REQ> feedback
REP> OK

Feedback containing one erronenous device token (daemon configured to print device tokens as hex):

REQ> feedback
REP> OK 0:7B168DB2D8F3EBAE6A0235AFF7CEDAC279ECBA6DDE83099816383E94B3B2020C

Feedback containing two device tokens that no longer exists (daemon configured to print device tokens as Base64):

REQ> feedback
REP> OK 1344433059:W2EZnh9/mwvjB/AauQ3mQ/wKgAazGc/FwT+omnvv+pk= 1344498238:qnkz8vXkLFQjCtnmnayGV0zazaqEXd9ZGiSR2TY0M0U=

III.2. GCM

For a better grasp of the whole picture, please read the GCM documentation.

III.2.1. send command

III.2.1.1. Request

REQUEST = "send" collapsekey expiry delayidle count devicetoken { devicetoken } payload
delayidle = "delayidle" | "nodelayidle"
  • collapsekey is an arbitrary string (without whitespace) that is used to collapse a group of messages when the device is offline, so that only the last message gets sent to the client (quoting GCM Request format documentation).
  • expiry is how long the notification is valid. It may be an absolute value representing is a UNIX epoch date or, if it is prefixed with a + character, the following number is added to the current UNIX time to compute the expiry date. An expiry in the past will be discarded by the daemon. An expiry cannot be greater than four weeks in the future (2419200 seconds) (enforced by GCM). Note that the notification may be enqueued for some time (usually a few seconds) in the daemon before being send to APNS, depending on its load.
  • delayidle defines if the message should not be sent immediately if the device is idle ((quoting GCM Request format documentation).
  • count is the number of following device tokens. GCM enforces that requests must not address more than 1000 devices, but the daemon will automatically make multiple requests if needed.
  • devicetoken is the device token ("registration ID" in GCM language).
  • payload is what must be sent to the device. The format must be valid JSON and must not exceed 4096 bytes (enforced by GCM). No further check is done. This is the only argument that can contain spaces.

III.2.1.2. Reply

RESPONSE = rep_ok | rep_err
rep_ok = "OK" id { id }
rep_err = "ERROR" errormsg
  • id is the notification identifier. It is unique for each notification request sent (one request every 1000 device tokens).
  • errormsg is an error message describing the problem.

If an error happens while trying to send a notification to multiple devices, nothing will be pushed at all.

III.2.1.3. Example

Send a notification to two device tokens (registration ID in GCM language):

REQ> send 1343727824 2 APA91bE01klpKUSdNV7VV-8_kixm4MA9Vn10Hua1-1jGe9GZMXcvCSl1fKUUNmNGTJoPa3thUHEKUEjatJh-Qtlc5xbWlFt23wSfT69a5ucmp4jdXw20KZOVEc6rOaPbqL9aqjCDX16xiGCxU3G2qpBcxvtKEjD8RCyAc-iYQMcq4OxGHvOHXFY ZqmdcEq3sUvs9cl7K8nj48poxSQi15yhECergqmY0_G6Go3EIy0s17X-h35qeABBatPq0j1uS8CYH1Zj_UhHHb8u8kpwFv1iIGYvAAk5WPmBTTosnAV_C85MJ4 {"score":"4x8","time":"15:16.2342"}
REP> OK 34 35

Send a notification an expiry too far in the future:

REQ> send +3000000 1 APA91bE01klpKUSdNV7VV-8_kixm4MA9Vn10Hua1-1jGe9GZMXcvCSl1fKUUNmNGTJoPa3thUHEKUEjatJh-Qtlc5xbWlFt23wSfT69a5ucmp4jdXw20KZOVEc6rOaPbqL9aqjCDX16xiGCxU3G2qpBcxvtKEjD8RCyAc-iYQMcq4OxGHvOHXFY {"score":"4x8","time":"15:16.2342"}
REP> ERROR Expiry value too high (max 2419200s in the future): 3000000

Send a notification using an invalid payload:

REQ> send 1343727824 1 APA91bE01klpKUSdNV7VV-8_kixm4MA9Vn10Hua1-1jGe9GZMXcvCSl1fKUUNmNGTJoPa3thUHEKUEjatJh-Qtlc5xbWlFt23wSfT69a5ucmp4jdXw20KZOVEc6rOaPbqL9aqjCDX16xiGCxU3G2qpBcxvtKEjD8RCyAc-iYQMcq4OxGHvOHXFY hello
REP> ERROR Invalid JSON payload: hello

III.2.2. feedback command

III.2.2.1. Request

REQ = "feedback"

Simple, isn't it?

III.2.2.2. Reply

REP = "OK" { feedback }
feedback = devicetoken:state:newdevicetoken
state = "replaced" | "notregistered" | "invalid"
  • devicetoken is the affected device token.
  • state determines what to do with devicetoken. If "notregistered" or "invalid" you must remove it from your database and possibly investigate for the latter. If "replaced", devicetoken must be replaced with newdevicetoken in your database.
  • newdevicetoken is the device token with which you must replace devicetoken in your database if state is "replaced". Otherwise it is empty.

III.2.2.3. Example

No feedback:

REQ> feedback
REP> OK

Feedback containing one unregistered device token and one invalid device token: print

REQ> feedback
REP> OK APA91bE01klpKUSdNV7VV-8_kixm4MA9Vn10Hua1-1jGe9GZMXcvCSl1fKUUNmNGTJoPa3thUHEKUEjatJh-Qtlc5xbWlFt23wSfT69a5ucmp4jdXw20KZOVEc6rOaPbqL9aqjCDX16xiGCxU3G2qpBcxvtKEjD8RCyAc-iYQMcq4OxGHvOHXFY:unregistered: ZqmdcEq3sUvs9cl7K8nj48poxSQi15yhECergqmY0_G6Go3EIy0s17X-h35qeABBatPq0j1uS8CYH1Zj_UhHHb8u8kpwFv1iIGYvAAk5WPmBTTosnAV_C85MJ4:invalid:

Feedback containing a replaced device token:

REQ> feedback
REP> OK
APA91bE01klpKUSdNV7VV-8_kixm4MA9Vn10Hua1-1jGe9GZMXcvCSl1fKUUNmNGTJoPa3thUHEKUEjatJh-Qtlc5xbWlFt23wSfT69a5ucmp4jdXw20KZOVEc6rOaPbqL9aqjCDX16xiGCxU3G2qpBcxvtKEjD8RCyAc-iYQMcq4OxGHvOHXFY:replaced:ZqmdcEq3sUvs9cl7K8nj48poxSQi15yhECergqmY0_G6Go3EIy0s17X-h35qeABBatPq0j1uS8CYH1Zj_UhHHb8u8kpwFv1iIGYvAAk5WPmBTTosnAV_C85MJ4

IV. CONFIGURATION

Configuration file is pretty well commented and should not pose you any problem.

IV.1. APNS certificate

As time of writing, APNS is signed by Entrust. You can check this using:

openssl s_client -connect gateway.sandbox.push.apple.com:2195 

This command will show you the certificate used by APNS. The output is quite verbose, but you will see a "Certificate chain" section at the beginning. At time of writing, the certificate chain is:

Certificate chain
   0 s:/C=US/ST=California/L=Cupertino/O=Apple Inc./OU=iTMS Engineering/CN=gateway.sandbox.push.apple.com
     i:/C=US/O=Entrust, Inc./OU=www.entrust.net/rpa is incorporated by reference/OU=(c) 2009 Entrust, Inc./CN=Entrust Certification Authority - L1C
   1 s:/C=US/O=Entrust, Inc./OU=www.entrust.net/rpa is incorporated by reference/OU=(c) 2009 Entrust, Inc./CN=Entrust Certification Authority - L1C
     i:/O=Entrust.net/OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/OU=(c) 1999 Entrust.net Limited/CN=Entrust.net Certification Authority (2048)

This last line is the root certificate you need to authenticate APNS' certificate. On Debian, you can find it in /etc/ssl/certs/Entrust.net_Premium_2048_Secure_Server_CA.pem

In case you need it, just ask "Entrust root certificate" to Google and once you are on the page on Entrust's website, select "Root Certificates" and download "entrust_2048_ca.cer". This is the same file as above.

IV.2. GCM certificate

GCM is signed by Equifax (GeoTrust):

openssl s_client -connect android.googleapis.com:443

At time of writing, the certificate chain is:

Certificate chain
   0 s:/C=US/ST=California/L=Mountain View/O=Google Inc/CN=*.googleapis.com
     i:/C=US/O=Google Inc/CN=Google Internet Authority
   1 s:/C=US/O=Google Inc/CN=Google Internet Authority
     i:/C=US/O=Equifax/OU=Equifax Secure Certificate Authority

The last line is the root certificate you need to authenticate GCM' certificate. On Debian, you can find it in /etc/ssl/certs/Equifax_Secure_CA.pem

In case yu need it: http://www.geotrust.com/resources/root-certificates/index.html

V. IMPLEMENTATION BIG PICTURE

Each service has at least two kind of threads:

  • one listener thread which receives commands through a ZeroMQ REP/REQ socket;
  • one or more agent threads pushing notifications to the provider;
  • for APNS, there is one additional thread to periodically retrieve informations for the feedback service.

Internally, the communication between various threads is done using persistent queues, implemented with SQLite. The class containing most of the logic is OrderedPersistentQueue. ChronologicalPersistentQueue derives from it and the order is turned into a timestamp to ensure a timely delivery. PersistentFIFO also derives from it and just provides a simpler API than the base class.

The listener thread listens on a REP/REQ (ping/pong) ZeroMQ socket for user commands. It puts new notifications on a persistent queue and gets feedback information from another persistent queue.

Depending on the configuration file, there are 1 to N threads connected to the push service for sending notifications. It waits on the notification persistent queue and carries them out as they come. It also enqueues some feedback informations on the feedback persistent queue.

For APNS, one thread is dedicated to periodically retrieve informations for the feedback service and enqueues them on the feedback persistent queue.

Something went wrong with that request. Please try again.