def actionExpand(self, request, tzid): """ Expand a timezone within specified start/end dates. """ if set(request.args.keys()) - set(("start", "end", "changedsince",)): 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 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 _error(self, status, description): raise HTTPError(JSONResponse( responsecode.BAD_REQUEST, { "status": status, "description": description, }, ))
def _ok(self, status, description, result=None): if result is None: result = {} result["status"] = status result["description"] = description return JSONResponse( responsecode.OK, result, )
def _processRequest(self): """ Process the request by sending it to the relevant server. @return: the HTTP response. @rtype: L{Response} """ store = self.storeMap[self.server.details()] j = json.loads(self.data) if self.stream is not None: j["stream"] = self.stream j["streamType"] = self.streamType try: if store.conduit.isStreamAction(j): stream = ProducerStream() class StreamProtocol(Protocol): def connectionMade(self): stream.registerProducer(self.transport, False) def dataReceived(self, data): stream.write(data) def connectionLost(self, reason): stream.finish() result = yield store.conduit.processRequestStream( j, StreamProtocol()) try: ct, name = result except ValueError: code = responsecode.BAD_REQUEST else: headers = {"content-type": MimeType.fromString(ct)} headers["content-disposition"] = MimeDisposition( "attachment", params={"filename": name}) returnValue(Response(responsecode.OK, headers, stream)) else: result = yield store.conduit.processRequest(j) code = responsecode.OK except Exception as e: # Send the exception over to the other side result = { "result": "exception", "class": ".".join(( e.__class__.__module__, e.__class__.__name__, )), "details": str(e), } code = responsecode.BAD_REQUEST response = JSONResponse(code, result) returnValue(response)
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 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): """ The server-to-server POST method. """ # Check shared secret if not self.store.directoryService().serversDB.getThisServer( ).checkSharedSecret(request.headers): self.log.error("Invalid shared secret header in cross-pod request") raise HTTPError( StatusResponse(responsecode.FORBIDDEN, "Not authorized to make this request")) # Look for XPOD header xpod = request.headers.getRawHeaders("XPOD") contentType = request.headers.getHeader("content-type") if xpod is not None: # Attachments are sent in the request body with the JSON data in a header. We # decode the header and add the request.stream as an attribute of the JSON object. xpod = xpod[0] try: j = json.loads(base64.b64decode(xpod)) except (TypeError, ValueError) as e: self.log.error("Invalid JSON header in request: {ex}\n{xpod}", ex=e, xpod=xpod) raise HTTPError( StatusResponse( responsecode.BAD_REQUEST, "Invalid JSON header in request: {}\n{}".format( e, xpod))) j["stream"] = request.stream j["streamType"] = contentType else: # Check content first if "{}/{}".format(contentType.mediaType, contentType.mediaSubtype) != "application/json": self.log.error("MIME type {mime} not allowed in request", mime=contentType) raise HTTPError( StatusResponse( responsecode.BAD_REQUEST, "MIME type {} not allowed in request".format( contentType))) body = (yield allDataFromStream(request.stream)) try: j = json.loads(body) except ValueError as e: self.log.error("Invalid JSON data in request: {ex}\n{body}", ex=e, body=body) raise HTTPError( StatusResponse( responsecode.BAD_REQUEST, "Invalid JSON data in request: {}\n{}".format(e, body))) # Log extended item if not hasattr(request, "extendedLogItems"): request.extendedLogItems = {} request.extendedLogItems[ "xpod"] = j["action"] if "action" in j else "unknown" # Get the conduit to process the data try: result = yield self.store.conduit.processRequest(j) except FailedCrossPodRequestError as e: raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e))) except Exception as e: raise HTTPError( StatusResponse(responsecode.INTERNAL_SERVER_ERROR, str(e))) response = JSONResponse(responsecode.OK, result) returnValue(response)
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)
def http_POST(self, request): """ The server-to-server POST method. """ # Check shared secret if not self.store.directoryService().serversDB().getThisServer( ).checkSharedSecret(request.headers): self.log.error("Invalid shared secret header in cross-pod request") raise HTTPError( StatusResponse(responsecode.FORBIDDEN, "Not authorized to make this request")) # Look for XPOD header xpod = request.headers.getRawHeaders("XPOD") contentType = request.headers.getHeader("content-type") if xpod is not None: # Attachments are sent in the request body with the JSON data in a header. We # decode the header and add the request.stream as an attribute of the JSON object. xpod = xpod[0] try: j = json.loads(base64.b64decode(xpod)) except (TypeError, ValueError) as e: self.log.error("Invalid JSON header in request: {ex}\n{xpod}", ex=e, xpod=xpod) raise HTTPError( StatusResponse( responsecode.BAD_REQUEST, "Invalid JSON header in request: {}\n{}".format( e, xpod))) j["stream"] = request.stream j["streamType"] = contentType else: # Check content first if "{}/{}".format(contentType.mediaType, contentType.mediaSubtype) != "application/json": self.log.error("MIME type {mime} not allowed in request", mime=contentType) raise HTTPError( StatusResponse( responsecode.BAD_REQUEST, "MIME type {} not allowed in request".format( contentType))) body = (yield allDataFromStream(request.stream)) try: j = json.loads(body) except ValueError as e: self.log.error("Invalid JSON data in request: {ex}\n{body}", ex=e, body=body) raise HTTPError( StatusResponse( responsecode.BAD_REQUEST, "Invalid JSON data in request: {}\n{}".format(e, body))) # Log extended item if not hasattr(request, "extendedLogItems"): request.extendedLogItems = {} request.extendedLogItems[ "xpod"] = j["action"] if "action" in j else "unknown" # Look for a streaming action which needs special handling if self.store.conduit.isStreamAction(j): # Get the conduit to process the data stream try: stream = ProducerStream() class StreamProtocol(Protocol): def connectionMade(self): stream.registerProducer(self.transport, False) def dataReceived(self, data): stream.write(data) def connectionLost(self, reason): stream.finish() result = yield self.store.conduit.processRequestStream( j, StreamProtocol()) try: ct, name = result except ValueError: code = responsecode.BAD_REQUEST else: headers = {"content-type": MimeType.fromString(ct)} headers["content-disposition"] = MimeDisposition( "attachment", params={"filename": name}) returnValue(Response(responsecode.OK, headers, stream)) except Exception as e: # Send the exception over to the other side result = { "result": "exception", "class": ".".join(( e.__class__.__module__, e.__class__.__name__, )), "details": str(e), } code = responsecode.BAD_REQUEST else: # Get the conduit to process the data try: result = yield self.store.conduit.processRequest(j) code = responsecode.OK if result[ "result"] == "ok" else responsecode.BAD_REQUEST except Exception as e: # Send the exception over to the other side result = { "result": "exception", "class": ".".join(( e.__class__.__module__, e.__class__.__name__, )), "details": str(e), } code = responsecode.BAD_REQUEST response = JSONResponse(code, result) returnValue(response)