def json(self, req, obj, fields, all_fields, multi=False, expand=None): data = self.get_fields(req, obj, fields, all_fields) if not multi: data['@odata.context'] = req.path if expand: data.update(expand) return _dumpb_json(data)
def json_multi(self, req, obj, fields, all_fields, top, skip, count, deltalink, add_count=False): header = b'{\n' header += b' "@odata.context": "%s",\n' % req.path.encode('utf-8') if add_count: header += b' "@odata.count": "%d",\n' % count if deltalink: header += b' "@odata.deltaLink": "%s",\n' % deltalink else: path = req.path if req.query_string: args = self.parse_qs(req) if '$skip' in args: del args['$skip'] else: args = {} args['$skip'] = skip+top nextLink = path + '?' + _encode_qs(list(args.items())) header += b' "@odata.nextLink": "%s",\n' % (_dumpb_json(nextLink)[1:-1]) header += b' "value": [\n' yield header first = True try: for o in obj: if isinstance(o, tuple): o, resource = o all_fields = resource.fields if not first: yield b',\n' first = False wa = self.json(req, o, fields, all_fields, multi=True) yield b'\n'.join([b' '+line for line in wa.splitlines()]) except Exception: logging.exception("failed to marshal %s JSON response", req.path) yield b'\n ]\n}'
def on_get(self, req, resp, subscriptionid=None): record = _record(req, self.options) if subscriptionid: try: subscription, sink, userid = record.subscriptions[ subscriptionid] except KeyError: resp.status = falcon.HTTP_404 return data = _export_subscription(subscription) else: user = record.user userid = user.userid data = { '@odata.context': req.path, 'value': [ _export_subscription(subscription) for (subscription, _, uid) in record.subscriptions.values() if uid == userid ], # TODO doesn't scale } resp.content_type = "application/json" resp.body = _dumpb_json(data)
def on_patch(self, req, resp, subscriptionid=None): if not subscriptionid: raise utils.HTTPBadRequest('missing required subscriptionid') record = _record(req, self.options) try: subscription, sink, userid = record.subscriptions[subscriptionid] except KeyError: resp.status = falcon.HTTP_404 return fields = self.load_json(req) self.validate_json(update_subscription_schema, fields) for k, v in fields.items(): if v and k == 'expirationDateTime': # NOTE(longsleep): Setting a dict key which is already there is threadsafe in current CPython implementations. try: dateutil.parser.parse(v) subscription['expirationDateTime'] = v except ValueError: raise utils.HTTPBadRequest('expirationDateTime is not a valid datetime string') if sink.expired: sink.expired = False logging.debug('subscription updated before it expired, id:%s', subscriptionid) data = _export_subscription(subscription) resp.content_type = "application/json" resp.body = _dumpb_json(data)
def on_patch_subscriptions_by_id(self, req, resp, subscriptionid): """Handle PATCH request. Args: req (Request): Falcon request object. resp (Response): Falcon response object. subscriptionid (str): subscription ID. """ record = _record(req, self.options) try: subscription, sink, _ = record.subscriptions[subscriptionid] except KeyError: resp.status = falcon.HTTP_404 return json_data = req.context.json_data for k, v in json_data.items(): if v and k == 'expirationDateTime': # NOTE(longsleep): Setting a dict key which is already there is threadsafe in current CPython implementations. try: dateutil.parser.parse(v) subscription['expirationDateTime'] = v except ValueError: raise utils.HTTPBadRequest('expirationDateTime is not a valid datetime string') if sink.expired: sink.expired = False logging.debug('subscription updated before it expired, id:%s', subscriptionid) data = _export_subscription(subscription) resp.body = _dumpb_json(data) resp.status = falcon.HTTP_200
def on_get_subscriptions_by_id(self, req, resp, subscriptionid): """Handle GET request - return by subscription ID. Args: req (Request): Falcon request object. resp (Response): Falcon response object. subscriptionid (str): subscription ID. """ record = _record(req, self.options) try: subscription = record.subscriptions[subscriptionid][0] except KeyError: raise utils.HTTPNotFound() data = _export_subscription(subscription) resp.body = _dumpb_json(data) resp.status = falcon.HTTP_200
def on_get(self, req, resp): """Handle GET request - return all subscriptions. Args: req (Request): Falcon request object. resp (Response): Falcon response object. """ record = _record(req, self.options) userid = record.user.userid data = { '@odata.context': req.path, 'value': [ _export_subscription(subscription) for subscription, _, uid in record.subscriptions.values() if uid == userid ], # TODO doesn't scale } resp.body = _dumpb_json(data) resp.status = falcon.HTTP_200
def on_post(self, req, resp): """Handle POST request. Args: req (Request): Falcon request object. resp (Response): Falcon response object. """ subscription_id = req.context.request_id verify = not self.options or not self.options.insecure json_data = req.context.json_data max_retry = config.SUBSCRIPTION_INTERNAL_RETRY retry = 0 while retry != max_retry: retry += 1 try: self._add_subscription( req, subscription_id, self.options, verify, json_data ) break except MAPIErrorNoSupport: logging.exception( "subscription not possible right now, trying %d/%d", retry, max_retry ) # A short nap for the resetting connection. time.sleep(0.5) else: raise falcon.HTTPInternalServerError( description="subscription is not possible, please retry" ) # Prepare response. resp.status = falcon.HTTP_201 resp.content_type = "application/json" resp.body = _dumpb_json(_export_subscription(json_data)) if self.options and self.options.with_metrics: SUBSCR_COUNT.inc() SUBSCR_ACTIVE.inc()
def on_post(self, req, resp): record = _record(req, self.options) server = record.server user = record.user store = record.store fields = self.load_json(req) self.validate_json(subscription_schema, fields) id_ = str(uuid.uuid4()) verify = not self.options or not self.options.insecure # Validate URL. try: # Enforce URL to valid and to be public, unless running insecure. if not validators.url(fields['notificationUrl'], public=True): if verify: raise ValueError('url validator failed') else: logging.warning('ignored notification url validation error (insecure enabled), auth_user:%s, id:%s, url:%s', server.auth_user, id_, fields['notificationUrl']) notificationUrl = urlparse(fields['notificationUrl']) if notificationUrl.scheme != 'https': if not verify and notificationUrl.scheme == 'http': logging.warning('allowing unencrypted notification url (insecure enabled), auth_user:%s, id:%s, url:%s', server.auth_user, id_, fields['notificationUrl']) else: raise ValueError('must use https scheme') except Exception: logging.debug('invalid subscription notification url, auth_user:%s, id:%s, url:%s', server.auth_user, id_, fields['notificationUrl'], exc_info=True) raise utils.HTTPBadRequest("Subscription notification url invalid.") # Validate webhook. validationToken = str(uuid.uuid4()) try: # TODO async logging.debug('validating subscription notification url, auth_user:%s, id:%s, url:%s', server.auth_user, id_, fields['notificationUrl']) r = REQUEST_SESSION.post(fields['notificationUrl']+'?validationToken='+validationToken, timeout=10, verify=verify) # TODO(longsleep): Add timeout configuration. if r.text != validationToken: logging.debug('subscription validation failed, validation token mismatch, id:%s, url:%s', id_, fields['notificationUrl']) raise utils.HTTPBadRequest("Subscription validation request failed.") except Exception: logging.exception('subscription validation request error, id:%s, url:%s', id_, fields['notificationUrl']) raise utils.HTTPBadRequest("Subscription validation request failed.") # Validate subscription data. subscription_object = _subscription_object(store, fields['resource']) if not subscription_object: logging.error('subscription object is invalid, id:%s, resource:%s', id_, fields['resource']) raise utils.HTTPBadRequest("Subscription object invalid.") target, folder_types, data_type = subscription_object # Create subscription. subscription = fields subscription['id'] = id_ subscription['_datatype'] = data_type sink = SubscriptionSink(store, self.options, subscription) object_types = ['item'] # TODO folders not supported by graph atm? event_types = [x.strip() for x in subscription['changeType'].split(',')] try: target.subscribe(sink, object_types=object_types, event_types=event_types, folder_types=folder_types) except MAPIErrorNoSupport: # Mhm connection is borked. # TODO(longsleep): Clean up and start from new. # TODO(longsleep): Add internal retry, do not throw exception to client. logging.exception('subscription not possible right now, resetting connection') raise falcon.HTTPInternalServerError(description='subscription not possible, please retry') record.subscriptions[id_] = (subscription, sink, user.userid) logging.debug( 'subscription created, auth_user:%s, id:%s, target:%s, object_types:%s, event_types:%s, folder_types:%s', server.auth_user, id_, target, object_types, event_types, folder_types ) resp.content_type = "application/json" resp.body = _dumpb_json(_export_subscription(subscription)) resp.status = falcon.HTTP_201 if self.options and self.options.with_metrics: SUBSCR_COUNT.inc() SUBSCR_ACTIVE.inc()