[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