Apple Push Notifications with Haskell
November 8, 2012 3 Comments
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
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
Pingback: Simple test server for Apple push notification and feedback integration | Brave New Method
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.