Building Push-Triggered Sync, Part III: Connecting to APNs

Now that we’ve converted our notification data into a format that’s suitable for sending to Apple, our fledgling push provider needs a connection into APNs in order to send that data. Once again, Apple’s documentation comes to our rescue: the chapter “Provider Communication with Apple Push Notification Service” describes how to manage connections to the push notification service.

Among the important bits in this chapter:

  • When connecting, the provider needs the SSL certificate from the Member Center available for authenticating itself to Apple.
  • The provider should maintain a persistent connection to APNs, rather than connecting and disconnecting for each notification. (At higher volumes, Apple might even consider this behavior to be a DDOS attack!)
  • There are two different environments for push notifications: development and production. A connection is specific to one app’s bundle ID, and therefore (at least in Omni’s bundle ID configuration) specific to one of these environments.

To handle all this, we need to build a little flexibility into the provider. We turned to the open-source gcfg library, which can be imported into a Go project simply by running

go get "code.google.com/p/gcfg"

and then importing that same URL at the head of a Go file. With this library, we can define a configuration file that tells the provider about what APNs host(s) it should connect to, and what certificates it should use along the way.

Let’s take a look first at the Go struct that the provider reads its APNs configuration into:

type ConnConfig struct {
    Domain   string
    CertFile string
    KeyFile  string
}

This struct expresses a single connection to APNs. It defines the domain we’ll connect to, letting us switch between sandbox and production environments by changing the host we contact. It also lets us tell the provider where its certificates are, so that it can establish a secure connection and identify itself to APNs, all in one swoop.

For OmniFocus, though, we have multiple bundle IDs – one for the Universal version of the app, and one for the iPhone-only variant – so we’ll need a way to connect to APNs multiple times. To accomplish this, we parse something slightly more complex out of the configuration file:

func ParseConfigFile(filePath string) (map[string]*ConnConfig, error) {
    var configuration struct {
        Apns map[string]*ConnConfig
    }

    err := gcfg.ReadFileInto(&configuration, filePath)
    if err != nil {
        return nil, err
    }
    return configuration.Apns, nil
}

Instead of just reading a single instance of the ConnConfig struct, we’ll read an entire map of them, keyed by strings. The gcfg package uses maps to represent configuration subsections – instead of just specifying a single apns section, we can specify a bunch, each with its own key:

[apns "com.omnigroup.OmniFocus2.iPad"] ; Universal
domain = sandbox.push.apple.com
certFile = debug-universal.cer
keyFile = debug-universal.key

[apns "com.omnigroup.OmniFocus2.iPhone"] ; iPhone-only
domain = sandbox.push.apple.com
certFile = debug-iphone.cer
keyFile = debug-iphone.key

This way, parsing a configuration file can return one ConnConfig struct for each bundle ID that we’ll use to connect. The provider’s connection code can then iterate over these structs, establishing multiple connections along the way. For each connection, that code is fairly straightforward:

func (config *ConnConfig) connectAPNs() (*tls.Conn, error) {
    cert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile)
    if err != nil {
        return nil, err
    }

    tlsConfig := new(tls.Config)
    tlsConfig.Certificates = []tls.Certificate{cert}

    host := net.JoinHostPort("gateway." + config.Domain, "2195")
    conn, err := tls.Dial("tcp", host, tlsConfig)
    if err != nil {
        return nil, err
    }

    return conn, err
}

Once this connection is established, the provider can hang on to this tls.Conn pointer as long as it needs to, sending multiple notifications – in the form of the frames described in the previous post – when changes occur in OmniFocus. In the next post, we’ll take a look at how the provider manages all the notifications it needs to send across these connections.