Example #1
0
def _parse_accept_header(accept_header: str):
    """Parse accept headers and select one that fits CSE.

    CSE is looking for headers like
    * application/json;version=33.0
    * *;version=33.0
    * */*;version=33.0
    * application/*+json;version=33.0
    If multiple matches are found, Will pick the first match.

    :param str accept_header: value of 'Accept' header sent by client

    :returns: accept header that can be serviced by CSE

    :raises NotAcceptableRequestError: If none of the accept headers matches
        what CSE is looking for.
    """
    accept_header = accept_header.lower()
    accept_headers = accept_header.split(",")
    processed_headers = {}

    for header in accept_headers:
        # break the header into a tuple that follows the following structure
        # "application/json;version=33.0" ->
        #     ('application', 'json', 'version', '33.0')
        # "application/*;version=33.0" ->
        #     ('application', '*', 'version', '33.0')
        # "application/*+json;version=33.0" ->
        #     ('application', '*+json', 'version', '33.0')
        # "*/*;version=33.0" -> ('*', '*', 'version', '33.0')
        # "*;version=33.0" -> ('*', '', 'version', '33.0')

        tokens = header.split(';')
        application_fragment = ''
        version_fragment = ''
        if len(tokens) >= 1:
            application_fragment = tokens[0]
        if len(tokens) >= 2:
            version_fragment = tokens[1]

        tokens = application_fragment.split("/")
        val0 = ''
        val1 = ''
        if len(tokens) >= 1:
            val0 = tokens[0]
        if len(tokens) >= 2:
            val1 = tokens[1]

        tokens = version_fragment.split("=")
        val2 = ''
        val3 = ''
        if len(tokens) >= 1:
            val2 = tokens[0]
        if len(tokens) >= 2:
            val3 = tokens[1]

        processed_headers[header] = (val0, val1, val2, val3)

    selected_header = None
    for header, value in processed_headers.items():
        val0, val1, val2, _ = value

        # * -> */*
        if val0 == '*' and not val1:
            val1 = '*'

        if val0 == '*':
            val0 = 'application'

        # *+json -> json
        val1 = val1.replace('*+', '')
        if val1 == '*':
            val1 = 'json'

        if (val0, val1, val2) == ('application', 'json', 'version'):
            selected_header = header
            break

    if not selected_header:
        raise cse_exception.NotAcceptableRequestError(
            error_message="CSE can only serve response as json.")

    return selected_header
Example #2
0
def process_request(message):
    """
    Determine the correct api handler to invoke and invoke it.

    The request URI, api version and HTTP verb are used to determine the
    request operation and the corresponding handler.

    Additionally support for payload verification, query param verification
    will be added in a later point of time.

    URL template matching is also performed to compute values of url template
    parameters. These computed values, request body and query params are all
    sent to handlers in form of a dictionary.

    :param dict message: message received over AMQP/MQTT bus representing the
        incoming REST request.

    :returns: response computed by the handler after processing the request
    """
    LOGGER.debug(f"Incoming request message: {json.dumps(message)}")

    api_version_header = _parse_accept_header(
        accept_header=message['headers'].get('Accept'))
    api_version = _get_api_version_from_accept_header(
        api_version_header=api_version_header)

    # Convert to upper case for matching the ENUM values
    method = RequestMethod(message['method'].upper())

    url = message['requestUri']
    url_tokens = url.split("/")
    # ignore the vcd host and /api in the url
    if len(url_tokens) > 2:
        url_tokens = url_tokens[2:]

    query_params = None
    if message['queryString']:
        query_params = dict(parse_qsl(message['queryString']))

    request_body = None
    # Should we do a content-type check? and allow only application/json content?  # noqa: E501
    # Process request body only for requests with HTTP verbs that allow body
    if method in [RequestMethod.POST,
                  RequestMethod.PUT,
                  RequestMethod.DELETE] and \
            len(message['body']) > 0:
        raw_body = base64.b64decode(message['body']).decode(
            sys.getfilesystemencoding())  # noqa: E501
        request_body = json.loads(raw_body)

    import container_service_extension.server.service as cse_service
    server_config = cse_service.Service().get_service_config()

    found = False
    operation = None
    handler_method = None
    url_data = {}
    for entry in CSE_REQUEST_DISPATCHER_LIST:
        if found:
            break
        if len(entry['url_tokens']) != len(url_tokens):
            continue

        url_matched = True
        for i in range(0, len(url_tokens)):
            token = entry['url_tokens'][i]
            if token.startswith("$"):
                url_data[token[1:]] = url_tokens[i]
            elif token.lower() != url_tokens[i].lower():
                url_matched = False
                url_data.clear()
                break

        if not url_matched:
            continue

        if method not in entry.keys():
            raise cse_exception.MethodNotAllowedRequestError()

        handlers = entry[method]
        matched_handler = None
        supported_api_versions = []
        for versions in handlers.keys():
            supported_api_versions.extend(list(versions))
            if api_version in versions or '*' in versions:
                matched_handler = handlers[versions]
                break

        if not matched_handler:
            raise cse_exception.NotAcceptableRequestError(
                error_message="Invalid api version specified. Expected "
                f"api version '{supported_api_versions}'.")

        required_feature_flags = matched_handler.get('feature_flags', {})
        feature_flags_satisfied = True
        for feature_flag in required_feature_flags:
            value = server_config['feature_flags'].get(feature_flag, False)
            if not value:
                LOGGER.debug("Url matched but failed to satisfy feature "
                             f"flag {feature_flag}")
                feature_flags_satisfied = False
                break

        if feature_flags_satisfied:
            operation = matched_handler['operation']
            handler_method = matched_handler['handler']

            # ToDo: Extra validation based on allowed query params, content type etc.  # noqa: E501
            found = True
        else:
            break

    if not found:
        raise cse_exception.NotFoundRequestError()

    # /system operations are excluded from these checks
    if operation not in (CseOperation.SYSTEM_INFO, CseOperation.SYSTEM_UPDATE):
        if not cse_service.Service().is_running():
            raise cse_exception.BadRequestError(
                error_message='CSE service is disabled. '
                'Contact the System Administrator.')

    # create request data dict from incoming message data
    request_data = {}

    # update request data dict with query params data
    if query_params:
        request_data[RequestKey.QUERY_PARAMS] = query_params
        LOGGER.debug(f"query parameters: {query_params}")

    # update request_data with request_body
    if request_body:
        request_data[RequestKey.INPUT_SPEC] = request_body
        LOGGER.debug(f"request body: {request_body}")

    # update request_data with url template param key-values
    request_data.update(url_data)
    request_data['url'] = url

    # extract out the authorization token
    tenant_auth_token = message['headers'].get('x-vcloud-authorization')
    is_jwt_token = False
    auth_header = message['headers'].get('Authorization')
    if auth_header:
        tokens = auth_header.split(" ")
        if len(tokens) == 2 and tokens[0].lower() == 'bearer':
            tenant_auth_token = tokens[1]
            is_jwt_token = True

    # create operation context
    operation_ctx = ctx.OperationContext(tenant_auth_token,
                                         is_jwt=is_jwt_token,
                                         request_id=message['id'])

    try:
        body_content = handler_method(request_data, operation_ctx)
    finally:
        if not operation_ctx.is_async:
            operation_ctx.end()

    if not isinstance(body_content, (list, dict)):
        body_content = {RESPONSE_MESSAGE_KEY: str(body_content)}
    response = {
        'status_code': operation.ideal_response_code,
        'body': body_content,
    }
    LOGGER.debug(f"Outgoing response: {str(response)}")
    return response
def process_request(message):
    from container_service_extension.server.service import Service
    LOGGER.debug(f"Incoming request message: {json.dumps(message)}")

    api_version_header = _parse_accept_header(
        accept_header=message['headers'].get('Accept'))
    api_version = _get_api_version_from_accept_header(
        api_version_header=api_version_header)
    url_data = _get_url_data(method=message['method'],
                             url=message['requestUri'],
                             api_version=api_version)  # noqa: E501
    operation = url_data[_OPERATION_KEY]

    # Check api version and if server is disabled or not
    # /system operations are excluded from these checks
    if operation not in (CseOperation.SYSTEM_INFO, CseOperation.SYSTEM_UPDATE):
        if not Service().is_running():
            raise cse_exception.BadRequestError(
                error_message='CSE service is disabled. '
                'Contact the System Administrator.')
        else:
            server_api_version = server_utils.get_server_api_version()
            if api_version != server_api_version:
                raise cse_exception.NotAcceptableRequestError(
                    error_message="Invalid api version specified. Expected "
                    f"api version '{server_api_version}'.")

    # create request data dict from incoming message data
    request_data = {}
    is_cse_3_0_request = _is_cse_3_0_endpoint(message['requestUri'])
    if len(message['body']) > 0:
        raw_body = base64.b64decode(message['body']).decode(
            sys.getfilesystemencoding())  # noqa: E501
        request_body = json.loads(raw_body)
        if is_cse_3_0_request:
            request_data[shared_constants.RequestKey.INPUT_SPEC] = request_body
        else:
            request_data.update(request_body)
        LOGGER.debug(f"request body: {request_data}")
    # update request data dict with query params data
    if message['queryString']:
        query_params = dict(parse_qsl(message['queryString']))
        if is_cse_3_0_request:
            request_data[shared_constants.RequestKey.V35_QUERY] = query_params
        else:
            request_data.update(query_params)
        LOGGER.debug(f"query parameters: {query_params}")
    # update request spec with operation specific data in the url
    request_data.update(url_data)
    # remove None values from request payload
    data = {k: v for k, v in request_data.items() if v is not None}
    # extract out the authorization token
    tenant_auth_token = message['headers'].get('x-vcloud-authorization')
    is_jwt_token = False
    auth_header = message['headers'].get('Authorization')
    if auth_header:
        tokens = auth_header.split(" ")
        if len(tokens) == 2 and tokens[0].lower() == 'bearer':
            tenant_auth_token = tokens[1]
            is_jwt_token = True

    # create operation context
    operation_ctx = ctx.OperationContext(tenant_auth_token,
                                         is_jwt=is_jwt_token,
                                         request_id=message['id'])

    try:
        body_content = OPERATION_TO_HANDLER[operation](data, operation_ctx)
    finally:
        if not operation_ctx.is_async:
            operation_ctx.end()

    if not isinstance(body_content, (list, dict)):
        body_content = \
            {shared_constants.RESPONSE_MESSAGE_KEY: str(body_content)}
    response = {
        'status_code': operation.ideal_response_code,
        'body': body_content,
    }
    LOGGER.debug(f"Outgoing response: {str(response)}")
    return response