| 1 | ## |
|---|
| 2 | # Copyright (c) 2005-2007 Apple Inc. All rights reserved. |
|---|
| 3 | # |
|---|
| 4 | # Licensed under the Apache License, Version 2.0 (the "License"); |
|---|
| 5 | # you may not use this file except in compliance with the License. |
|---|
| 6 | # You may obtain a copy of the License at |
|---|
| 7 | # |
|---|
| 8 | # http://www.apache.org/licenses/LICENSE-2.0 |
|---|
| 9 | # |
|---|
| 10 | # Unless required by applicable law or agreed to in writing, software |
|---|
| 11 | # distributed under the License is distributed on an "AS IS" BASIS, |
|---|
| 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|---|
| 13 | # See the License for the specific language governing permissions and |
|---|
| 14 | # limitations under the License. |
|---|
| 15 | # |
|---|
| 16 | # DRI: David Reid, dreid@apple.com |
|---|
| 17 | ## |
|---|
| 18 | |
|---|
| 19 | import os |
|---|
| 20 | import copy |
|---|
| 21 | |
|---|
| 22 | from twisted.python import log |
|---|
| 23 | |
|---|
| 24 | from twistedcaldav.py.plistlib import readPlist |
|---|
| 25 | |
|---|
| 26 | defaultConfigFile = "/etc/caldavd/caldavd.plist" |
|---|
| 27 | |
|---|
| 28 | serviceDefaultParams = { |
|---|
| 29 | "twistedcaldav.directory.xmlfile.XMLDirectoryService": { |
|---|
| 30 | "xmlFile": "/etc/caldavd/accounts.xml", |
|---|
| 31 | }, |
|---|
| 32 | "twistedcaldav.directory.appleopendirectory.OpenDirectoryService": { |
|---|
| 33 | "node": "/Search", |
|---|
| 34 | "requireComputerRecord": True, |
|---|
| 35 | }, |
|---|
| 36 | } |
|---|
| 37 | |
|---|
| 38 | defaultConfig = { |
|---|
| 39 | # |
|---|
| 40 | # Public network address information |
|---|
| 41 | # |
|---|
| 42 | # This is the server's public network address, which is provided to |
|---|
| 43 | # clients in URLs and the like. It may or may not be the network |
|---|
| 44 | # address that the server is listening to directly, though it is by |
|---|
| 45 | # default. For example, it may be the address of a load balancer or |
|---|
| 46 | # proxy which forwards connections to the server. |
|---|
| 47 | # |
|---|
| 48 | "ServerHostName": "localhost", # Network host name. |
|---|
| 49 | "HTTPPort": -1, # HTTP port (None to disable HTTP) |
|---|
| 50 | "SSLPort" : -1, # SSL port (None to disable HTTPS) |
|---|
| 51 | |
|---|
| 52 | # Note: we'd use None above, but that confuses the command-line parser. |
|---|
| 53 | |
|---|
| 54 | # |
|---|
| 55 | # Network address configuration information |
|---|
| 56 | # |
|---|
| 57 | # This configures the actual network address that the server binds to. |
|---|
| 58 | # |
|---|
| 59 | "BindAddresses": [], # List of IP addresses to bind to [empty = all] |
|---|
| 60 | "BindHTTPPorts": [], # List of port numbers to bind to for HTTP [empty = same as "Port"] |
|---|
| 61 | "BindSSLPorts" : [], # List of port numbers to bind to for SSL [empty = same as "SSLPort"] |
|---|
| 62 | |
|---|
| 63 | # |
|---|
| 64 | # Data store |
|---|
| 65 | # |
|---|
| 66 | "DocumentRoot": "/Library/CalendarServer/Documents", |
|---|
| 67 | "UserQuota" : 104857600, # User quota (in bytes) |
|---|
| 68 | "MaximumAttachmentSize": 1048576, # Attachment size limit (in bytes) |
|---|
| 69 | |
|---|
| 70 | # |
|---|
| 71 | # Directory service |
|---|
| 72 | # |
|---|
| 73 | # A directory service provides information about principals (eg. |
|---|
| 74 | # users, groups, locations and resources) to the server. |
|---|
| 75 | # |
|---|
| 76 | "DirectoryService": { |
|---|
| 77 | "type": "twistedcaldav.directory.xmlfile.XMLDirectoryService", |
|---|
| 78 | "params": serviceDefaultParams["twistedcaldav.directory.xmlfile.XMLDirectoryService"], |
|---|
| 79 | }, |
|---|
| 80 | |
|---|
| 81 | # |
|---|
| 82 | # Special principals |
|---|
| 83 | # |
|---|
| 84 | "AdminPrincipals": [], # Principals with "DAV:all" access (relative URLs) |
|---|
| 85 | "SudoersFile": "/etc/caldavd/sudoers.plist", # Principals that can pose as other principals |
|---|
| 86 | "EnableProxyPrincipals": True, # Create "proxy access" principals |
|---|
| 87 | |
|---|
| 88 | # |
|---|
| 89 | # Authentication |
|---|
| 90 | # |
|---|
| 91 | "Authentication": { |
|---|
| 92 | "Basic" : { "Enabled": False }, # Clear text; best avoided |
|---|
| 93 | "Digest" : { # Digest challenge/response |
|---|
| 94 | "Enabled": True, |
|---|
| 95 | "Algorithm": "md5", |
|---|
| 96 | "Qop": "", |
|---|
| 97 | "Secret": "", |
|---|
| 98 | }, |
|---|
| 99 | "Kerberos": { # Kerberos/SPNEGO |
|---|
| 100 | "Enabled": False, |
|---|
| 101 | "ServicePrincipal": '' |
|---|
| 102 | }, |
|---|
| 103 | }, |
|---|
| 104 | |
|---|
| 105 | # |
|---|
| 106 | # Logging |
|---|
| 107 | # |
|---|
| 108 | "Verbose": False, |
|---|
| 109 | "AccessLogFile" : "/var/log/caldavd/access.log", # Apache-style access log |
|---|
| 110 | "ErrorLogFile" : "/var/log/caldavd/error.log", # Server activity log |
|---|
| 111 | "ServerStatsFile": "/Library/CalendarServer/Documents/stats.plist", |
|---|
| 112 | "PIDFile" : "/var/run/caldavd.pid", |
|---|
| 113 | "RotateAccessLog": False, |
|---|
| 114 | |
|---|
| 115 | # |
|---|
| 116 | # SSL/TLS |
|---|
| 117 | # |
|---|
| 118 | "SSLCertificate": "/etc/certificates/Default.crt", # Public key |
|---|
| 119 | "SSLPrivateKey": "/etc/certificates/Default.key", # Private key |
|---|
| 120 | |
|---|
| 121 | # |
|---|
| 122 | # Process management |
|---|
| 123 | # |
|---|
| 124 | |
|---|
| 125 | # Username and Groupname to drop privileges to, if empty privileges will |
|---|
| 126 | # not be dropped. |
|---|
| 127 | |
|---|
| 128 | "UserName": "", |
|---|
| 129 | "GroupName": "", |
|---|
| 130 | "ProcessType": "Combined", |
|---|
| 131 | "MultiProcess": { |
|---|
| 132 | "ProcessCount": 0, |
|---|
| 133 | "LoadBalancer": { |
|---|
| 134 | "Enabled": True, |
|---|
| 135 | "Scheduler": "LeastConnections", |
|---|
| 136 | }, |
|---|
| 137 | }, |
|---|
| 138 | |
|---|
| 139 | # |
|---|
| 140 | # Service ACLs |
|---|
| 141 | # |
|---|
| 142 | "EnableSACLs": False, |
|---|
| 143 | |
|---|
| 144 | # |
|---|
| 145 | # Non-standard CalDAV extensions |
|---|
| 146 | # |
|---|
| 147 | "EnableDropBox" : False, # Calendar Drop Box |
|---|
| 148 | "EnableNotifications": False, # Drop Box Notifications |
|---|
| 149 | |
|---|
| 150 | # |
|---|
| 151 | # Implementation details |
|---|
| 152 | # |
|---|
| 153 | # The following are specific to how the server is built, and useful |
|---|
| 154 | # for development, but shouldn't be needed by users. |
|---|
| 155 | # |
|---|
| 156 | |
|---|
| 157 | # Twisted |
|---|
| 158 | "Twisted": { |
|---|
| 159 | "twistd": "/usr/share/caldavd/bin/twistd", |
|---|
| 160 | }, |
|---|
| 161 | |
|---|
| 162 | # Python Director |
|---|
| 163 | "PythonDirector": { |
|---|
| 164 | "pydir": "/usr/share/caldavd/bin/pydir.py", |
|---|
| 165 | "ConfigFile": "/etc/caldavd/pydir.xml", |
|---|
| 166 | "ControlSocket": "/var/run/caldavd-pydir.sock", |
|---|
| 167 | }, |
|---|
| 168 | |
|---|
| 169 | # Umask |
|---|
| 170 | "umask": 0027, |
|---|
| 171 | |
|---|
| 172 | # A unix socket used for communication between the child and master |
|---|
| 173 | # processes. |
|---|
| 174 | "ControlSocket": "/var/run/caldavd.sock", |
|---|
| 175 | |
|---|
| 176 | # A secret key (SHA-1 hash of random string) that is used for internal |
|---|
| 177 | # crypto operations and shared by multiple server processes |
|---|
| 178 | "SharedSecret": "", |
|---|
| 179 | } |
|---|
| 180 | |
|---|
| 181 | class Config (object): |
|---|
| 182 | def __init__(self, defaults): |
|---|
| 183 | self.setDefaults(defaults) |
|---|
| 184 | self._data = copy.deepcopy(self._defaults) |
|---|
| 185 | self._configFile = None |
|---|
| 186 | |
|---|
| 187 | def __str__(self): |
|---|
| 188 | return str(self._data) |
|---|
| 189 | |
|---|
| 190 | def update(self, items): |
|---|
| 191 | dsType = items.get("DirectoryService", {}).get("type", None) |
|---|
| 192 | if dsType is None: |
|---|
| 193 | dsType = self._data["DirectoryService"]["type"] |
|---|
| 194 | else: |
|---|
| 195 | if dsType == self._data["DirectoryService"]["type"]: |
|---|
| 196 | oldParams = self._data["DirectoryService"]["params"] |
|---|
| 197 | newParams = items["DirectoryService"].get("params", {}) |
|---|
| 198 | _mergeData(oldParams, newParams) |
|---|
| 199 | else: |
|---|
| 200 | if dsType in serviceDefaultParams: |
|---|
| 201 | self._data["DirectoryService"]["params"] = copy.deepcopy(serviceDefaultParams[dsType]) |
|---|
| 202 | |
|---|
| 203 | for param in items.get("DirectoryService", {}).get("params", {}): |
|---|
| 204 | if param not in serviceDefaultParams[dsType]: |
|---|
| 205 | raise ConfigurationError("Parameter %s is not supported by service %s" % (param, dsType)) |
|---|
| 206 | |
|---|
| 207 | for param in tuple(self._data["DirectoryService"]["params"]): |
|---|
| 208 | if param not in serviceDefaultParams[self._data["DirectoryService"]["type"]]: |
|---|
| 209 | del self._data["DirectoryService"]["params"][param] |
|---|
| 210 | else: |
|---|
| 211 | self._data["DirectoryService"]["params"] = {} |
|---|
| 212 | |
|---|
| 213 | _mergeData(self._data, items) |
|---|
| 214 | self.updateServerCapabilities() |
|---|
| 215 | |
|---|
| 216 | def updateServerCapabilities(self): |
|---|
| 217 | """ |
|---|
| 218 | Change server capabilities based on the current config parameters. |
|---|
| 219 | Here are the "features" in the config that need special treatment: |
|---|
| 220 | |
|---|
| 221 | EnableDropBox |
|---|
| 222 | EnableNotifications |
|---|
| 223 | """ |
|---|
| 224 | |
|---|
| 225 | from twistedcaldav.resource import CalendarPrincipalResource |
|---|
| 226 | CalendarPrincipalResource.enableDropBox(self.EnableDropBox) |
|---|
| 227 | CalendarPrincipalResource.enableNotifications(self.EnableNotifications) |
|---|
| 228 | |
|---|
| 229 | def updateDefaults(self, items): |
|---|
| 230 | _mergeData(self._defaults, items) |
|---|
| 231 | self.update(items) |
|---|
| 232 | |
|---|
| 233 | def setDefaults(self, defaults): |
|---|
| 234 | self._defaults = copy.deepcopy(defaults) |
|---|
| 235 | |
|---|
| 236 | def __setattr__(self, attr, value): |
|---|
| 237 | if '_data' in self.__dict__ and attr in self.__dict__['_data']: |
|---|
| 238 | self._data[attr] = value |
|---|
| 239 | else: |
|---|
| 240 | self.__dict__[attr] = value |
|---|
| 241 | |
|---|
| 242 | def __getattr__(self, attr): |
|---|
| 243 | if attr in self._data: |
|---|
| 244 | return self._data[attr] |
|---|
| 245 | |
|---|
| 246 | raise AttributeError(attr) |
|---|
| 247 | |
|---|
| 248 | def reload(self): |
|---|
| 249 | self._data = copy.deepcopy(self._defaults) |
|---|
| 250 | self.loadConfig(self._configFile) |
|---|
| 251 | |
|---|
| 252 | def loadConfig(self, configFile): |
|---|
| 253 | self._configFile = configFile |
|---|
| 254 | |
|---|
| 255 | if configFile and os.path.exists(configFile): |
|---|
| 256 | configDict = readPlist(configFile) |
|---|
| 257 | configDict = _cleanup(configDict) |
|---|
| 258 | self.update(configDict) |
|---|
| 259 | |
|---|
| 260 | def _mergeData(oldData, newData): |
|---|
| 261 | for key, value in newData.iteritems(): |
|---|
| 262 | if isinstance(value, (dict,)): |
|---|
| 263 | if key in oldData: |
|---|
| 264 | assert isinstance(oldData[key], (dict,)) |
|---|
| 265 | else: |
|---|
| 266 | oldData[key] = {} |
|---|
| 267 | _mergeData(oldData[key], value) |
|---|
| 268 | else: |
|---|
| 269 | oldData[key] = value |
|---|
| 270 | |
|---|
| 271 | def _cleanup(configDict): |
|---|
| 272 | cleanDict = copy.deepcopy(configDict) |
|---|
| 273 | |
|---|
| 274 | def deprecated(oldKey, newKey): |
|---|
| 275 | log.err("Configuration option %r is deprecated in favor of %r." % (oldKey, newKey)) |
|---|
| 276 | |
|---|
| 277 | def renamed(oldKey, newKey): |
|---|
| 278 | deprecated(oldKey, newKey) |
|---|
| 279 | cleanDict[newKey] = configDict[oldKey] |
|---|
| 280 | del cleanDict[oldKey] |
|---|
| 281 | |
|---|
| 282 | renamedOptions = { |
|---|
| 283 | "BindAddress" : "BindAddresses", |
|---|
| 284 | "ServerLogFile" : "AccessLogFile", |
|---|
| 285 | "MaximumAttachmentSizeBytes" : "MaximumAttachmentSize", |
|---|
| 286 | "UserQuotaBytes" : "UserQuota", |
|---|
| 287 | "DropBoxEnabled" : "EnableDropBox", |
|---|
| 288 | "NotificationsEnabled" : "EnableNotifications", |
|---|
| 289 | "CalendarUserProxyEnabled" : "EnableProxyPrincipals", |
|---|
| 290 | "SACLEnable" : "EnableSACLs", |
|---|
| 291 | "ServerType" : "ProcessType", |
|---|
| 292 | } |
|---|
| 293 | |
|---|
| 294 | for key in configDict: |
|---|
| 295 | if key in defaultConfig: |
|---|
| 296 | continue |
|---|
| 297 | |
|---|
| 298 | if key == "SSLOnly": |
|---|
| 299 | deprecated(key, "HTTPPort") |
|---|
| 300 | if configDict["SSLOnly"]: |
|---|
| 301 | cleanDict["HTTPPort"] = None |
|---|
| 302 | del cleanDict["SSLOnly"] |
|---|
| 303 | |
|---|
| 304 | elif key == "SSLEnable": |
|---|
| 305 | deprecated(key, "SSLPort") |
|---|
| 306 | if not configDict["SSLEnable"]: |
|---|
| 307 | cleanDict["SSLPort"] = None |
|---|
| 308 | del cleanDict["SSLEnable"] |
|---|
| 309 | |
|---|
| 310 | elif key == "Port": |
|---|
| 311 | deprecated(key, "HTTPPort") |
|---|
| 312 | if not configDict.get("SSLOnly", False): |
|---|
| 313 | cleanDict["HTTPPort"] = cleanDict["Port"] |
|---|
| 314 | del cleanDict["Port"] |
|---|
| 315 | |
|---|
| 316 | elif key == "twistdLocation": |
|---|
| 317 | deprecated(key, "Twisted -> twistd") |
|---|
| 318 | if "Twisted" not in cleanDict: |
|---|
| 319 | cleanDict["Twisted"] = {} |
|---|
| 320 | cleanDict["Twisted"]["twistd"] = cleanDict["twistdLocation"] |
|---|
| 321 | del cleanDict["twistdLocation"] |
|---|
| 322 | |
|---|
| 323 | elif key == "pydirLocation": |
|---|
| 324 | deprecated(key, "PythonDirector -> pydir") |
|---|
| 325 | if "PythonDirector" not in cleanDict: |
|---|
| 326 | cleanDict["PythonDirector"] = {} |
|---|
| 327 | cleanDict["PythonDirector"]["pydir"] = cleanDict["pydirLocation"] |
|---|
| 328 | del cleanDict["pydirLocation"] |
|---|
| 329 | |
|---|
| 330 | elif key == "pydirConfig": |
|---|
| 331 | deprecated(key, "PythonDirector -> pydir") |
|---|
| 332 | if "PythonDirector" not in cleanDict: |
|---|
| 333 | cleanDict["PythonDirector"] = {} |
|---|
| 334 | cleanDict["PythonDirector"]["ConfigFile"] = cleanDict["pydirConfig"] |
|---|
| 335 | del cleanDict["pydirConfig"] |
|---|
| 336 | |
|---|
| 337 | elif key in renamedOptions: |
|---|
| 338 | renamed(key, renamedOptions[key]) |
|---|
| 339 | |
|---|
| 340 | elif key == "RunStandalone": |
|---|
| 341 | log.err("Ignoring obsolete configuration option: %s" % (key,)) |
|---|
| 342 | del cleanDict[key] |
|---|
| 343 | |
|---|
| 344 | else: |
|---|
| 345 | log.err("Ignoring unknown configuration option: %s" % (key,)) |
|---|
| 346 | del cleanDict[key] |
|---|
| 347 | |
|---|
| 348 | return cleanDict |
|---|
| 349 | |
|---|
| 350 | class ConfigurationError (RuntimeError): |
|---|
| 351 | """ |
|---|
| 352 | Invalid server configuration. |
|---|
| 353 | """ |
|---|
| 354 | |
|---|
| 355 | config = Config(defaultConfig) |
|---|
| 356 | |
|---|
| 357 | def parseConfig(configFile): |
|---|
| 358 | config.loadConfig(configFile) |
|---|