def process_handler(request): _ctypes = {'xml': 'application/samlmetadata+xml;application/xml;text/xml', 'json': 'application/json'} def _d(x, do_split=True): if x is not None: x = x.strip() if x is None or len(x) == 0: return None, None if '.' in x: (pth, dot, extn) = x.rpartition('.') assert (dot == '.') if extn in _ctypes: return pth, extn return x, None log.debug(request) if request.matchdict is None: raise exc.exception_response(400) if request.body: try: request.matchdict.update(request.json_body) except ValueError as ex: pass entry = request.matchdict.get('entry', 'request') path = list(request.matchdict.get('path', [])) match = request.params.get('q', request.params.get('query', None)) # Enable matching on scope. match = (match.split('@').pop() if match and not match.endswith('@') else match) log.debug("match={}".format(match)) if 0 == len(path): path = ['entities'] alias = path.pop(0) path = '/'.join(path) # Ugly workaround bc WSGI drops double-slashes. path = path.replace(':/', '://') msg = "handling entry={}, alias={}, path={}" log.debug(msg.format(entry, alias, path)) pfx = None if 'entities' not in alias: pfx = request.registry.aliases.get(alias, None) if pfx is None: raise exc.exception_response(404) path, ext = _d(path, True) if pfx and path: q = "{%s}%s" % (pfx, path) path = "/%s/%s" % (alias, path) else: q = path # TODO - sometimes the client sends > 1 accept header value with ','. accept = str(request.accept).split(',')[0] # import pdb; pdb.set_trace() if (not accept or 'application/*' in accept or 'text/*' in accept or '*/*' in accept) and ext: accept = _ctypes[ext] try: accepter = MediaAccept(accept) for p in request.registry.plumbings: state = {entry: True, 'headers': {'Content-Type': None}, 'accept': accepter, 'url': request.current_route_url(), 'select': q, 'match': match.lower() if match else match, 'path': path, 'stats': {}} r = p.process(request.registry.md, state=state, raise_exceptions=True, scheduler=request.registry.scheduler) log.debug(r) if r is None: r = [] response = Response() response.headers.update(state.get('headers', {})) ctype = state.get('headers').get('Content-Type', None) if not ctype: r, t = _fmt(r, accepter) ctype = t response.text = b2u(r) response.size = len(r) response.content_type = ctype cache_ttl = int(state.get('cache', 0)) response.expires = (datetime.now() + timedelta(seconds=cache_ttl)) return response except ResourceException as ex: import traceback log.debug(traceback.format_exc()) log.warn(ex) raise exc.exception_response(409) except BaseException as ex: import traceback log.debug(traceback.format_exc()) log.error(ex) raise exc.exception_response(500) if request.method == 'GET': raise exc.exception_response(404)
def process_handler(request: Request) -> Response: """ The main request handler for pyFF. Implements API call hooks and content negotiation. :param request: the HTTP request object :return: the data to send to the client """ _ctypes = { 'xml': 'application/samlmetadata+xml;application/xml;text/xml', 'json': 'application/json' } def _d(x: Optional[str], do_split: bool = True) -> Tuple[Optional[str], Optional[str]]: """ Split a path into a base component and an extension. """ if x is not None: x = x.strip() if x is None or len(x) == 0: return None, None if '.' in x: (pth, dot, extn) = x.rpartition('.') assert dot == '.' if extn in _ctypes: return pth, extn return x, None log.debug(f'Processing request: {request}') if request.matchdict is None: raise exc.exception_response(400) if request.body: try: request.matchdict.update(request.json_body) except ValueError as ex: pass entry = request.matchdict.get('entry', 'request') path_elem = list(request.matchdict.get('path', [])) match = request.params.get('q', request.params.get('query', None)) # Enable matching on scope. match = match.split( '@').pop() if match and not match.endswith('@') else match log.debug("match={}".format(match)) if not path_elem: path_elem = ['entities'] alias = path_elem.pop(0) path = '/'.join(path_elem) # Ugly workaround bc WSGI drops double-slashes. path = path.replace(':/', '://') msg = "handling entry={}, alias={}, path={}" log.debug(msg.format(entry, alias, path)) pfx = None if 'entities' not in alias: pfx = request.registry.aliases.get(alias, None) if pfx is None: raise exc.exception_response(404) # content_negotiation_policy is one of three values: # 1. extension - current default, inspect the path and if it ends in # an extension, e.g. .xml or .json, always strip off the extension to # get the entityID and if no accept header or a wildcard header, then # use the extension to determine the return Content-Type. # # 2. adaptive - only if no accept header or if a wildcard, then inspect # the path and if it ends in an extension strip off the extension to # get the entityID and use the extension to determine the return # Content-Type. # # 3. header - future default, do not inspect the path for an extension and # use only the Accept header to determine the return Content-Type. policy = config.content_negotiation_policy # TODO - sometimes the client sends > 1 accept header value with ','. accept = str(request.accept).split(',')[0] valid_accept = accept and not ('application/*' in accept or 'text/*' in accept or '*/*' in accept) new_path: Optional[str] = path path_no_extension, extension = _d(new_path, True) accept_from_extension = accept if extension: accept_from_extension = _ctypes.get(extension, accept) if policy == 'extension': new_path = path_no_extension if not valid_accept: accept = accept_from_extension elif policy == 'adaptive': if not valid_accept: new_path = path_no_extension accept = accept_from_extension if not accept: log.warning('Could not determine accepted response type') raise exc.exception_response(400) q: Optional[str] if pfx and new_path: q = f'{{{pfx}}}{new_path}' new_path = f'/{alias}/{new_path}' else: q = new_path try: accepter = MediaAccept(accept) for p in request.registry.plumbings: state = { entry: True, 'headers': { 'Content-Type': None }, 'accept': accepter, 'url': request.current_route_url(), 'select': q, 'match': match.lower() if match else match, 'path': new_path, 'stats': {}, } r = p.process(request.registry.md, state=state, raise_exceptions=True, scheduler=request.registry.scheduler) log.debug(f'Plumbing process result: {r}') if r is None: r = [] response = Response() _headers = state.get('headers', {}) response.headers.update(_headers) ctype = _headers.get('Content-Type', None) if not ctype: r, t = _fmt(r, accepter) ctype = t response.text = b2u(r) response.size = len(r) response.content_type = ctype cache_ttl = int(state.get('cache', 0)) response.expires = datetime.now() + timedelta(seconds=cache_ttl) return response except ResourceException as ex: import traceback log.debug(traceback.format_exc()) log.warning(f'Exception from processing pipeline: {ex}') raise exc.exception_response(409) except BaseException as ex: import traceback log.debug(traceback.format_exc()) log.error(f'Exception from processing pipeline: {ex}') raise exc.exception_response(500) if request.method == 'GET': raise exc.exception_response(404)