Apple Push Notifications with Go language

I started to familiarize myself to the Go language, and decided to do the usual try out, i.e. sending Apple Push Notifications. It’s my personal usability benchmark for new programming environments. So far in the series

Step 1. Prerequisites

Get and build Go. Example here was done on Ubuntu 10.04 LTS x64 with Go installed based on instructions here at Go getting started guide.

  • 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 2. The Code.

The code here is complete, copy it to file apn.go or get it from Github.

Make sure you change the certificate files (cert.pem and key-noenc.pem) to point to your own certificate files. Also, replace the push token with your own push token, it’s written as hexadecimal string in this example for clarity.

package main

import (
   "crypto/tls"
   "fmt"
   "net"
   "json"
   "os"
   "time"
   "bytes"
   "encoding/hex"
   "encoding/binary"
)

func main() {

   // load certificates and setup config
   cert, err := tls.LoadX509KeyPair("cert.pem", "key-noenc.pem")
   if err != nil {
       fmt.Printf("error: %s\n", err.String())
       os.Exit(1)
   }
   conf := &tls.Config {
        Certificates: []tls.Certificate{cert},
   }

   // connect to the APNS and wrap socket to tls client
   conn, err := net.Dial("tcp", "", "gateway.sandbox.push.apple.com:2195")
   if err != nil {
      fmt.Printf("tcp error: %s\n", err.String())
      os.Exit(1)
   }
   tlsconn := tls.Client(conn, conf)

   // Force handshake to verify successful authorization.
   // Handshake is handled otherwise automatically on first
   // Read/Write attempt
   err = tlsconn.Handshake()
   if err != nil {
      fmt.Printf("tls error: %s\n", err.String())
      os.Exit(1)
   }
   // informational debugging stuff
   state := tlsconn.ConnectionState()
   fmt.Printf("conn state %v\n", state)

   // prepare binary payload from JSON structure
   payload := make(map[string]interface{})
   payload["aps"] = map[string]string{"alert": "Hello Push"}
   bpayload, err := json.Marshal(payload)

   // decode hexadecimal push device token to binary byte array
   btoken, _ := hex.DecodeString("6b4628de9317c80edd1c791640b58fdfc46d21d0d2d1351687239c44d8e30ab1") 

   // build the actual pdu
   buffer := bytes.NewBuffer([]byte{})
   // command
   binary.Write(buffer, binary.BigEndian, uint8(1))

   // transaction id, optional
   binary.Write(buffer, binary.BigEndian, uint32(1))

   // expiration time, 1 hour
   binary.Write(buffer, binary.BigEndian, uint32(time.Seconds() + 60*60))

   // push device token
   binary.Write(buffer, binary.BigEndian, uint16(len(btoken)))
   binary.Write(buffer, binary.BigEndian, btoken)

   // push payload
   binary.Write(buffer, binary.BigEndian, uint16(len(bpayload)))
   binary.Write(buffer, binary.BigEndian, bpayload)
   pdu := buffer.Bytes()

   // write pdu
   _, err = tlsconn.Write(pdu)
   if err != nil {
      fmt.Printf("write error: %s\n", err.String())
      os.Exit(1)
   }

   // wait for 5 seconds error pdu from the socket
   tlsconn.SetReadTimeout(5*1E9)

   readb := [6]byte{}
   n, err := tlsconn.Read(readb[:])
   if n > 0 {
     fmt.Printf("received: %s\n", hex.EncodeToString(readb[:n]))
   }

   tlsconn.Close()
}


Step 3. Compile and Run

Simple

$ 6g apn.go
$ 6l apn.6
$ ./6.out
conn state {true 47}
$

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

12 Responses to Apple Push Notifications with Go language

  1. How would you compare Go with Node.js / JavaScript and Erlang? In this use case and in general.

    • tikonen says:

      I would do this with Go for real life app, but much more important than the language are available libraries and stable tools.

  2. Kai Backman says:

    Looks good, especially for your first go program. A few comments, you might already know these but the sample is so short it’s hard to tell..

    – A more canonical way than printing the error and calling os.Exit() would be to just call panic. However, in this case I think you did the right thing, the output would be messier if you’d used panic. I would probably wrap the error reporting into a helper function and call that instead:

    func checkError(err os.Error, format string, v …interface{}) {
    if err != nil {
    fmt.Printf(format, v)
    os.Exit(1)
    }
    }

    and then call checkError(err, “LoadX509KeyPair: %v\n”, err)

    – You don’t need to explicitly call err.String(), the fmt library uses reflection and can dig the information out for you if you use the generic %v formatting parameter. All os.Errors satisfy the interface { String() string }.

    – The way you build the data structure passed to json.Marshal works (and is pretty short) but would probably get tedious for a more complex input. Usually you just define structs directly and let the json library use reflection to dig the data out:

    type jsonNotification struct {
    Aps map[string] string “aps” // tag required to force lover casing of the json output
    }

    and then your marshaling code would become:
    notification := jsonNotification{
    Aps: map[string] string {“alert”: “Hello Notification”},
    }
    payload, err := json.Marshal(notification)

    – You really should be checking the error from binary.Write.. In this case you know it probably wont fail, but it’s pretty bad form. That said checking every error is quite tedious and the calls to binary.Write are repetitive so when I recently did similar code I wrote this little helper to make the code more readable:

    func bwrite(w io.Writer, values …interface{}) os.Error {
    for _, v := range values {
    err := binary.Write(w, binary.BigEndian, v)
    if err != nil {
    return err
    }
    }
    return nil
    }

    you can then replace the mid section with this:
    err := bwrite(buffer, uint8(1). uint32(1), uint32(time.Seconds() + 60*60), uint16(len(btoken)), btoken, uint16(len(bpayload)), bpayload)
    checkError(err, “binary write %v\n”, err)

    or split if up into several writes if you want to.

    I can see who has been influencing Petteri to tinker around with Go.. :-)

    • tikonen says:

      Thanks for feedback! I didn’t realize the binary Write could fail, unless you mean Out of memory condition?

      • Kai Backman says:

        binary.Write will fail if the underlying stream fails. In this case you are writing to a bytes.Buffer so it wont fail except with OOM. However, memory depletion doesn’t really count as there is usually not much you can do if you detect it.

        I’m just a stickler with checking error values, you never know when something in the code changes subtly and the person doing the change misses a hidden assumption like the one above. It’s more a question of canonical form than an actual error in this particular code.

  3. Kai Backman says:

    Duh, just noticed the existing binary.Write already supports passing multiple values. The benefit of the shorthand is mostly to hide the encoding which probably isn’t useful. So you could just say this:


    err := binary.Write(buffer, binary.BigEndian, uint8(1). uint32(1), uint32(time.Seconds() + 60*60), uint16(len(btoken)), btoken, uint16(len(bpayload)), bpayload)

  4. Nice feedback!

    Kai, I actually pointed Teemu towards Go. I’m a specialist in finding new languages and platforms and then having neither time nor skills to really test them.

  5. sj2004 says:

    Yeah, for first program this is impressive. It looks clean, compact and easy to read.
    Great write up.

  6. Ryan says:

    HI, thanks for the post. I’m using this as a starting point for writing a push notification service and it’s been very useful so far. I have a question about the .pem files.

    How do you generate them? I have the certificate from Apple as well as the private key. What is meant to be in each of the .pem files and how would I generate them from the keychain?

    Thanks
    Ryan

  7. Pingback: Apple Push Notifications with Haskell « Brave New Method

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

Leave a comment