[CalendarServer-changes] [8145] CalendarServer/branches/users/sagen/applepush

source_changes at macosforge.org source_changes at macosforge.org
Tue Oct 4 17:06:45 PDT 2011


Revision: 8145
          http://trac.macosforge.org/projects/calendarserver/changeset/8145
Author:   sagen at apple.com
Date:     2011-10-04 17:06:43 -0700 (Tue, 04 Oct 2011)
Log Message:
-----------
Checkpoint of work so far.  APN advertisement, subscription, push all work.  Have to implement feedback still.

Modified Paths:
--------------
    CalendarServer/branches/users/sagen/applepush/calendarserver/tap/util.py
    CalendarServer/branches/users/sagen/applepush/twistedcaldav/notify.py
    CalendarServer/branches/users/sagen/applepush/twistedcaldav/resource.py
    CalendarServer/branches/users/sagen/applepush/twistedcaldav/stdconfig.py
    CalendarServer/branches/users/sagen/applepush/txdav/common/datastore/sql.py
    CalendarServer/branches/users/sagen/applepush/txdav/common/datastore/sql_schema/current.sql
    CalendarServer/branches/users/sagen/applepush/txdav/common/icommondatastore.py

Added Paths:
-----------
    CalendarServer/branches/users/sagen/applepush/twistedcaldav/applepush.py
    CalendarServer/branches/users/sagen/applepush/twistedcaldav/test/test_applepush.py
    CalendarServer/branches/users/sagen/applepush/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_5_to_6.sql
    CalendarServer/branches/users/sagen/applepush/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_5_to_6.sql

Modified: CalendarServer/branches/users/sagen/applepush/calendarserver/tap/util.py
===================================================================
--- CalendarServer/branches/users/sagen/applepush/calendarserver/tap/util.py	2011-10-04 22:11:00 UTC (rev 8144)
+++ CalendarServer/branches/users/sagen/applepush/calendarserver/tap/util.py	2011-10-05 00:06:43 UTC (rev 8145)
@@ -56,6 +56,7 @@
 from twistedcaldav.directory.sudo import SudoDirectoryService
 from twistedcaldav.directory.wiki import WikiDirectoryService
 from twistedcaldav.notify import NotifierFactory, getPubSubConfiguration
+from twistedcaldav.applepush import APNSubscriptionResource
 from twistedcaldav.directorybackedaddressbook import DirectoryBackedAddressBookResource
 from twistedcaldav.resource import CalDAVResource, AuthenticationWrapper
 from twistedcaldav.schedule import IScheduleInboxResource
@@ -358,6 +359,7 @@
     webAdminResourceClass           = WebAdminResource
     addressBookResourceClass        = DirectoryAddressBookHomeProvisioningResource
     directoryBackedAddressBookResourceClass = DirectoryBackedAddressBookResource
+    apnSubscriptionResourceClass    = APNSubscriptionResource
 
     directory = directoryFromConfig(config)
 
@@ -593,6 +595,16 @@
         root.putChild("admin", webAdmin)
 
     #
+    # Apple Push Notification Subscriptions
+    #
+    apnConfig = config.Notifications.Services["ApplePushNotifier"]
+    if apnConfig.Enabled:
+        log.info("Setting up APNS resource at /%s" %
+            (apnConfig["SubscriptionURL"],))
+        apnResource = apnSubscriptionResourceClass(newStore)
+        root.putChild(apnConfig["SubscriptionURL"], apnResource)
+
+    #
     # Configure ancillary data
     #
     log.info("Setting up Timezone Cache")

Added: CalendarServer/branches/users/sagen/applepush/twistedcaldav/applepush.py
===================================================================
--- CalendarServer/branches/users/sagen/applepush/twistedcaldav/applepush.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/applepush/twistedcaldav/applepush.py	2011-10-05 00:06:43 UTC (rev 8145)
@@ -0,0 +1,377 @@
+##
+# Copyright (c) 2005-2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.internet.ssl import ChainingOpenSSLContextFactory
+from twext.python.log import Logger, LoggingMixIn
+from twext.python.log import LoggingMixIn
+from twext.web2 import responsecode
+from twext.web2.http import Response
+from twext.web2.http_headers import MimeType
+from twext.web2.resource import Resource
+from twext.web2.server import parsePOSTData
+from twisted.application import service
+from twisted.internet import reactor, protocol
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.protocol import ClientFactory, ReconnectingClientFactory
+import OpenSSL
+import struct
+import time
+
+"""
+ApplePushNotifierService is a MultiService responsible for setting up the
+APN provider and feedback connections.  Once connected, calling its enqueue( )
+method sends notifications to any device token which is subscribed to the
+enqueued key.
+
+The Apple Push Notification protocol is described here:
+
+http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html
+"""
+
+
+log = Logger()
+
+
+class ApplePushNotifierService(service.MultiService, LoggingMixIn):
+
+    @classmethod
+    def makeService(cls, settings, store):
+        service = cls()
+
+        service.store = store
+        service.providers = {}
+        service.feedbacks = {}
+        service.dataHost = settings["DataHost"]
+
+        for protocol in ("CalDAV", "CardDAV"):
+
+            provider = APNProviderService(
+                settings["ProviderHost"],
+                settings["ProviderPort"],
+                settings[protocol]["CertificatePath"],
+                settings[protocol]["PrivateKeyPath"],
+            )
+            provider.setServiceParent(service)
+            service.providers[protocol] = provider
+            service.log_info("APNS %s topic: %s" %
+                (protocol, settings[protocol]["Topic"]))
+
+            feedback = APNFeedbackService(
+                settings["FeedbackHost"],
+                settings["FeedbackPort"],
+                settings[protocol]["CertificatePath"],
+                settings[protocol]["PrivateKeyPath"],
+            )
+            feedback.setServiceParent(service)
+            service.feedbacks[protocol] = feedback
+
+
+        return service
+
+
+    @inlineCallbacks
+    def enqueue(self, op, id):
+
+        try:
+            protocol, id = id.split("|", 1)
+        except ValueError:
+            # id has no protocol, so we can't do anything with it
+            self.log_error("Notification id '%s' is missing protocol" % (id,))
+            return
+
+        provider = self.providers.get(protocol, None)
+        if provider is not None:
+            key = "/%s/%s/%s/" % (protocol, self.dataHost, id)
+
+            # Look up subscriptions for this key
+            txn = self.store.newTransaction()
+            subscriptions = (yield txn.apnSubscriptionsByKey(key))
+            yield txn.commit()
+
+            for token, guid in subscriptions:
+                self.log_debug("Sending APNS: token='%s' key='%s' guid='%s'" %
+                    (token, key, guid))
+                provider.sendNotification(token, key)
+
+
+
+class APNProviderProtocol(protocol.Protocol, LoggingMixIn):
+    """
+    Implements the Provider portion of APNS
+    """
+
+    # Sent by provider
+    COMMAND_SIMPLE   = 0
+    COMMAND_ENHANCED = 1
+
+    # Received by provider
+    COMMAND_ERROR    = 8
+
+    # Returned only for an error.  Successful notifications get no response.
+    STATUS_CODES = {
+        0   : "No errors encountered",
+        1   : "Processing error",
+        2   : "Missing device token",
+        3   : "Missing topic",
+        4   : "Missing payload",
+        5   : "Invalid token size",
+        6   : "Invalid topic size",
+        7   : "Invalid payload size",
+        8   : "Invalid token",
+        255 : "None (unknown)",
+    }
+
+    def makeConnection(self, transport):
+        self.identifier = 0
+        self.log_debug("ProviderProtocol makeConnection")
+        protocol.Protocol.makeConnection(self, transport)
+
+    def connectionMade(self):
+        self.log_debug("ProviderProtocol connectionMade")
+        # TODO: glyph review
+        # Store a reference to ourself on the factory so the service can
+        # later call us
+        self.factory.connection = self
+        # self.sendNotification(TOKEN, "xyzzy")
+
+    def connectionLost(self, reason=None):
+        self.log_error("ProviderProtocol connectionLost: %s" % (reason,))
+        # TODO: glyph review
+        # Clear the reference to us from the factory
+        self.factory.connection = None
+
+    def dataReceived(self, data):
+        self.log_debug("ProviderProtocol dataReceived %d bytes" % (len(data),))
+        command, status, identifier = struct.unpack("!BBI", data)
+        if command == self.COMMAND_ERROR:
+            self.processError(status, identifier)
+
+    def processError(self, status, identifier):
+        msg = self.STATUS_CODES.get(status, "Unknown status code")
+        self.log_debug("ProviderProtocol processError %d on identifier %d: %s" % (status, identifier, msg))
+        # TODO: do we want to retry after certain errors?
+
+    def sendNotification(self, token, node):
+        try:
+            binaryToken = token.replace(" ", "").decode("hex")
+        except:
+            self.log_error("Invalid APN token in database: %s" % (token,))
+            return
+
+        self.identifier += 1
+        payload = '{"key" : "%s"}' % (node,)
+        payloadLength = len(payload)
+        self.log_debug("ProviderProtocol sendNotification identifier=%d payload=%s" % (self.identifier, payload))
+
+        self.transport.write(
+            struct.pack("!BIIH32sH%ds" % (payloadLength,),
+                self.COMMAND_ENHANCED,  # Command
+                self.identifier,        # Identifier
+                0,                      # Expiry
+                32,                     # Token Length
+                binaryToken,            # Token
+                payloadLength,          # Payload Length
+                payload,                # Payload in JSON format
+            )
+        )
+
+
+class APNProviderFactory(ReconnectingClientFactory, LoggingMixIn):
+
+    protocol = APNProviderProtocol
+
+    def buildProtocol(self, addr):
+        p = self.protocol()
+        # TODO: glyph review
+        # Give protocol a back-reference to factory so it can set/clear
+        # the "connection" reference on the factory
+        p.factory = self
+        return p
+
+    def clientConnectionLost(self, connector, reason):
+        self.log_error("Connection to APN server lost: %s" % (reason,))
+        ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
+
+    def clientConnectionFailed(self, connector, reason):
+        self.log_error("Unable to connect to APN server: %s" % (reason,))
+        self.connected = False
+        ReconnectingClientFactory.clientConnectionFailed(self, connector,
+            reason)
+
+
+class APNConnectionService(service.Service, LoggingMixIn):
+
+    def __init__(self, host, port, certPath, keyPath, chainPath="",
+        sslMethod="TLSv1_METHOD", testConnector=None):
+
+        self.host = host
+        self.port = port
+        self.certPath = certPath
+        self.keyPath = keyPath
+        self.chainPath = chainPath
+        self.sslMethod = sslMethod
+        self.testConnector = testConnector
+
+    def connect(self, factory):
+        if self.testConnector is not None:
+            # For testing purposes
+            self.testConnector.connect(self, factory)
+        else:
+            context = ChainingOpenSSLContextFactory(
+                self.keyPath,
+                self.certPath,
+                certificateChainFile=self.chainPath,
+                sslmethod=getattr(OpenSSL.SSL, self.sslMethod)
+            )
+            reactor.connectSSL(self.host, self.port, factory, context)
+
+
+class APNProviderService(APNConnectionService):
+
+    def __init__(self, host, port, certPath, keyPath, chainPath="",
+        sslMethod="TLSv1_METHOD", testConnector=None):
+
+        APNConnectionService.__init__(self, host, port, certPath, keyPath,
+            chainPath="", sslMethod=sslMethod,
+            testConnector=testConnector)
+
+    def startService(self):
+        self.log_debug("APNProviderService startService")
+        self.factory = APNProviderFactory()
+        self.connect(self.factory)
+
+    def stopService(self):
+        self.log_debug("APNProviderService stopService")
+
+    def sendNotification(self, token, key):
+        # TODO: glyph review
+        # Service has reference to factory has reference to protocol instance
+        connection = getattr(self.factory, "connection", None)
+        if connection is None:
+            self.log_debug("APNProviderService sendNotification has no connection")
+        else:
+            self.log_debug("APNProviderService sendNotification: %s %s" %
+                (token, key))
+            connection.sendNotification(token, key)
+
+
+class APNFeedbackProtocol(protocol.Protocol, LoggingMixIn):
+    """
+    Implements the Feedback portion of APNS
+    """
+
+    def dataReceived(self, data):
+        self.log_debug("FeedbackProtocol dataReceived %d bytes" % (len(data),))
+        timestamp, tokenLength, binaryToken = struct.unpack("!IH32s", data)
+        token = binaryToken.encode("hex")
+        self.processFeedback(timestamp, token)
+
+    def processFeedback(self, timestamp, token):
+        self.log_debug("FeedbackProtocol processFeedback time=%d token=%s" %
+            (timestamp, token))
+        # TODO: actually see if we need to remove the token from subscriptions
+
+
+class APNFeedbackFactory(ClientFactory, LoggingMixIn):
+
+    protocol = APNFeedbackProtocol
+
+    def buildProtocol(self, addr):
+        p = self.protocol()
+        # TODO: glyph review
+        # Give protocol a back-reference to factory so it can set/clear
+        # the "connection" reference on the factory
+        p.factory = self
+        return p
+
+    def clientConnectionLost(self, connector, reason):
+        self.log_error("Connection to APN feedback server lost: %s" % (reason,))
+        ClientFactory.clientConnectionLost(self, connector, reason)
+
+    def clientConnectionFailed(self, connector, reason):
+        self.log_error("Unable to connect to APN feedback server: %s" %
+            (reason,))
+        self.connected = False
+        ClientFactory.clientConnectionFailed(self, connector, reason)
+
+
+class APNFeedbackService(APNConnectionService):
+
+    def __init__(self, host, port, certPath, keyPath, chainPath="",
+        sslMethod="TLSv1_METHOD", testConnector=None):
+
+        APNConnectionService.__init__(self, host, port, certPath, keyPath,
+            chainPath="", sslMethod=sslMethod,
+            testConnector=testConnector)
+
+    def startService(self):
+        self.log_debug("APNFeedbackService startService")
+        # TODO: Set a timer to connect to feedback at an interval
+        # self.factory = APNFeedbackFactory()
+        # self.connect(self.factory)
+
+    def stopService(self):
+        self.log_debug("APNFeedbackService stopService")
+
+
+class APNSubscriptionResource(Resource):
+
+    # method can be GET or POST
+    # params are "token" (device token) and "key" (push key), e.g.:
+    # token=2d0d55cd7f98bcb81c6e24abcdc35168254c7846a43e2828b1ba5a8f82e219df
+    # key=/CalDAV/calendar.example.com/E0B38B00-4166-11DD-B22C-A07C87F02F6A/
+
+    def __init__(self, store):
+        self.store = store
+        # Hopefully we can use this store to manage subscriptions
+
+    def http_GET(self, request):
+        return self.processSubscription(request.args)
+
+    @inlineCallbacks
+    def http_POST(self, request):
+        yield parsePOSTData(request)
+        returnValue(self.processSubscription(request.args))
+
+    @inlineCallbacks
+    def processSubscription(self, args):
+        token = args.get("token", None)
+        key = args.get("key", None)
+        if key and token:
+            key = key[0]
+            token = token[0].replace(" ", "")
+            yield self.addSubscription(token, key)
+            code = responsecode.OK
+            msg = None
+        else:
+            code = responsecode.BAD_REQUEST
+            msg = "Invalid request: both 'token' and 'key' must be provided"
+
+        returnValue(self.renderResponse(code, body=msg))
+
+    @inlineCallbacks
+    def addSubscription(self, token, key):
+        now = int(time.time()) # epoch seconds
+        txn = self.store.newTransaction()
+        yield txn.addAPNSubscription(token, key, now, "xyzzy")
+        subscriptions = (yield txn.apnSubscriptionsByToken(token))
+        print subscriptions
+        yield txn.commit()
+
+    def renderResponse(self, code, body=None):
+        response = Response(code, {}, body)
+        response.headers.setHeader("content-type", MimeType("text", "html"))
+        return response

Modified: CalendarServer/branches/users/sagen/applepush/twistedcaldav/notify.py
===================================================================
--- CalendarServer/branches/users/sagen/applepush/twistedcaldav/notify.py	2011-10-04 22:11:00 UTC (rev 8144)
+++ CalendarServer/branches/users/sagen/applepush/twistedcaldav/notify.py	2011-10-05 00:06:43 UTC (rev 8145)
@@ -147,15 +147,16 @@
         id = self.getID(label=label)
         pubSubConfig = self._notifierFactory.pubSubConfig
         name = getPubSubPath(id, pubSubConfig)
-        try:
-            if self._notifierFactory.nodeCacher:
-                nodeCacher = self._notifierFactory.nodeCacher
-            else:
-                nodeCacher = getNodeCacher()
-            (yield nodeCacher.waitForNode(self, name))
-        except NodeCreationException, e:
-            self.log_warn(e)
-            returnValue(None)
+        if pubSubConfig["enabled"]:
+            try:
+                if self._notifierFactory.nodeCacher:
+                    nodeCacher = self._notifierFactory.nodeCacher
+                else:
+                    nodeCacher = getNodeCacher()
+                (yield nodeCacher.waitForNode(self, name))
+            except NodeCreationException, e:
+                self.log_warn(e)
+                returnValue(None)
         returnValue(name)
 
 class NotificationClientLineProtocol(LineReceiver, LoggingMixIn):
@@ -458,7 +459,7 @@
 
     def enqueue(self, op, id):
         """
-        Let's the notifier object know that a change has been made for this
+        Let the notifier object know that a change has been made for this
         id, and enough time has passed to allow for coalescence.
 
         @type op: C{str}
@@ -1258,12 +1259,11 @@
 
 def getPubSubConfiguration(config):
     # TODO: Should probably cache this
-    results = { 'enabled' : False }
+    results = { 'enabled' : False, 'host' : config.ServerHostName }
     settings = getXMPPSettings(config)
     if settings is not None:
         results['enabled'] = True
         results['service'] = settings['ServiceAddress']
-        results['host'] = config.ServerHostName
         results['port'] = config.SSLPort or config.HTTPPort
         results['xmpp-server'] = (
             settings['Host'] if settings['Port'] == 5222
@@ -1278,16 +1278,33 @@
     Returns the Apple push notification settings specific to the notifier
     ID, which includes a prefix that is either "CalDAV" or "CardDAV"
     """
-    settings = getXMPPSettings(config)
-    if settings is None:
-        return None
-
     try:
         prefix, id = id.split("|", 1)
     except ValueError:
         # id has no prefix, so we can't look up APS config
         return None
 
+    # If we are directly talking to apple push, advertise those settings
+    applePushSettings = config.Notifications.Services.ApplePushNotifier
+    if applePushSettings.Enabled:
+        settings = {}
+        settings["APSBundleID"] = applePushSettings[prefix]["Topic"]
+        if config.EnableSSL:
+            url = "https://%s:%s/%s" % (config.ServerHostName, config.SSLPort,
+                applePushSettings.SubscriptionURL)
+        else:
+            url = "http://%s:%s/%s" % (config.ServerHostName, config.HTTPPort,
+                applePushSettings.SubscriptionURL)
+        settings["SubscriptionURL"] = url
+        settings["APSEnvironment"] = applePushSettings.Environment
+        return settings
+
+    # ...otherwise pick up the apple push settings we get via XMPP and
+    # apn bridge
+    settings = getXMPPSettings(config)
+    if settings is None:
+        return None
+
     if (settings.has_key(prefix) and
         settings[prefix]["APSBundleID"] and
         settings[prefix]["SubscriptionURL"]):
@@ -1441,12 +1458,47 @@
             config.Memcached.MaxClients,
         )
 
+        from calendarserver.tap.util import (
+            storeFromConfig, pgConnectorFromConfig, oracleConnectorFromConfig,
+            pgServiceFromConfig
+        )
+        from twext.enterprise.ienterprise import POSTGRES_DIALECT
+        from twext.enterprise.ienterprise import ORACLE_DIALECT
+        from twext.enterprise.adbapi2 import ConnectionPool
+
+        pool = None
+        if not config.UseDatabase:
+            txnFactory = None
+        else:
+            dialect = POSTGRES_DIALECT
+            paramstyle = 'pyformat'
+            if config.DBType == '':
+                # get a PostgresService to tell us what the local connection
+                # info is, but *don't* start it (that would start one postgres
+                # master per slave, resulting in all kinds of mayhem...)
+                connectionFactory = pgServiceFromConfig(
+                    config, None).produceConnection
+            elif config.DBType == 'postgres':
+                connectionFactory = pgConnectorFromConfig(config)
+            elif config.DBType == 'oracle':
+                dialect = ORACLE_DIALECT
+                paramstyle = 'numeric'
+                connectionFactory = oracleConnectorFromConfig(config)
+            else:
+                raise UsageError("unknown DB type: %r" % (config.DBType,))
+            pool = ConnectionPool(connectionFactory, dialect=dialect,
+                                  paramstyle=paramstyle)
+            txnFactory = pool.connection
+
+        store = storeFromConfig(config, txnFactory)
+
         multiService = service.MultiService()
 
         notifiers = []
         for key, settings in config.Notifications.Services.iteritems():
             if settings["Enabled"]:
-                notifier = namedClass(settings["Service"])(settings)
+                notifier = namedClass(settings["Service"]).makeService(settings,
+                    store)
                 notifier.setServiceParent(multiService)
                 notifiers.append(notifier)
 
@@ -1462,6 +1514,10 @@
 
 class SimpleLineNotifierService(service.Service):
 
+    @classmethod
+    def makeService(cls, settings, store):
+        return cls(settings)
+
     def __init__(self, settings):
         self.notifier = SimpleLineNotifier(settings)
         self.server = internet.TCPServer(settings["Port"],
@@ -1479,6 +1535,10 @@
 
 class XMPPNotifierService(service.Service):
 
+    @classmethod
+    def makeService(cls, settings, store):
+        return cls(settings)
+
     def __init__(self, settings):
         self.notifier = XMPPNotifier(settings)
 

Modified: CalendarServer/branches/users/sagen/applepush/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/users/sagen/applepush/twistedcaldav/resource.py	2011-10-04 22:11:00 UTC (rev 8144)
+++ CalendarServer/branches/users/sagen/applepush/twistedcaldav/resource.py	2011-10-05 00:06:43 UTC (rev 8145)
@@ -2224,7 +2224,10 @@
             returnValue(customxml.MaxCollections.fromString(config.MaxCollectionsPerHome))
             
         elif qname == (customxml.calendarserver_namespace, "push-transports"):
-            if config.Notifications.Services.XMPPNotifier.Enabled:
+
+            if (config.Notifications.Services.XMPPNotifier.Enabled or
+                config.Notifications.Services.ApplePushNotifier.Enabled):
+
                 nodeName = (yield self._newStoreHome.nodeName())
                 if nodeName:
                     notifierID = self._newStoreHome.notifierID()
@@ -2251,7 +2254,8 @@
                             )
 
                         pubSubConfiguration = getPubSubConfiguration(config)
-                        if pubSubConfiguration['xmpp-server']:
+                        if (pubSubConfiguration['enabled'] and
+                            pubSubConfiguration['xmpp-server']):
                             children.append(
                                 customxml.PubSubTransportProperty(
                                     customxml.PubSubXMPPServerProperty(
@@ -2268,7 +2272,8 @@
             returnValue(None)
 
         elif qname == (customxml.calendarserver_namespace, "pushkey"):
-            if config.Notifications.Services.XMPPNotifier.Enabled:
+            if (config.Notifications.Services.XMPPNotifier.Enabled or
+                config.Notifications.Services.ApplePushNotifier.Enabled):
                 nodeName = (yield self._newStoreHome.nodeName())
                 if nodeName:
                     returnValue(customxml.PubSubXMPPPushKeyProperty(nodeName))

Modified: CalendarServer/branches/users/sagen/applepush/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/sagen/applepush/twistedcaldav/stdconfig.py	2011-10-04 22:11:00 UTC (rev 8144)
+++ CalendarServer/branches/users/sagen/applepush/twistedcaldav/stdconfig.py	2011-10-05 00:06:43 UTC (rev 8145)
@@ -629,6 +629,27 @@
                 "Enabled" : False,
                 "Port" : 62308,
             },
+            "ApplePushNotifier" : {
+                "Service" : "twistedcaldav.applepush.ApplePushNotifierService",
+                "Enabled" : False,
+                "SubscriptionURL" : "apns",
+                "DataHost" : "",
+                "ProviderHost" : "gateway.push.apple.com",
+                "ProviderPort" : 2195,
+                "FeedbackHost" : "feedback.push.apple.com",
+                "FeedbackPort" : 2196,
+                "Environment" : "PRODUCTION",
+                "CalDAV" : {
+                    "CertificatePath" : "",
+                    "PrivateKeyPath" : "",
+                    "Topic" : "",
+                },
+                "CardDAV" : {
+                    "CertificatePath" : "",
+                    "PrivateKeyPath" : "",
+                    "Topic" : "",
+                },
+            },
             "XMPPNotifier" : {
                 "Service" : "twistedcaldav.notify.XMPPNotifierService",
                 "Enabled" : False,
@@ -1173,7 +1194,15 @@
         configDict.Notifications["Enabled"] = False
 
     for key, service in configDict.Notifications["Services"].iteritems():
+
+        # The default for apple push DataHost is ServerHostName
         if (
+            service["Service"] == "twistedcaldav.applepush.ApplePushNotifierService" and
+            service["DataHost"] == ""
+        ):
+            service["DataHost"] = configDict.ServerHostName
+
+        if (
             service["Service"] == "twistedcaldav.notify.XMPPNotifierService" and
             service["Enabled"]
         ):

Added: CalendarServer/branches/users/sagen/applepush/twistedcaldav/test/test_applepush.py
===================================================================
--- CalendarServer/branches/users/sagen/applepush/twistedcaldav/test/test_applepush.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/applepush/twistedcaldav/test/test_applepush.py	2011-10-05 00:06:43 UTC (rev 8145)
@@ -0,0 +1,96 @@
+##
+# Copyright (c) 2011 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.applepush import (
+    ApplePushNotifierService, APNProviderService
+)
+from twistedcaldav.test.util import TestCase
+from twisted.internet.defer import inlineCallbacks
+import struct
+
+class ApplePushNotifierServiceTests(TestCase):
+
+    @inlineCallbacks
+    def test_makeService(self):
+
+        settings = {
+            "Service" : "twistedcaldav.applepush.ApplePushNotifierService",
+            "Enabled" : True,
+            "SubscriptionURL" : "apn",
+            "DataHost" : "calendars.example.com",
+            "ProviderHost" : "gateway.push.apple.com",
+            "ProviderPort" : 2195,
+            "FeedbackHost" : "feedback.push.apple.com",
+            "FeedbackPort" : 2196,
+            "CalDAV" : {
+                "CertificatePath" : "caldav.cer",
+                "PrivateKeyPath" : "caldav.pem",
+                "Topic" : "caldav_topic",
+            },
+            "CardDAV" : {
+                "CertificatePath" : "carddav.cer",
+                "PrivateKeyPath" : "carddav.pem",
+                "Topic" : "carddav_topic",
+            },
+        }
+        service = (yield ApplePushNotifierService.makeService(settings, None))
+        self.assertEquals(set(service.providers.keys()), set(["CalDAV","CardDAV"]))
+        self.assertEquals(set(service.feedbacks.keys()), set(["CalDAV","CardDAV"]))
+
+
+    def test_provider(self):
+        """
+        Sending a notification writes to the transport
+        """
+        testConnector = TestConnector()
+        service = APNProviderService("example.com", 1234, "caldav.cer",
+            "caldav.pem", testConnector=testConnector)
+        service.startService()
+
+        token = "b23b2d34 096f7f3c 7989970c 2d7a074f 50ebebfd 8702ed98 3657ada4 39432e23"
+        key = "/CalDAV/user01/calendar"
+        service.sendNotification(token, key)
+
+        # Verify data sent
+        self.assertEquals(len(testConnector.transport.data), 80)
+        data = struct.unpack("!BIIH32sH", testConnector.getData()[:45])
+        self.assertEquals(data[0], 1) # command
+        self.assertEquals(data[4].encode("hex"), token.replace(" ", "")) # token
+        payloadLength = data[5]
+        payload = struct.unpack("%ds" % (payloadLength,),
+            testConnector.getData()[45:])
+        self.assertEquals(payload[0], '{"key" : "%s"}' % (key,))
+
+
+class TestConnector(object):
+
+    def connect(self, service, factory):
+        service.protocol = factory.buildProtocol(None)
+        service.connected = 1
+        self.transport = StubTransport()
+        service.protocol.makeConnection(self.transport)
+
+    def getData(self):
+        return self.transport.data
+
+
+class StubTransport(object):
+
+    def __init__(self):
+        self.data = None
+
+    def write(self, data):
+        self.data = data

Modified: CalendarServer/branches/users/sagen/applepush/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/sagen/applepush/txdav/common/datastore/sql.py	2011-10-04 22:11:00 UTC (rev 8144)
+++ CalendarServer/branches/users/sagen/applepush/txdav/common/datastore/sql.py	2011-10-05 00:06:43 UTC (rev 8145)
@@ -323,6 +323,103 @@
         return NotificationCollection.notificationsWithUID(self, uid)
 
 
+    @classproperty
+    def _insertAPNSubscriptionQuery(cls): #@NoSelf
+        apn = schema.APN_SUBSCRIPTIONS
+        return Insert({apn.TOKEN: Parameter("token"),
+                       apn.RESOURCE_KEY: Parameter("resourceKey"),
+                       apn.MODIFIED: Parameter("modified"),
+                       apn.SUBSCRIBER_GUID: Parameter("subscriber")})
+
+    @classproperty
+    def _updateAPNSubscriptionQuery(cls): #@NoSelf
+        apn = schema.APN_SUBSCRIPTIONS
+        return Update({apn.MODIFIED: Parameter("modified")},
+                      Where=(apn.TOKEN == Parameter("token")).And(
+                             apn.RESOURCE_KEY == Parameter("resourceKey")))
+
+    @classproperty
+    def _selectAPNSubscriptionQuery(cls): #@NoSelf
+        apn = schema.APN_SUBSCRIPTIONS
+        return Select([apn.MODIFIED, apn.SUBSCRIBER_GUID], From=apn,
+                Where=(
+                    apn.TOKEN == Parameter("token")).And(
+                    apn.RESOURCE_KEY == Parameter("resourceKey")
+                )
+            )
+
+    @inlineCallbacks
+    def addAPNSubscription(self, token, key, timestamp, subscriber):
+        """
+        Add an Apple Push Notification subscription
+        """
+        # Select
+        # if not there, insert
+        #   if insert fails, pass (since someone just added it)
+        #   if it succeeds, return
+        # if there, update
+        #   if update fails, insert
+        #   if update succeeds, return
+
+
+        row = yield self._selectAPNSubscriptionQuery.on(self,
+            token=token, resourceKey=key)
+        if not row: # Subscription does not yet exist
+            try:
+                yield self._insertAPNSubscriptionQuery.on(self,
+                    token=token, resourceKey=key, modified=timestamp,
+                    subscriber=subscriber)
+            except Exception:
+                # Subscription may have been added by someone else, which is fine
+                pass
+
+        else: # Subscription exists, so update with new timestamp
+            try:
+                yield self._updateAPNSubscriptionQuery.on(self,
+                    token=token, resourceKey=key, modified=timestamp)
+            except Exception:
+                # Subscription may have been added by someone else, which is fine
+                pass
+
+
+    @classproperty
+    def _removeAPNSubscriptionQuery(cls): #@NoSelf
+        apn = schema.APN_SUBSCRIPTIONS
+        return Delete(From=apn,
+                      Where=(apn.TOKEN == Parameter("token")).And(
+                          apn.RESOURCE_KEY == Parameter("resourceKey")))
+
+    def removeAPNSubscription(self, token, key):
+        """
+        Remove an Apple Push Notification subscription
+        """
+        return self._removeAPNSubscriptionQuery.on(self,
+            token=token, resourceKey=key)
+
+    @classproperty
+    def _apnSubscriptionsByTokenQuery(cls): #@NoSelf
+        """
+        Look up Apple Push Notification subscriptions by device token
+        """
+        apn = schema.APN_SUBSCRIPTIONS
+        return Select([apn.RESOURCE_KEY, apn.MODIFIED, apn.SUBSCRIBER_GUID],
+                      From=apn, Where=apn.TOKEN == Parameter("token"))
+
+    def apnSubscriptionsByToken(self, token):
+        return self._apnSubscriptionsByTokenQuery.on(self, token=token)
+
+    @classproperty
+    def _apnSubscriptionsByKeyQuery(cls): #@NoSelf
+        """
+        Look up Apple Push Notification subscriptions by key
+        """
+        apn = schema.APN_SUBSCRIPTIONS
+        return Select([apn.TOKEN, apn.SUBSCRIBER_GUID],
+                      From=apn, Where=apn.RESOURCE_KEY == Parameter("resourceKey"))
+
+    def apnSubscriptionsByKey(self, key):
+        return self._apnSubscriptionsByKeyQuery.on(self, resourceKey=key)
+
     def postCommit(self, operation):
         """
         Run things after C{commit}.

Modified: CalendarServer/branches/users/sagen/applepush/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/branches/users/sagen/applepush/txdav/common/datastore/sql_schema/current.sql	2011-10-04 22:11:00 UTC (rev 8144)
+++ CalendarServer/branches/users/sagen/applepush/txdav/common/datastore/sql_schema/current.sql	2011-10-05 00:06:43 UTC (rev 8145)
@@ -435,6 +435,24 @@
 );
 
 
+-------------------------------------------
+-- Apple Push Notification Subscriptions --
+-------------------------------------------
+
+create table APN_SUBSCRIPTIONS (
+  TOKEN                         varchar(255) not null,
+  RESOURCE_KEY                  varchar(255) not null,
+  MODIFIED                      integer not null,
+  SUBSCRIBER_GUID               varchar(255) not null,
+  unique(TOKEN, RESOURCE_KEY) -- implicit index
+);
+
+create index APN_SUBSCRIPTIONS_TOKENS
+   on APN_SUBSCRIPTIONS(TOKEN);
+create index APN_SUBSCRIPTIONS_RESOURCE_KEY
+   on APN_SUBSCRIPTIONS(RESOURCE_KEY);
+
+
 --------------------
 -- Schema Version --
 --------------------
@@ -444,5 +462,5 @@
   VALUE                         varchar(255)
 );
 
-insert into CALENDARSERVER values ('VERSION', '5');
+insert into CALENDARSERVER values ('VERSION', '6');
 

Added: CalendarServer/branches/users/sagen/applepush/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_5_to_6.sql
===================================================================
--- CalendarServer/branches/users/sagen/applepush/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_5_to_6.sql	                        (rev 0)
+++ CalendarServer/branches/users/sagen/applepush/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_5_to_6.sql	2011-10-05 00:06:43 UTC (rev 8145)
@@ -0,0 +1,40 @@
+----
+-- Copyright (c) 2011 Apple Inc. All rights reserved.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+----
+
+-------------------------------------------------
+-- Upgrade database schema from VERSION 5 to 6 --
+-------------------------------------------------
+
+---------------------------------------------------------
+-- New table for Apple Push Notification Subscriptions --
+---------------------------------------------------------
+
+create table APN_SUBSCRIPTIONS (
+  TOKEN                         varchar(255) not null,
+  RESOURCE_KEY                  varchar(255) not null,
+  MODIFIED                      integer not null,
+  SUBSCRIBER_GUID               varchar(255) not null,
+  unique(TOKEN, RESOURCE_KEY) -- implicit index
+);
+
+create index APN_SUBSCRIPTIONS_TOKENS
+  on APN_SUBSCRIPTIONS(TOKEN);
+create index APN_SUBSCRIPTIONS_RESOURCE_KEY
+  on APN_SUBSCRIPTIONS(RESOURCE_KEY);
+
+-- Now update the version
+update CALENDARSERVER set VALUE = '6' where NAME = 'VERSION';
+

Added: CalendarServer/branches/users/sagen/applepush/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_5_to_6.sql
===================================================================
--- CalendarServer/branches/users/sagen/applepush/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_5_to_6.sql	                        (rev 0)
+++ CalendarServer/branches/users/sagen/applepush/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_5_to_6.sql	2011-10-05 00:06:43 UTC (rev 8145)
@@ -0,0 +1,40 @@
+----
+-- Copyright (c) 2011 Apple Inc. All rights reserved.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+----
+
+-------------------------------------------------
+-- Upgrade database schema from VERSION 5 to 6 --
+-------------------------------------------------
+
+---------------------------------------------------------
+-- New table for Apple Push Notification Subscriptions --
+---------------------------------------------------------
+
+create table APN_SUBSCRIPTIONS (
+  TOKEN                         varchar(255) not null,
+  RESOURCE_KEY                  varchar(255) not null,
+  MODIFIED                      integer not null,
+  SUBSCRIBER_GUID               varchar(255) not null,
+  unique(TOKEN, RESOURCE_KEY) -- implicit index
+);
+
+create index APN_SUBSCRIPTIONS_TOKENS
+  on APN_SUBSCRIPTIONS(TOKEN);
+create index APN_SUBSCRIPTIONS_RESOURCE_KEY
+  on APN_SUBSCRIPTIONS(RESOURCE_KEY);
+
+-- Now update the version
+update CALENDARSERVER set VALUE = '6' where NAME = 'VERSION';
+

Modified: CalendarServer/branches/users/sagen/applepush/txdav/common/icommondatastore.py
===================================================================
--- CalendarServer/branches/users/sagen/applepush/txdav/common/icommondatastore.py	2011-10-04 22:11:00 UTC (rev 8144)
+++ CalendarServer/branches/users/sagen/applepush/txdav/common/icommondatastore.py	2011-10-05 00:06:43 UTC (rev 8145)
@@ -147,8 +147,22 @@
             notification collection exists.
         """
 
+    def addAPNSubscription(token, key, timestamp, subscriber):
+        """
+        """
 
+    def removeAPNSubscription(token, key):
+        """
+        """
 
+    def apnSubscriptionsByToken(token):
+        """
+        """
+
+    def apnSubscriptionsByKey(key):
+        """
+        """
+
 class IShareableCollection(Interface):
     """
     A collection resource which may be shared.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20111004/5667d4c8/attachment-0001.html>


More information about the calendarserver-changes mailing list