ic-response-verification 3.2.0

Client side response verification for the Internet Computer
Documentation
use crate::cel::error::{CelParserError, CelParserResult};
use crate::cel::parser::CelValue;
use ic_http_certification::{
    cel::{
        CelExpression, DefaultCelExpression, DefaultFullCelExpression, DefaultRequestCertification,
        DefaultResponseOnlyCelExpression,
    },
    DefaultResponseCertification,
};
use std::collections::HashMap;

fn validate_object<'a>(
    cel: &'a CelValue<'a>,
    name: &str,
) -> CelParserResult<&'a HashMap<&'a str, CelValue<'a>>> {
    let CelValue::Object(object_name, object_value) = cel else {
        return Err(CelParserError::UnexpectedNodeType {
            node_name: name.into(),
            expected_type: "Object".into(),
            found_type: cel.to_string(),
        });
    };

    if *object_name != name {
        return Err(CelParserError::UnexpectedNodeName {
            node_type: "Object".into(),
            expected_name: name.into(),
            found_name: (*object_name).into(),
        });
    }

    Ok(object_value)
}

fn validate_function<'a>(
    cel: &'a CelValue<'a>,
    name: &'a str,
) -> CelParserResult<&'a Vec<CelValue<'a>>> {
    let CelValue::Function(function_name, function_value) = cel else {
        return Err(CelParserError::UnexpectedNodeType {
            node_name: name.into(),
            expected_type: "Function".into(),
            found_type: cel.to_string(),
        });
    };

    if *function_name != name {
        return Err(CelParserError::UnexpectedNodeName {
            node_type: "Function".into(),
            expected_name: name.into(),
            found_name: (*function_name).into(),
        });
    }

    Ok(function_value)
}

fn validate_string_array<'a>(
    cel: &'a CelValue<'a>,
    name: &'a str,
) -> CelParserResult<Vec<&'a str>> {
    let CelValue::Array(array) = cel else {
        return Err(CelParserError::UnexpectedNodeType {
            node_name: name.into(),
            expected_type: "Array".into(),
            found_type: cel.to_string(),
        });
    };

    let elements = array
        .iter()
        .map(|e| {
            let CelValue::String(e) = e else {
                return Err(CelParserError::UnexpectedNodeType {
                    node_name: name.into(),
                    expected_type: "String".into(),
                    found_type: cel.to_string(),
                });
            };

            Ok(*e)
        })
        .collect::<Result<_, _>>()?;

    Ok(elements)
}

fn validate_request_certification<'a>(
    certification: &'a HashMap<&'a str, CelValue<'a>>,
) -> CelParserResult<Option<DefaultRequestCertification<'a>>> {
    let no_request_certification = certification.get("no_request_certification");
    let request_certification = certification.get("request_certification");

    match (no_request_certification, request_certification) {
        (Some(_), Some(_)) => Err(CelParserError::ExtraneousRequestCertificationProperty),
        (None, None) => Err(CelParserError::MissingRequestCertificationProperty),
        (Some(_), None) => Ok(None),
        (None, Some(request_certification)) => {
            let request_certification =
                validate_object(request_certification, "RequestCertification")?;

            let Some(certified_request_headers) =
                request_certification.get("certified_request_headers")
            else {
                return Err(CelParserError::MissingObjectProperty {
                    object_name: "RequestCertification".into(),
                    expected_property_name: "certified_request_headers".into(),
                });
            };
            let certified_request_headers =
                validate_string_array(certified_request_headers, "certified_request_headers")?;

            let Some(certified_query_parameters) =
                request_certification.get("certified_query_parameters")
            else {
                return Err(CelParserError::MissingObjectProperty {
                    object_name: "RequestCertification".into(),
                    expected_property_name: "certified_query_parameters".into(),
                });
            };
            let certified_query_parameters =
                validate_string_array(certified_query_parameters, "certified_query_parameters")?;

            Ok(Some(DefaultRequestCertification::new(
                certified_request_headers,
                certified_query_parameters,
            )))
        }
    }
}

fn validate_response_certification<'a>(
    certification: &'a HashMap<&'a str, CelValue<'a>>,
) -> CelParserResult<DefaultResponseCertification<'a>> {
    let Some(response_certification) = certification.get("response_certification") else {
        return Err(CelParserError::MissingObjectProperty {
            object_name: "RequestCertification".into(),
            expected_property_name: "response_certification".into(),
        });
    };
    let response_certification = validate_object(response_certification, "ResponseCertification")?;

    let get_response_certification_headers =
        |property_name| -> CelParserResult<Option<Vec<&'a str>>> {
            response_certification
                .get(property_name)
                .map(|certified_response_headers| {
                    validate_object(certified_response_headers, "ResponseHeaderList")
                })
                .transpose()?
                .and_then(|certified_response_headers| certified_response_headers.get("headers"))
                .map(|headers| validate_string_array(headers, property_name))
                .transpose()
        };

    let certified_response_headers =
        get_response_certification_headers("certified_response_headers")?;

    let response_header_exclusions =
        get_response_certification_headers("response_header_exclusions")?;

    match (certified_response_headers, response_header_exclusions) {
        (Some(_), Some(_)) => Err(CelParserError::ExtraneousResponseCertificationProperty),
        (None, None) => Err(CelParserError::MissingResponseCertificationProperty),
        (Some(headers), None) => Ok(DefaultResponseCertification::certified_response_headers(
            headers,
        )),
        (None, Some(headers)) => Ok(DefaultResponseCertification::response_header_exclusions(
            headers,
        )),
    }
}

pub(crate) fn map_cel_ast<'a>(cel: &'a CelValue<'a>) -> CelParserResult<CelExpression<'a>> {
    let default_certification = validate_function(cel, "default_certification")?;

    let Some(validation_args) = default_certification.first() else {
        return Err(CelParserError::MissingFunctionParameter {
            function_name: "default_certification".into(),
            parameter_name: "ValidationArgs".into(),
            parameter_type: "Object".into(),
            parameter_position: 0,
        });
    };

    let validation_args = validate_object(validation_args, "ValidationArgs")?;

    let no_certification = validation_args.get("no_certification");
    let certification = validation_args.get("certification");

    match (no_certification, certification) {
        (Some(_), Some(_)) => Err(CelParserError::ExtraneousValidationArgsProperty),
        (None, None) => Err(CelParserError::MissingValidationArgsProperty),
        (Some(_), None) => Ok(CelExpression::Default(DefaultCelExpression::Skip)),
        (None, Some(certification)) => {
            let certification = validate_object(certification, "Certification")?;

            let request_certification = validate_request_certification(certification)?;

            let response_certification = validate_response_certification(certification)?;

            let Some(request_certification) = request_certification else {
                return Ok(CelExpression::Default(DefaultCelExpression::ResponseOnly(
                    DefaultResponseOnlyCelExpression {
                        response: response_certification,
                    },
                )));
            };

            Ok(CelExpression::Default(DefaultCelExpression::Full(
                DefaultFullCelExpression {
                    request: request_certification,
                    response: response_certification,
                },
            )))
        }
    }
}