Apple Push Notifications with Haskell

In series of language evaluation it’s this time Push notifications with Haskell. Haskell is a pure functional language with strong static typing and as such is not ideally suited for IO code (networking) with dynamic data (JSON). Let’s see how it compares with the others. So far in the series

Step 1. Prerequisites

This code uses GHC 7.4.2 (Haskell compiler). On OS/X its easiest to install with port (or homebrew)

    $ sudo port install ghc
    $ ghc --version
    The Glorious Glasgow Haskell Compilation System, version 7.4.2

This installs the compiler and interpreter. You will also need cabal package manager. Base Haskell installation has surprisingly few tricks out of the box and lots of libraries are needed.

    $ sudo port install hs-cabal
    $ cabal-0.14.0 update
    $ cabal-0.14.0 install cabal-install

After this cabal command can be run from ~/.cabal/bin/cabal

  • Read introduction to Apple Push here and get application and private key sandbox certificates as .pem files.
  • And of course you need to have 32 byte push token from your iOS application.

Step 1. The Utilities.

Hexadecimal to binary

Haskell like many similar languages encourage coding style where programs are written as composition of small simple functions. We’ll start with one that encodes hexadecimal string to ByteString. ByteString is Haskell’s practical presentation of binary data.

import qualified Data.ByteString as B
import GHC.Word (Word8)
import Data.Convertible (convert)
import Data.Char (ord, chr, toUpper)
import Data.Bits (shift, (.|.), (.&.)) 

hexToByteString :: String -> B.ByteString
hexToByteString s
  | null s = B.empty
  | otherwise = B.pack . hexToWord8 $ s
  where
    hexToWord8 :: String -> [Word8]
    hexToWord8 [] = []
    hexToWord8 [x] = error "Invalid hex stream"
    hexToWord8 (x:y:xs) = [ hn .|. ln ] ++ hexToWord8 xs
      where
        hn = (shift (decodeNibble x) 4)
        ln = decodeNibble y
        decodeNibble c
          | o >= oA && o <= oF = convert (o - oA + 10) :: Word8
          | o >= o0 && o <= o9 = convert (o - o0) :: Word8
          | otherwise = error $ "Invalid hex: " ++ [c]
          where o = ord . toUpper $ c
                oA = ord 'A'
                oF = ord 'F'
                o0 = ord '0'
                o9 = ord '9'

Note the imports for Word8 and convert. Haskell as statically typed language requires that every single item has to have unambiguous data type. Imported functions are needed to manipulate and convert data so we can get from String (that is list of Char’s) to list of 8bit bytes (list of Word8’s) finally to ByteString

Install these packages to import Data.Convertible and ByteString

~/.cabal/bin/cabal install convertible
~/.cabal/bin/cabal install bytestring

Save file as Hex.hs and try it out

$ ghci
GHCi, version 7.4.2: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude> :load Hex.hs
[1 of 1] Compiling Util.Hex         ( Hex.hs, interpreted )
Ok, modules loaded: Util.Hex.
*Util.Hex> hexToByteString "AABBCC"
"\170\187\204"
*Util.Hex> :type hexToByteString "AABBCC"
hexToByteString "AABBCC" :: B.ByteString
*Util.Hex> hexToByteString "Something"
"*** Exception: Invalid hex stream
*Util.Hex> hexToByteString "AA"
"\170"

Seems to be working.

JSON encoding

Next step is function that produces a UTF8 encoded JSON object that contains our push notification payload. We use simple object that contains only the alert message.

Save this in file Push.hs

import Text.JSON as JSON
import qualified Data.ByteString.UTF8 as BU

getJSONWithMessage :: String -> JSObject (JSValue)
getJSONWithMessage msg =
  let jmsg = JSString (toJSString msg) in
  toJSObject [("aps",
               JSObject (toJSObject [("alert", jmsg)]))]

Install packages for UTF8 ByteString and Text.JSON

~/.cabal/bin/cabal install json
~/.cabal/bin/cabal install utf8-string

Check that we get correctly formatted JSON string

Prelude> :load Push.hs
[1 of 1] Compiling Main             ( Push.hs, interpreted )
Ok, modules loaded: Main.
*Main> getJSONWithMessage "Hello World"
*Main> getJSONWithMessage "Hello World"
JSONObject {fromJSObject = [("aps",JSObject (JSONObject {fromJSObject = [("alert",JSString (JSONString {fromJSString = "Hello World"}))]}))]}
*Main> JSON.encode . getJSONWithMessage $ "Hello World"
"{\"aps\":{\"alert\":\"Hello World\"}}"

Lets also check that messages with non-ASCII character can be supported

*Main BU> BU.fromString . JSON.encode . getJSONWithMessage $ "Mötörhead"
"{\"aps\":{\"alert\":\"M\195\182t\195\182rhead\"}}"
*Main BU> 

The encoded JSON looks fine.

Building the PDU

For this purpose we use Put that supports building Lazy BinaryString’s with content

import qualified Data.ByteString as B
import qualified Data.ByteString.UTF8 as BU
import qualified Data.ByteString.Lazy as BL
import Data.Binary.Put
import GHC.Word (Word32, Word16)
import Data.Convertible (convert)

buildPDU :: B.ByteString -> BU.ByteString -> Word32 -> Put
buildPDU token payload expiry
  | (B.length token) /= 32 = fail "Invalid token"
  | (B.length payload > 255) = fail "Too long payload"
  | otherwise = do
    putWord8 1 -- command
    putWord32be 1 -- transaction id, can be anything
    putWord32be expiry  -- expiry time as seconds from epoch
    putWord16be ((convert $ B.length token) :: Word16) -- length of token
    putByteString token  -- push token
    putWord16be ((convert $ B.length payload) :: Word16) - payload length
    putByteString payload -- the json encoded as utf-8 string

We need also simple function to compute expiry time as relative to now

import Data.Time.Clock.POSIX (getPOSIXTime)

getExpiryTime :: IO (Word32)
getExpiryTime = do
  pt <- getPOSIXTime
  -- One hour expiry time
  return ( (round pt + 60*60):: Word32)

Install packages for Data.Binary.Put

~/.cabal/bin/cabal install binary

Step 2. Connecting to the Server

Make sure you have the certificate files (cert.pem and key-noenc.pem).

import Network.Socket
import Network.BSD (getHostByName, hostAddress, getProtocolNumber)
import OpenSSL
import OpenSSL.Session as SSL (
  context,
  contextSetPrivateKeyFile,
  contextSetCertificateFile,
  contextSetCiphers,
  contextSetDefaultCiphers,
  contextSetVerificationMode,
  contextSetCAFile,
  connection,
  connect,
  shutdown,
  write,
  read,
  SSL,
  VerificationMode(..),
  ShutdownType(..)
  )

main = withOpenSSL $ do
  -- Prepare SSL context
  ssl <- context
  contextSetPrivateKeyFile ssl "key-noenc.pem"
  contextSetCertificateFile ssl "cert.pem"
  contextSetDefaultCiphers ssl
  contextSetVerificationMode ssl SSL.VerifyNone

  -- Open socket
  proto <- (getProtocolNumber "tcp")
  he <- getHostByName "gateway.sandbox.push.apple.com"
  sock <- socket AF_INET Stream proto
  Network.Socket.connect sock (SockAddrInet 2195 (hostAddress he))

  -- Promote socket to SSL stream
  sslsocket <- connection ssl sock
  SSL.connect sslsocket  -- Handshake

  -- we'll send pdu here

Install OpenSSL package

~/.cabal/bin/cabal install HsOpenSSL

Step 3. Send the PDU

Now we’re finally ready to send the actual push notification message. Replace the push token to your own in following example.


...
  -- Promoto socket to SSL stream
  sslsocket <- connection ssl sock
  SSL.connect sslsocket  -- Handshake

  expiration <- getExpiryTime
  -- we send pdu here
  let token = "6b4628de9317c80edd1c791640b58fdfc46d21d0d2d1351687239c44d8e30ab1"
      message = "Hello World"
      btoken = hexToByteString token
      payload = BU.fromString . JSON.encode . getJSONWithMessage $ message
      lpdu = runPut $ buildPDU btoken payload expiration  -- build binary pdu
      pdu = toStrict lpdu  -- from lazy bytestring to strict
    in do
    SSL.write sslsocket pdu
    SSL.shutdown sslsocket Unidirectional -- Close gracefully
  where
    toStrict = B.concat . BL.toChunks

Then compile and run program

$ ghc -threaded -o Push Push.hs
[1 of 2] Compiling Hex              ( Hex.hs, Hex.o )
[2 of 2] Compiling Main             ( Push.hs, Push.o )
Linking Push ...
$ ./Push

If everything went fine, the program exits within few seconds and you’ll see your push notification appear on your iOS device.

Full source of this example available here: https://github.com/tikonen/blog/tree/master/apn-haskell

3 Responses to Apple Push Notifications with Haskell

  1. Hello,
    I am very excited about getting into Haskell but I have been having a great deal of difficulty getting it running on iOS or finding any up to date cross compilation information. I would greatly appreciate a blog or any insights you might have in getting up and running for a beginner.

    Thanks,
    Casey

  2. Pingback: Simple test server for Apple push notification and feedback integration | Brave New Method

  3. Nate Symer says:

    I’d like to mention that Aeson is vastly superior to Text.JSON. Not only does it offer lenses and traversals, but it’s significantly faster and operates on ByteStrings rather than Strings. Strings are the enemy of properly encoded foreign characters and performance.

Leave a comment