diff --git a/packages/ic-http-gateway/src/error.rs b/packages/ic-http-gateway/src/error.rs index a7028a4..e659704 100644 --- a/packages/ic-http-gateway/src/error.rs +++ b/packages/ic-http-gateway/src/error.rs @@ -7,6 +7,9 @@ pub type HttpGatewayResult = Result; /// HTTP gateway error type. #[derive(thiserror::Error, Debug)] pub enum HttpGatewayError { + #[error(r#"Response verification error: "{0}""#)] + ResponseVerificationError(#[from] ic_response_verification::ResponseVerificationError), + /// Inner error from agent. #[error(r#"Agent error: "{0}""#)] AgentError(#[from] ic_agent::AgentError), diff --git a/packages/ic-http-gateway/src/protocol/handler.rs b/packages/ic-http-gateway/src/protocol/handler.rs index 98f6061..fdef36e 100644 --- a/packages/ic-http-gateway/src/protocol/handler.rs +++ b/packages/ic-http-gateway/src/protocol/handler.rs @@ -91,15 +91,17 @@ pub async fn process_request( let agent_response = match query_result { Ok((response,)) => response, Err(e) => { - let err_res = handle_agent_error(e)?; - - return Ok(HttpGatewayResponse { - canister_response: err_res, - metadata: HttpGatewayResponseMetadata { - upgraded_to_update_call: false, - response_verification_version: None, - }, - }); + return match handle_agent_error(&e) { + None => Err(e.into()), + Some(err_res) => Ok(HttpGatewayResponse { + canister_response: err_res, + metadata: HttpGatewayResponseMetadata { + upgraded_to_update_call: true, + response_verification_version: None, + internal_error: Some(e.into()), + }, + }), + } } }; @@ -118,15 +120,17 @@ pub async fn process_request( match update_result { Ok((response,)) => response, Err(e) => { - let err_res = handle_agent_error(e)?; - - return Ok(HttpGatewayResponse { - canister_response: err_res, - metadata: HttpGatewayResponseMetadata { - upgraded_to_update_call: true, - response_verification_version: None, - }, - }); + return match handle_agent_error(&e) { + None => Err(e.into()), + Some(err_res) => Ok(HttpGatewayResponse { + canister_response: err_res, + metadata: HttpGatewayResponseMetadata { + upgraded_to_update_call: true, + response_verification_version: None, + internal_error: Some(e.into()), + }, + }), + } } } } else { @@ -164,11 +168,14 @@ pub async fn process_request( return Ok(HttpGatewayResponse { canister_response: Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(HttpGatewayResponseBody::Bytes(err.as_bytes().to_vec())) + .body(HttpGatewayResponseBody::Bytes( + err.to_string().as_bytes().to_vec(), + )) .unwrap(), metadata: HttpGatewayResponseMetadata { upgraded_to_update_call: is_update_call, response_verification_version: None, + internal_error: Some(err), }, }); } @@ -210,6 +217,7 @@ pub async fn process_request( response_verification_version: Some( validation_info.verification_version, ), + internal_error: None, }, }); } @@ -249,23 +257,26 @@ pub async fn process_request( metadata: HttpGatewayResponseMetadata { upgraded_to_update_call: is_update_call, response_verification_version: validation_info.map(|e| e.verification_version), + internal_error: None, }, }) } -fn handle_agent_error(error: AgentError) -> HttpGatewayResult { +fn handle_agent_error(error: &AgentError) -> Option { match error { // Turn all `DestinationInvalid`s into 404 AgentError::CertifiedReject(RejectResponse { reject_code: RejectCode::DestinationInvalid, reject_message, .. - }) => Ok(Response::builder() - .status(StatusCode::NOT_FOUND) - .body(HttpGatewayResponseBody::Bytes( - reject_message.as_bytes().to_vec(), - )) - .unwrap()), + }) => Some( + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(HttpGatewayResponseBody::Bytes( + reject_message.as_bytes().to_vec(), + )) + .unwrap(), + ), // If the result is a Replica error, returns the 500 code and message. There is no information // leak here because a user could use `dfx` to get the same reply. @@ -275,22 +286,26 @@ fn handle_agent_error(error: AgentError) -> HttpGatewayResult response.reject_code, response.reject_message, response.error_code, ); - Ok(Response::builder() - .status(StatusCode::BAD_GATEWAY) - .body(HttpGatewayResponseBody::Bytes(msg.as_bytes().to_vec())) - .unwrap()) + Some( + Response::builder() + .status(StatusCode::BAD_GATEWAY) + .body(HttpGatewayResponseBody::Bytes(msg.as_bytes().to_vec())) + .unwrap(), + ) } AgentError::UncertifiedReject(RejectResponse { reject_code: RejectCode::DestinationInvalid, reject_message, .. - }) => Ok(Response::builder() - .status(StatusCode::NOT_FOUND) - .body(HttpGatewayResponseBody::Bytes( - reject_message.as_bytes().to_vec(), - )) - .unwrap()), + }) => Some( + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(HttpGatewayResponseBody::Bytes( + reject_message.as_bytes().to_vec(), + )) + .unwrap(), + ), // If the result is a Replica error, returns the 500 code and message. There is no information // leak here because a user could use `dfx` to get the same reply. @@ -300,21 +315,25 @@ fn handle_agent_error(error: AgentError) -> HttpGatewayResult response.reject_code, response.reject_message, response.error_code, ); - Ok(Response::builder() - .status(StatusCode::BAD_GATEWAY) - .body(HttpGatewayResponseBody::Bytes(msg.as_bytes().to_vec())) - .unwrap()) + Some( + Response::builder() + .status(StatusCode::BAD_GATEWAY) + .body(HttpGatewayResponseBody::Bytes(msg.as_bytes().to_vec())) + .unwrap(), + ) } - AgentError::ResponseSizeExceededLimit() => Ok(Response::builder() - .status(StatusCode::INSUFFICIENT_STORAGE) - .body(HttpGatewayResponseBody::Bytes( - b"Response size exceeds limit".to_vec(), - )) - .unwrap()), + AgentError::ResponseSizeExceededLimit() => Some( + Response::builder() + .status(StatusCode::INSUFFICIENT_STORAGE) + .body(HttpGatewayResponseBody::Bytes( + b"Response size exceeds limit".to_vec(), + )) + .unwrap(), + ), // Handle all other errors - e => Err(e.into()), + _ => None, } } diff --git a/packages/ic-http-gateway/src/protocol/validate.rs b/packages/ic-http-gateway/src/protocol/validate.rs index d5869ce..f433995 100644 --- a/packages/ic-http-gateway/src/protocol/validate.rs +++ b/packages/ic-http-gateway/src/protocol/validate.rs @@ -1,14 +1,11 @@ -use crate::IC_CERTIFICATE_HEADER_NAME; +use crate::{HttpGatewayResult, IC_CERTIFICATE_HEADER_NAME}; use candid::Principal; use ic_agent::Agent; use ic_http_certification::{HttpRequest, HttpResponse}; use ic_response_verification::{ types::VerificationInfo, verify_request_response_pair, MIN_VERIFICATION_VERSION, }; -use std::{ - borrow::Cow, - time::{SystemTime, UNIX_EPOCH}, -}; +use std::time::{SystemTime, UNIX_EPOCH}; const MAX_CERT_TIME_OFFSET_NS: u128 = 300_000_000_000; @@ -18,7 +15,7 @@ pub fn validate( request: HttpRequest, response: HttpResponse, allow_skip_verification: bool, -) -> Result, Cow<'static, str>> { +) -> HttpGatewayResult> { match (allow_skip_verification, has_ic_certificate(&response)) { // TODO: Remove this (FOLLOW-483) // Canisters don't have to provide certified variables @@ -34,8 +31,7 @@ pub fn validate( MAX_CERT_TIME_OFFSET_NS, ic_public_key.as_slice(), MIN_VERIFICATION_VERSION, - ) - .map_err(|_| "Body does not pass verification")?; + )?; Ok(Some(verification_info)) } } diff --git a/packages/ic-http-gateway/src/response/http_gateway_response.rs b/packages/ic-http-gateway/src/response/http_gateway_response.rs index 0c80676..ec17e6a 100644 --- a/packages/ic-http-gateway/src/response/http_gateway_response.rs +++ b/packages/ic-http-gateway/src/response/http_gateway_response.rs @@ -9,6 +9,8 @@ use std::{ task::{Context, Poll}, }; +use crate::HttpGatewayError; + pub type CanisterResponse = Response; /// A response from the HTTP gateway. @@ -22,6 +24,21 @@ pub struct HttpGatewayResponse { pub metadata: HttpGatewayResponseMetadata, } +/// Additional metadata regarding the response. +#[derive(Debug)] +pub struct HttpGatewayResponseMetadata { + /// Whether the original query call was upgraded to an update call. + pub upgraded_to_update_call: bool, + + /// The version of response verification that was used to verify the response. + /// If the protocol fails before getting to the verification step, or the + /// original query call is upgraded to an update call, this field will be `None`. + pub response_verification_version: Option, + + /// The internal error that resulted in the HTTP response being an error response. + pub internal_error: Option, +} + /// The body of an HTTP gateway response. #[derive(Debug)] pub enum HttpGatewayResponseBody { @@ -106,15 +123,3 @@ impl Stream for ResponseBodyStream { self.inner.as_mut().poll_next(cx) } } - -/// Additional metadata regarding the response. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct HttpGatewayResponseMetadata { - /// Whether the original query call was upgraded to an update call. - pub upgraded_to_update_call: bool, - - /// The version of response verification that was used to verify the response. - /// If the protocol fails before getting to the verification step, or the - /// original query call is upgraded to an update call, this field will be `None`. - pub response_verification_version: Option, -} diff --git a/packages/ic-http-gateway/tests/custom_assets.rs b/packages/ic-http-gateway/tests/custom_assets.rs index aab2376..564506a 100644 --- a/packages/ic-http-gateway/tests/custom_assets.rs +++ b/packages/ic-http-gateway/tests/custom_assets.rs @@ -73,11 +73,27 @@ fn test_custom_assets_index_html() { response.canister_response.body(), HttpGatewayResponseBody::Bytes(body) if body == index_html ); - assert_eq!( + + assert_response_metadata( response.metadata, HttpGatewayResponseMetadata { upgraded_to_update_call: false, response_verification_version: Some(2), - } + internal_error: None, + }, + ); +} + +fn assert_response_metadata( + response_metadata: HttpGatewayResponseMetadata, + expected_response_metadata: HttpGatewayResponseMetadata, +) { + assert_eq!( + response_metadata.upgraded_to_update_call, + expected_response_metadata.upgraded_to_update_call + ); + assert_eq!( + response_metadata.response_verification_version, + expected_response_metadata.response_verification_version ); }