OwlCyberSecurity - MANAGER
Edit File: timezonestdservice.py
## # Copyright (c) 2011-2017 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. ## """ Timezone service resource and operations. This is based on http://tools.ietf.org/html/draft-ietf-tzdist-service which is the IETF proposal for a standard timezone service. """ __all__ = [ "TimezoneStdServiceResource", ] from twext.python.log import Logger from txweb2 import responsecode from txweb2.dav.method.propfind import http_PROPFIND from txweb2.dav.noneprops import NonePropertyStore from txweb2.dav.util import joinURL from txweb2.http import HTTPError, JSONResponse, StatusResponse from txweb2.http import Response from txweb2.http_headers import MimeType from txweb2.stream import MemoryStream from txdav.xml import element as davxml from twisted.internet.defer import succeed, inlineCallbacks, returnValue, \ DeferredList from twistedcaldav import xmlutil from twistedcaldav.client.geturl import getURL from twistedcaldav.config import config from twistedcaldav.extensions import DAVResource, \ DAVResourceWithoutChildrenMixin from twistedcaldav.ical import tzexpandlocal from twistedcaldav.resource import ReadOnlyNoCopyResourceMixIn from twistedcaldav.timezones import TimezoneException, TimezoneCache, readVTZ, \ addVTZ from twistedcaldav.util import bestAcceptType from twistedcaldav.xmlutil import addSubElement from pycalendar.icalendar.calendar import Calendar from pycalendar.datetime import DateTime from pycalendar.exceptions import InvalidData import hashlib import itertools import json import os import urllib log = Logger() class TimezoneStdServiceResource (ReadOnlyNoCopyResourceMixIn, DAVResourceWithoutChildrenMixin, DAVResource): """ Timezone Service resource. Strictly speaking this is an HTTP-only resource no WebDAV support needed. Extends L{DAVResource} to provide timezone service functionality. """ def __init__(self, parent): """ @param parent: the parent resource of this one. """ assert parent is not None DAVResource.__init__(self, principalCollections=parent.principalCollections()) self.parent = parent self.expandcache = {} self.primary = True self.info_source = None if config.TimezoneService.Mode == "primary": log.info("Using primary Timezone Service") self._initPrimaryService() elif config.TimezoneService.Mode == "secondary": log.info("Using secondary Timezone Service") self._initSecondaryService() else: raise ValueError("Invalid TimezoneService mode: %s" % (config.TimezoneService.Mode,)) self.formats = [] self.formats.append("text/calendar") if config.EnableJSONData: self.formats.append("application/calendar+json") self.formats.append("text/plain") def _initPrimaryService(self): tzpath = TimezoneCache.getDBPath() xmlfile = os.path.join(tzpath, "timezones.xml") self.timezones = PrimaryTimezoneDatabase(tzpath, xmlfile) if not os.path.exists(xmlfile): self.timezones.createNewDatabase() else: self.timezones.readDatabase() self.info_source = TimezoneCache.version def _initSecondaryService(self): # Must have writeable paths tzpath = TimezoneCache.getDBPath() xmlfile = config.TimezoneService.XMLInfoPath if not xmlfile: xmlfile = os.path.join(tzpath, "timezones.xml") self.timezones = SecondaryTimezoneDatabase(tzpath, xmlfile, None) try: self.timezones.readDatabase() except: pass self.info_source = "Secondary" self.primary = False def onStartup(self): return self.timezones.onStartup() def deadProperties(self): if not hasattr(self, "_dead_properties"): self._dead_properties = NonePropertyStore(self) return self._dead_properties def etag(self): return succeed(None) def checkPreconditions(self, request): return None def checkPrivileges(self, request, privileges, recurse=False, principal=None, inherited_aces=None): return succeed(None) def defaultAccessControlList(self): return succeed( davxml.ACL( # DAV:Read for all principals (includes anonymous) davxml.ACE( davxml.Principal(davxml.All()), davxml.Grant( davxml.Privilege(davxml.Read()), ), davxml.Protected(), ), ) ) def contentType(self): return MimeType.fromString("text/html; charset=utf-8") def resourceType(self): return None def isCollection(self): return False def isCalendarCollection(self): return False def isPseudoCalendarCollection(self): return False def render(self, request): output = """<html> <head> <title>Timezone Standard Service Resource</title> </head> <body> <h1>Timezone Standard Service Resource.</h1> </body </html>""" response = Response(200, {}, output) response.headers.setHeader("content-type", MimeType("text", "html")) return response http_PROPFIND = http_PROPFIND def problemReport(self, code, description, status): raise HTTPError(JSONResponse( status, { "type": "https://datatracker.ietf.org/doc/draft-ietf-tzdist-service/", "error-code": code, "title": description, "status": status, }, contentType="application/problem+json", pretty=config.TimezoneService.PrettyPrintJSON, )) def http_POST(self, request): raise HTTPError(StatusResponse(responsecode.NOT_ALLOWED, "Method not allowed")) @inlineCallbacks def http_GET(self, request): """ The timezone service GET method. """ yield self.authorize(request, (davxml.Read(),)) urlbits = map(urllib.unquote, request.path.strip("/").split("/")[1:]) child = urlbits[0] if len(urlbits) > 0 else "" if not child: if len(request.args) != 0: self.problemReport("invalid-action", "Invalid request-URI query parameters", responsecode.BAD_REQUEST) # Do normal GET behavior returnValue(self.render(request)) childResponder = { "capabilities": self.childCapabilities, "zones": self.childZones, "observances": self.childObservances, }.get(child, None) if childResponder is None: self.problemReport("invalid-action", "Invalid action", responsecode.BAD_REQUEST) returnValue(childResponder(request, urlbits)) def childCapabilities(self, request, urlbits): """ Request on {/service-prefix}/capabilities. """ if len(urlbits) != 1: self.problemReport("invalid-action", "Invalid path segment", responsecode.BAD_REQUEST) return self.actionCapabilities(request) def childZones(self, request, urlbits): """ Request on {/service-prefix}/zones. """ if len(urlbits) == 2: return self.actionGet(request, urlbits[1]) elif len(urlbits) == 1: pattern = request.args.get("pattern", ()) if len(pattern) == 0: return self.actionList(request) else: return self.actionFind(request) else: self.problemReport("invalid-action", "Invalid path segment", responsecode.BAD_REQUEST) def childObservances(self, request, urlbits): """ Request on {/service-prefix}/observances. """ if len(urlbits) == 2: return self.actionExpand(request, urlbits[1]) elif len(urlbits) == 1: self.problemReport("invalid-action", "Missing {tzid} path segment", responsecode.BAD_REQUEST) else: self.problemReport("invalid-action", "Invalid path segment", responsecode.BAD_REQUEST) def actionCapabilities(self, request): """ Return the capabilities of this server. """ if len(request.args) != 0: self.problemReport("invalid-action", "Invalid request-URI query parameters", responsecode.BAD_REQUEST) urlbase = request.path.rsplit("/", 1)[0] result = { "version": "1", "info": { "primary-source" if self.primary else "secondary_source": self.info_source, "formats": self.formats, "contacts": [], }, "actions": [ { "name": "capabilities", "uri-template": joinURL(urlbase, "capabilities"), "parameters": [], }, { "name": "list", "uri-template": joinURL(urlbase, "zones{?changedsince}"), "parameters": [ {"name": "changedsince", "required": False, "multi": False, }, ], }, { "name": "get", "uri-template": joinURL(urlbase, "zones{/tzid}{?start,end}"), "parameters": [ {"name": "start", "required": False, "multi": False}, {"name": "stop", "required": False, "multi": False, }, ], }, { "name": "expand", "uri-template": joinURL(urlbase, "zones{/tzid}/observances{?start,end}"), "parameters": [ {"name": "start", "required": True, "multi": False, }, {"name": "end", "required": True, "multi": False, }, ], }, { "name": "find", "uri-template": joinURL(urlbase, "zones{?pattern}"), "parameters": [ {"name": "pattern", "required": True, "multi": False, }, ], }, ] } return JSONResponse(responsecode.OK, result, pretty=config.TimezoneService.PrettyPrintJSON) def actionList(self, request): """ Return a list of all timezones known to the server. """ if set(request.args.keys()) - set(("changedsince",)): self.problemReport("invalid-action", "Invalid request-URI query parameters", responsecode.BAD_REQUEST) changedsince = request.args.get("changedsince", ()) if len(changedsince) > 1: self.problemReport("invalid-changedsince", "Too many changedsince request-URI query parameters", responsecode.BAD_REQUEST) if len(changedsince) == 1: # Validate a date-time stamp changedsince = changedsince[0] try: dt = DateTime.parseText(changedsince, fullISO=True) except ValueError: self.problemReport("invalid-changedsince", "Invalid changedsince request-URI query parameter value", responsecode.BAD_REQUEST) if not dt.utc(): self.problemReport("invalid-changedsince", "Invalid changedsince request-URI query parameter value - not UTC", responsecode.BAD_REQUEST) timezones = [] for tz in self.timezones.listTimezones(changedsince): timezones.append({ "tzid": tz.tzid, "last-modified": tz.dtstamp, "aliases": tz.aliases, }) result = { "dtstamp": self.timezones.dtstamp, "timezones": timezones, } return JSONResponse(responsecode.OK, result, pretty=config.TimezoneService.PrettyPrintJSON) def actionGet(self, request, tzid): """ Return the specified timezone data. """ if set(request.args.keys()) - set(("start", "end",)): self.problemReport("invalid-action", "Invalid request-URI query parameters", responsecode.BAD_REQUEST) accepted_type = bestAcceptType(request.headers.getHeader("accept"), self.formats) if accepted_type is None: self.problemReport("invalid-format", "Accept header does not match available media types", responsecode.NOT_ACCEPTABLE) calendar = self.timezones.getTimezone(tzid) if calendar is None: self.problemReport("tzid-not-found", "Time zone identifier not found", responsecode.NOT_FOUND) tzdata = calendar.getText(format=accepted_type if accepted_type != "text/plain" else None) response = Response() response.stream = MemoryStream(tzdata) response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (accepted_type,))) return response def actionExpand(self, request, tzid): """ Expand a timezone within specified start/end dates. """ if set(request.args.keys()) - set(("start", "end",)): self.problemReport("invalid-action", "Invalid request-URI query parameters", responsecode.BAD_REQUEST) start = request.args.get("start", ()) if len(start) == 0: self.problemReport("invalid-start", "Missing start request-URI query parameter", responsecode.BAD_REQUEST) if len(start) > 1: self.problemReport("invalid-start", "Too many start request-URI query parameters", responsecode.BAD_REQUEST) elif len(start) == 1: try: if len(start[0]) != 20: raise ValueError() start = DateTime.parseText(start[0], fullISO=True) except ValueError: self.problemReport("invalid-start", "Invalid start request-URI query parameter value", responsecode.BAD_REQUEST) end = request.args.get("end", ()) if len(end) == 0: self.problemReport("invalid-end", "Missing end request-URI query parameter", responsecode.BAD_REQUEST) if len(end) > 1: self.problemReport("invalid-end", "Too many end request-URI query parameters", responsecode.BAD_REQUEST) elif len(end) == 1: try: if len(end[0]) != 20: raise ValueError() end = DateTime.parseText(end[0], fullISO=True) except ValueError: self.problemReport("invalid-end", "Invalid end request-URI query parameter value", responsecode.BAD_REQUEST) if end <= start: self.problemReport("invalid-end", "Invalid end request-URI query parameter value - earlier than start", responsecode.BAD_REQUEST) tzdata = self.timezones.getTimezone(tzid) if tzdata is None: self.problemReport("tzid-not-found", "Time zone identifier not found", responsecode.NOT_FOUND) # Now do the expansion (but use a cache to avoid re-calculating TZs) observances = self.expandcache.get((tzid, start, end), None) if observances is None: observances = tzexpandlocal(tzdata, start, end, utc_onset=True) self.expandcache[(tzid, start, end)] = observances # Turn into JSON result = { "dtstamp": self.timezones.dtstamp, "tzid": tzid, "observances": [ { "name": name, "onset": onset.getXMLText(), "utc-offset-from": utc_offset_from, "utc-offset-to": utc_offset_to, } for onset, utc_offset_from, utc_offset_to, name in observances ], } return JSONResponse(responsecode.OK, result, pretty=config.TimezoneService.PrettyPrintJSON) def actionFind(self, request): """ Return a list of all timezones matching a pattern. """ if set(request.args.keys()) - set(("pattern",)): self.problemReport("invalid-action", "Invalid request-URI query parameters", responsecode.BAD_REQUEST) pattern = request.args.get("pattern", ()) if len(pattern) == 0: self.problemReport("invalid-pattern", "Missing pattern request-URI query parameter", responsecode.BAD_REQUEST) elif len(pattern) > 1: self.problemReport("invalid-pattern", "Too many pattern request-URI query parameters", responsecode.BAD_REQUEST) pattern = pattern[0] def _comp_is(pattern, s): return pattern == s def _comp_startswith(pattern, s): return s.startswith(pattern) def _comp_endswith(pattern, s): return s.endswith(pattern) def _comp_contains(pattern, s): return pattern in s def _normalize(s): return s.replace("_", " ").lower() if pattern.startswith("*") and pattern.endswith("*"): pattern = pattern[1:-1] comparator = _comp_contains elif pattern.endswith("*"): pattern = pattern[:-1] comparator = _comp_startswith elif pattern.startswith("*"): pattern = pattern[1:] comparator = _comp_endswith else: comparator = _comp_is pattern = _normalize(pattern) if not pattern: self.problemReport("invalid-pattern", "Invalid pattern request-URI query parameter value", responsecode.BAD_REQUEST) timezones = [] for tz in self.timezones.listTimezones(None): matched = comparator(pattern, _normalize(tz.tzid)) if not matched: for alias in tz.aliases: if comparator(pattern, _normalize(alias)): matched = True break if matched: timezones.append({ "tzid": tz.tzid, "last-modified": tz.dtstamp, "aliases": tz.aliases, }) result = { "dtstamp": self.timezones.dtstamp, "timezones": timezones, } return JSONResponse(responsecode.OK, result, pretty=config.TimezoneService.PrettyPrintJSON) class TimezoneInfo(object): """ Maintains information from an on-disk store of timezone files. """ def __init__(self, tzid, aliases, dtstamp, md5): self.tzid = tzid self.aliases = aliases self.dtstamp = dtstamp self.md5 = md5 @classmethod def readXML(cls, node): """ Parse XML data. """ if node.tag != "timezone": return None tzid = node.findtext("tzid") dtstamp = node.findtext("dtstamp") aliases = tuple([alias_node.text for alias_node in node.findall("alias")]) md5 = node.findtext("md5") return cls(tzid, aliases, dtstamp, md5) def generateXML(self, parent): """ Generate the XML element for this timezone info. """ node = xmlutil.addSubElement(parent, "timezone") xmlutil.addSubElement(node, "tzid", self.tzid) xmlutil.addSubElement(node, "dtstamp", self.dtstamp) for alias in self.aliases: xmlutil.addSubElement(node, "alias", alias) xmlutil.addSubElement(node, "md5", self.md5) class CommonTimezoneDatabase(object): """ Maintains the database of timezones read from an XML file. """ def __init__(self, basepath, xmlfile): self.basepath = basepath self.xmlfile = xmlfile self.dtstamp = None self.timezones = {} self.aliases = {} def onStartup(self): return succeed(None) def readDatabase(self): """ Read in XML data. """ _ignore, root = xmlutil.readXML(self.xmlfile, "timezones") self.dtstamp = root.findtext("dtstamp") for child in root: if child.tag == "timezone": tz = TimezoneInfo.readXML(child) if tz: self.timezones[tz.tzid] = tz for alias in tz.aliases: self.aliases[alias] = tz.tzid def listTimezones(self, changedsince): """ List timezones (not aliases) possibly changed since a particular dtstamp. """ for tzid, tzinfo in sorted(self.timezones.items(), key=lambda x: x[0]): # Ignore those that are aliases if tzid in self.aliases: continue # Detect timestamp changes if changedsince and tzinfo.dtstamp <= changedsince: continue yield tzinfo def getTimezone(self, tzid): """ Generate a PyCalendar containing the requested timezone. """ # We will just use our existing TimezoneCache here calendar = Calendar() try: vtz = readVTZ(tzid) calendar.addComponent(vtz.getComponents()[0].duplicate()) except TimezoneException: # Check if an alias exists and create data for that if tzid in self.aliases: try: vtz = readVTZ(self.aliases[tzid]) except TimezoneException: log.error("Failed to find timezone data for alias: {tzid}", tzid=tzid) return None else: vtz = vtz.duplicate() vtz.getComponents()[0].getProperties("TZID")[0].setValue(tzid) addVTZ(tzid, vtz) calendar.addComponent(vtz.getComponents()[0].duplicate()) else: log.error("Failed to find timezone data for: {tzid}", tzid=tzid) return None return calendar def _dumpTZs(self): _ignore, root = xmlutil.newElementTreeWithRoot("timezones") addSubElement(root, "dtstamp", self.dtstamp) for _ignore, v in sorted(self.timezones.items(), key=lambda x: x[0]): v.generateXML(root) xmlutil.writeXML(self.xmlfile, root) def _buildAliases(self): """ Rebuild aliases mappings from current tzinfo. """ self.aliases = {} for tzinfo in self.timezones.values(): for alias in tzinfo.aliases: self.aliases[alias] = tzinfo.tzid class PrimaryTimezoneDatabase(CommonTimezoneDatabase): """ Maintains the database of timezones read from an XML file. """ def __init__(self, basepath, xmlfile): super(PrimaryTimezoneDatabase, self).__init__(basepath, xmlfile) def createNewDatabase(self): """ Create a new DB xml file from scratch by scanning zoneinfo. """ self.dtstamp = DateTime.getNowUTC().getXMLText() self._scanTZs("") self._dumpTZs() def _scanTZs(self, path, checkIfChanged=False): # Read in all timezone files first for item in os.listdir(os.path.join(self.basepath, path)): fullPath = os.path.join(self.basepath, path, item) if item.find('.') == -1: self._scanTZs(os.path.join(path, item), checkIfChanged) elif item.endswith(".ics"): # Build TimezoneInfo object tzid = os.path.join(path, item[:-4]) try: with open(fullPath) as f: tzdata = f.read() md5 = hashlib.md5(tzdata).hexdigest() except IOError: log.error("Unable to read timezone file: {p}", p=fullPath) continue if checkIfChanged: oldtz = self.timezones.get(tzid) if oldtz != None and oldtz.md5 == md5: continue self.changeCount += 1 self.changed.add(tzid) self.timezones[tzid] = TimezoneInfo(tzid, (), self.dtstamp, md5) # Try links (aliases) file try: with open(os.path.join(self.basepath, "links.txt")) as f: aliases = f.read() except IOError, e: log.error("Unable to read links.txt file: {ex}", ex=str(e)) aliases = "" try: for alias in aliases.splitlines(): alias_from, alias_to = alias.split("\t") tzinfo = self.timezones.get(alias_to) if tzinfo: if alias_from != alias_to: if alias_from not in tzinfo.aliases: tzinfo.aliases += (alias_from,) self.aliases[alias_from] = alias_to else: log.error("Missing alias from '{fr}' to '{to}'", fr=alias_from, to=alias_to) except ValueError: log.error("Unable to parse links.txt file: {ex}", ex=str(e)) def updateDatabase(self): """ Update existing DB info by comparing md5's. """ self.dtstamp = DateTime.getNowUTC().getXMLText() self.changeCount = 0 self.changed = set() self._scanTZs("", checkIfChanged=True) if self.changeCount: self._dumpTZs() class SecondaryTimezoneDatabase(CommonTimezoneDatabase): """ Caches a database of timezones from another timezone service. """ def __init__(self, basepath, xmlfile, uri): super(SecondaryTimezoneDatabase, self).__init__(basepath, xmlfile) self.uri = uri self.discovered = False self._url = None log.debug("Configuring secondary server with basepath: {p}", p=self.basepath) if not os.path.exists(self.basepath): os.makedirs(self.basepath) # Paths need to be writeable if not os.access(basepath, os.W_OK): raise ValueError("Secondary Timezone Service needs writeable zoneinfo path at: %s" % (basepath,)) if os.path.exists(xmlfile) and not os.access(xmlfile, os.W_OK): raise ValueError("Secondary Timezone Service needs writeable xmlfile path at: %s" % (xmlfile,)) def onStartup(self): return self.syncWithServer() @inlineCallbacks def syncWithServer(self): """ Sync local data with that from the server we are replicating. """ log.debug("Sync'ing with secondary server") result = (yield self._getTimezoneListFromServer()) if result is None: # Nothing changed since last sync log.debug("No changes on secondary server") returnValue(None) newdtstamp, newtimezones = result # Compare timezone infos # New ones on the server newtzids = set(newtimezones.keys()) - set(self.timezones.keys()) # Check for changes changedtzids = set() for tzid in set(newtimezones.keys()) & set(self.timezones.keys()): if self.timezones[tzid].dtstamp < newtimezones[tzid].dtstamp: changedtzids.add(tzid) log.debug("Fetching {n} new, {c} changed timezones on secondary server", n=len(newtzids), c=len(changedtzids)) # Now apply changes - do requests in parallel for speedier fetching BATCH = 5 tzids = list(itertools.chain(newtzids, changedtzids)) tzids.sort() while tzids: yield DeferredList([self._getTimezoneFromServer(newtimezones[tzid]) for tzid in tzids[0:BATCH]]) tzids = tzids[BATCH:] self.dtstamp = newdtstamp self._dumpTZs() self._buildAliases() log.debug("Sync with secondary server complete") returnValue((len(newtzids), len(changedtzids),)) @inlineCallbacks def _discoverServer(self): """ Make sure we know the timezone service path """ if self.uri is None: if config.TimezoneService.SecondaryService.Host: self.uri = "https://%s/.well-known/timezone" % (config.TimezoneService.SecondaryService.Host,) elif config.TimezoneService.SecondaryService.URI: self.uri = config.TimezoneService.SecondaryService.URI elif not self.uri.startswith("https:") and not self.uri.startswith("http:"): self.uri = "https://%s/.well-known/timezone" % (self.uri,) testURI = "%s?action=capabilities" % (self.uri,) log.debug("Discovering secondary server: {uri}", uri=testURI) response = (yield getURL(testURI)) if response is None or response.code / 100 != 2: log.error("Unable to discover secondary server: {uri}", uri=testURI) self.discovered = False returnValue(False) # Cache the redirect target if hasattr(response, "location"): self.uri = response.location log.debug("Redirected secondary server to: {uri}", uri=self.uri) # TODO: Ignoring the data from capabilities for now self.discovered = True returnValue(True) @inlineCallbacks def _getTimezoneListFromServer(self): """ Retrieve the timezone list from the specified server """ # Make sure we have the server if not self.discovered: result = (yield self._discoverServer()) if not result: returnValue(None) # List all from the server url = "%s?action=list" % (self.uri,) if self.dtstamp: url = "%s&changedsince=%s" % (url, self.dtstamp,) log.debug("Getting timezone list from secondary server: {url}", url=url) response = (yield getURL(url)) if response is None or response.code / 100 != 2: returnValue(None) ct = response.headers.getRawHeaders("content-type", ("bogus/type",))[0] ct = ct.split(";", 1) ct = ct[0] if ct not in ("application/json",): returnValue(None) try: jroot = json.loads(response.data) dtstamp = jroot["dtstamp"] timezones = {} for timezone in jroot["timezones"]: tzid = timezone["tzid"] lastmod = timezone["last-modified"] aliases = timezone.get("aliases", ()) timezones[tzid] = TimezoneInfo(tzid, aliases, lastmod, None) except (ValueError, KeyError): log.debug("Failed to parse JSON timezone list response: {resp}", resp=response.data) returnValue(None) log.debug("Got {len} timezones from secondary server", len=len(timezones)) returnValue((dtstamp, timezones,)) @inlineCallbacks def _getTimezoneFromServer(self, tzinfo): # List all from the server url = "%s?action=get&tzid=%s" % (self.uri, tzinfo.tzid,) log.debug("Getting timezone from secondary server: {url}", url=url) response = (yield getURL(url)) if response is None or response.code / 100 != 2: returnValue(None) ct = response.headers.getRawHeaders("content-type", ("bogus/type",))[0] ct = ct.split(";", 1) ct = ct[0] if ct not in ("text/calendar",): log.error("Invalid content-type '{ct}' for tzid : {tzid}", ct=ct, tzid=tzinfo.tzid) returnValue(None) ical = response.data try: calendar = Calendar.parseText(ical) except InvalidData: log.error("Invalid calendar data for tzid: {tzid}", tzid=tzinfo.tzid) returnValue(None) ical = calendar.getText() tzinfo.md5 = hashlib.md5(ical).hexdigest() try: tzpath = os.path.join(self.basepath, tzinfo.tzid) + ".ics" if not os.path.exists(os.path.dirname(tzpath)): os.makedirs(os.path.dirname(tzpath)) with open(tzpath, "w") as f: f.write(ical) except IOError, e: log.error("Unable to write calendar file for {tzid}: {ex}", tzid=tzinfo.tzid, ex=str(e)) else: self.timezones[tzinfo.tzid] = tzinfo def _removeTimezone(self, tzid): tzpath = os.path.join(self.basepath, tzid) + ".ics" try: os.remove(tzpath) del self.timezones[tzid] except IOError, e: log.error("Unable to write calendar file for {tzid}: {ex}", tzid=tzid, ex=str(e))