mod client; pub(crate) mod request; use std::{ collections::HashMap, fmt::Debug, future::Future, str, time::{Duration, Instant}, }; use actix_web::{body, http::header, HttpRequest, HttpResponse, Responder}; use common_utils::errors::ReportSwitchExt; use error_stack::{report, IntoReport, Report, ResultExt}; use masking::ExposeOptionInterface; use router_env::{instrument, tracing, Tag}; use serde::Serialize; use self::request::{ContentType, HeaderExt, RequestBuilderExt}; pub use self::request::{Method, Request, RequestBuilder}; use crate::{ configs::settings::Connectors, consts, core::{ errors::{self, CustomResult, RouterResult}, payments, }, db::StorageInterface, logger, routes::{app::AppStateInfo, AppState}, services::authentication as auth, types::{self, api, storage, ErrorResponse}, }; pub type BoxedConnectorIntegration<'a, T, Req, Resp> = Box<&'a (dyn ConnectorIntegration + Send + Sync)>; pub trait ConnectorIntegrationAny: Send + Sync + 'static { fn get_connector_integration(&self) -> BoxedConnectorIntegration<'_, T, Req, Resp>; } impl ConnectorIntegrationAny for S where S: ConnectorIntegration + Send + Sync, { fn get_connector_integration(&self) -> BoxedConnectorIntegration<'_, T, Req, Resp> { Box::new(self) } } #[async_trait::async_trait] pub trait ConnectorIntegration: ConnectorIntegrationAny + Sync { fn get_headers( &self, _req: &types::RouterData, _connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(vec![]) } fn get_content_type(&self) -> &'static str { mime::APPLICATION_JSON.essence_str() } /// primarily used when creating signature based on request method of payment flow fn get_http_method(&self) -> Method { Method::Post } fn get_url( &self, _req: &types::RouterData, _connectors: &Connectors, ) -> CustomResult { Ok(String::new()) } fn get_request_body( &self, _req: &types::RouterData, ) -> CustomResult, errors::ConnectorError> { Ok(None) } /// This module can be called before executing a payment flow where a pre-task is needed /// Eg: Some connectors requires one-time session token before making a payment, we can add the session token creation logic in this block async fn execute_pretasks( &self, _router_data: &mut types::RouterData, _app_state: &AppState, ) -> CustomResult<(), errors::ConnectorError> { Ok(()) } /// This module can be called after executing a payment flow where a post-task needed /// Eg: Some connectors require payment sync to happen immediately after the authorize call to complete the transaction, we can add that logic in this block async fn execute_posttasks( &self, _router_data: &mut types::RouterData, _app_state: &AppState, ) -> CustomResult<(), errors::ConnectorError> { Ok(()) } fn build_request( &self, _req: &types::RouterData, _connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(None) } fn handle_response( &self, data: &types::RouterData, _res: types::Response, ) -> CustomResult, errors::ConnectorError> where T: Clone, Req: Clone, Resp: Clone, { Ok(data.clone()) } fn get_error_response( &self, _res: types::Response, ) -> CustomResult { Ok(ErrorResponse::get_not_implemented()) } fn get_certificate( &self, _req: &types::RouterData, ) -> CustomResult, errors::ConnectorError> { Ok(None) } fn get_certificate_key( &self, _req: &types::RouterData, ) -> CustomResult, errors::ConnectorError> { Ok(None) } } #[instrument(skip_all)] pub async fn execute_connector_processing_step< 'b, 'a, T: 'static, Req: Debug + Clone + 'static, Resp: Debug + Clone + 'static, >( state: &'b AppState, connector_integration: BoxedConnectorIntegration<'a, T, Req, Resp>, req: &'b types::RouterData, call_connector_action: payments::CallConnectorAction, ) -> CustomResult, errors::ConnectorError> where T: Clone + Debug, // BoxedConnectorIntegration: 'b, { // If needed add an error stack as follows // connector_integration.build_request(req).attach_printable("Failed to build request"); let mut router_data = req.clone(); match call_connector_action { payments::CallConnectorAction::HandleResponse(res) => { let response = types::Response { response: res.into(), status_code: 200, }; connector_integration.handle_response(req, response) } payments::CallConnectorAction::Avoid => Ok(router_data), payments::CallConnectorAction::StatusUpdate(status) => { router_data.status = status; Ok(router_data) } payments::CallConnectorAction::Trigger => { match connector_integration.build_request(req, &state.conf.connectors)? { Some(request) => { logger::debug!(connector_request=?request); let response = call_connector_api(state, request).await; logger::debug!(connector_response=?response); match response { Ok(body) => { let response = match body { Ok(body) => connector_integration.handle_response(req, body)?, Err(body) => { let error = connector_integration.get_error_response(body)?; router_data.response = Err(error); router_data } }; Ok(response) } Err(error) => Err(error .change_context(errors::ConnectorError::ProcessingStepFailed(None))), } } None => Ok(router_data), } } } } #[instrument(skip_all)] pub async fn call_connector_api( state: &AppState, request: Request, ) -> CustomResult, errors::ApiClientError> { let current_time = Instant::now(); let response = send_request(state, request).await; let elapsed_time = current_time.elapsed(); logger::info!(request_time=?elapsed_time); handle_response(response).await } #[instrument(skip_all)] async fn send_request( state: &AppState, request: Request, ) -> CustomResult { logger::debug!(method=?request.method, headers=?request.headers, payload=?request.payload, ?request); let url = &request.url; let should_bypass_proxy = client::proxy_bypass_urls(&state.conf.locker).contains(url); let client = client::create_client( &state.conf.proxy, should_bypass_proxy, request.certificate, request.certificate_key, )?; let headers = request.headers.construct_header_map()?; match request.method { Method::Get => client.get(url), Method::Post => { let client = client.post(url); match request.content_type { Some(ContentType::Json) => client.json(&request.payload), // Currently this is not used remove this if not required // If using this then handle the serde_part Some(ContentType::FormUrlEncoded) => { let url_encoded_payload = serde_urlencoded::to_string(&request.payload) .into_report() .change_context(errors::ApiClientError::UrlEncodingFailed) .attach_printable_lazy(|| { format!( "Unable to do url encoding on request: {:?}", &request.payload ) })?; logger::debug!(?url_encoded_payload); client.body(url_encoded_payload) } // If payload needs processing the body cannot have default None => client.body(request.payload.expose_option().unwrap_or_default()), } } Method::Put => { client .put(url) .body(request.payload.expose_option().unwrap_or_default()) // If payload needs processing the body cannot have default } Method::Delete => client.delete(url), } .add_headers(headers) .timeout(Duration::from_secs(crate::consts::REQUEST_TIME_OUT)) .send() .await .map_err(|error| match error { error if error.is_timeout() => errors::ApiClientError::RequestTimeoutReceived, _ => errors::ApiClientError::RequestNotSent(error.to_string()), }) .into_report() .attach_printable("Unable to send request to connector") } #[instrument(skip_all)] async fn handle_response( response: CustomResult, ) -> CustomResult, errors::ApiClientError> { response .map(|response| async { logger::info!(?response); let status_code = response.status().as_u16(); match status_code { 200..=202 | 302 => { logger::debug!(response=?response); // If needed add log line // logger:: error!( error_parsing_response=?err); let response = response .bytes() .await .into_report() .change_context(errors::ApiClientError::ResponseDecodingFailed) .attach_printable("Error while waiting for response")?; Ok(Ok(types::Response { response, status_code, })) } status_code @ 500..=599 => { let bytes = response.bytes().await.map_err(|error| { report!(error) .change_context(errors::ApiClientError::ResponseDecodingFailed) .attach_printable("Client error response received") })?; // let error = match status_code { // 500 => errors::ApiClientError::InternalServerErrorReceived, // 502 => errors::ApiClientError::BadGatewayReceived, // 503 => errors::ApiClientError::ServiceUnavailableReceived, // 504 => errors::ApiClientError::GatewayTimeoutReceived, // _ => errors::ApiClientError::UnexpectedServerResponse, // }; Ok(Err(types::Response { response: bytes, status_code, })) } status_code @ 400..=499 => { let bytes = response.bytes().await.map_err(|error| { report!(error) .change_context(errors::ApiClientError::ResponseDecodingFailed) .attach_printable("Client error response received") })?; /* let error = match status_code { 400 => errors::ApiClientError::BadRequestReceived(bytes), 401 => errors::ApiClientError::UnauthorizedReceived(bytes), 403 => errors::ApiClientError::ForbiddenReceived, 404 => errors::ApiClientError::NotFoundReceived(bytes), 405 => errors::ApiClientError::MethodNotAllowedReceived, 408 => errors::ApiClientError::RequestTimeoutReceived, 422 => errors::ApiClientError::UnprocessableEntityReceived(bytes), 429 => errors::ApiClientError::TooManyRequestsReceived, _ => errors::ApiClientError::UnexpectedServerResponse, }; Err(report!(error).attach_printable("Client error response received")) */ Ok(Err(types::Response { response: bytes, status_code, })) } _ => Err(report!(errors::ApiClientError::UnexpectedServerResponse) .attach_printable("Unexpected response from server")), } })? .await } #[derive(Debug, Eq, PartialEq)] pub enum ApplicationResponse { Json(R), StatusOk, TextPlain(String), JsonForRedirection(api::RedirectionResponse), Form(RedirectForm), } #[derive(Debug, Eq, PartialEq, Serialize)] pub struct ApplicationRedirectResponse { pub url: String, } impl From<&storage::PaymentAttempt> for ApplicationRedirectResponse { fn from(payment_attempt: &storage::PaymentAttempt) -> Self { Self { url: format!( "/payments/start/{}/{}/{}", &payment_attempt.payment_id, &payment_attempt.merchant_id, &payment_attempt.attempt_id ), } } } #[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)] pub struct RedirectForm { pub endpoint: String, pub method: Method, pub form_fields: HashMap, } impl From<(url::Url, Method)> for RedirectForm { fn from((mut redirect_url, method): (url::Url, Method)) -> Self { let form_fields = std::collections::HashMap::from_iter( redirect_url .query_pairs() .map(|(key, value)| (key.to_string(), value.to_string())), ); // Do not include query params in the endpoint redirect_url.set_query(None); Self { endpoint: redirect_url.to_string(), method, form_fields, } } } #[derive(Clone, Copy, PartialEq, Eq)] pub enum AuthFlow { Client, Merchant, } #[instrument(skip(request, payload, state, func, api_auth))] pub async fn server_wrap_util<'a, 'b, A, U, T, Q, F, Fut, E, OErr>( state: &'b A, request: &'a HttpRequest, payload: T, func: F, api_auth: &dyn auth::AuthenticateAndFetch, ) -> CustomResult, OErr> where F: Fn(&'b A, U, T) -> Fut, Fut: Future, E>>, Q: Serialize + Debug + 'a, T: Debug, A: AppStateInfo, CustomResult, E>: ReportSwitchExt, OErr>, CustomResult: ReportSwitchExt, { let auth_out = api_auth .authenticate_and_fetch(request.headers(), state) .await .switch()?; func(state, auth_out, payload).await.switch() } #[instrument( skip(request, payload, state, func, api_auth), fields(request_method, request_url_path) )] pub async fn server_wrap<'a, 'b, A, T, U, Q, F, Fut, E>( state: &'b A, request: &'a HttpRequest, payload: T, func: F, api_auth: &dyn auth::AuthenticateAndFetch, ) -> HttpResponse where F: Fn(&'b A, U, T) -> Fut, Fut: Future, E>>, Q: Serialize + Debug + 'a, T: Debug, A: AppStateInfo, CustomResult, E>: ReportSwitchExt, api_models::errors::types::ApiErrorResponse>, { let request_method = request.method().as_str(); let url_path = request.path(); tracing::Span::current().record("request_method", request_method); tracing::Span::current().record("request_url_path", url_path); let start_instant = Instant::now(); logger::info!(tag = ?Tag::BeginRequest); let res = match server_wrap_util(state, request, payload, func, api_auth).await { Ok(ApplicationResponse::Json(response)) => match serde_json::to_string(&response) { Ok(res) => http_response_json(res), Err(_) => http_response_err( r#"{ "error": { "message": "Error serializing response from connector" } }"#, ), }, Ok(ApplicationResponse::StatusOk) => http_response_ok(), Ok(ApplicationResponse::TextPlain(text)) => http_response_plaintext(text), Ok(ApplicationResponse::JsonForRedirection(response)) => { match serde_json::to_string(&response) { Ok(res) => http_redirect_response(res, response), Err(_) => http_response_err( r#"{ "error": { "message": "Error serializing response from connector" } }"#, ), } } Ok(ApplicationResponse::Form(response)) => build_redirection_form(&response) .respond_to(request) .map_into_boxed_body(), Err(error) => log_and_return_error_response(error), }; let response_code = res.status().as_u16(); let end_instant = Instant::now(); let request_duration = end_instant.saturating_duration_since(start_instant); logger::info!( tag = ?Tag::EndRequest, status_code = response_code, time_taken_ms = request_duration.as_millis(), ); res } pub fn log_and_return_error_response(error: Report) -> HttpResponse where T: actix_web::ResponseError + error_stack::Context + Clone, { logger::error!(?error); HttpResponse::from_error(error.current_context().clone()) } pub async fn authenticate_by_api_key( store: &dyn StorageInterface, api_key: &str, ) -> RouterResult { store .find_merchant_account_by_api_key(api_key) .await .change_context(errors::ApiErrorResponse::Unauthorized) .attach_printable("Merchant not authenticated") } pub fn http_response_json(response: T) -> HttpResponse { HttpResponse::Ok() .content_type("application/json") .append_header((header::VIA, "Juspay_router")) .append_header((header::STRICT_TRANSPORT_SECURITY, consts::HSTS_HEADER_VALUE)) .body(response) } pub fn http_response_plaintext(res: T) -> HttpResponse { HttpResponse::Ok() .content_type("text/plain") .append_header((header::VIA, "Juspay_router")) .append_header((header::STRICT_TRANSPORT_SECURITY, consts::HSTS_HEADER_VALUE)) .body(res) } pub fn http_response_ok() -> HttpResponse { HttpResponse::Ok() .append_header((header::VIA, "Juspay_router")) .append_header((header::STRICT_TRANSPORT_SECURITY, consts::HSTS_HEADER_VALUE)) .finish() } pub fn http_redirect_response( response: T, redirection_response: api::RedirectionResponse, ) -> HttpResponse { HttpResponse::Ok() .content_type("application/json") .append_header((header::VIA, "Juspay_router")) .append_header(( "Location", redirection_response.return_url_with_query_params, )) .append_header((header::STRICT_TRANSPORT_SECURITY, consts::HSTS_HEADER_VALUE)) .status(http::StatusCode::FOUND) .body(response) } pub fn http_response_err(response: T) -> HttpResponse { HttpResponse::BadRequest() .content_type("application/json") .append_header((header::VIA, "Juspay_router")) .append_header((header::STRICT_TRANSPORT_SECURITY, consts::HSTS_HEADER_VALUE)) .body(response) } pub trait ConnectorRedirectResponse { fn get_flow_type( &self, _query_params: &str, ) -> CustomResult { Ok(payments::CallConnectorAction::Avoid) } } pub trait Authenticate { fn get_client_secret(&self) -> Option<&String>; } impl Authenticate for api_models::payments::PaymentsRequest { fn get_client_secret(&self) -> Option<&String> { self.client_secret.as_ref() } } impl Authenticate for api_models::payment_methods::PaymentMethodListRequest { fn get_client_secret(&self) -> Option<&String> { self.client_secret.as_ref() } } pub fn build_redirection_form(form: &RedirectForm) -> maud::Markup { use maud::PreEscaped; maud::html! { (maud::DOCTYPE) html { meta name="viewport" content="width=device-width, initial-scale=1"; head { style { r##" "## } (PreEscaped(r##" "##)) } body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" { div id="loader1" class="lottie" style="height: 150px; display: block; position: relative; margin-left: auto; margin-right: auto;" { "" } (PreEscaped(r#""#)) (PreEscaped(r#" "#)) h3 style="text-align: center;" { "Please wait while we process your payment..." } form action=(PreEscaped(&form.endpoint)) method=(form.method.to_string()) #payment_form { @for (field, value) in &form.form_fields { input type="hidden" name=(field) value=(value); } } (PreEscaped(r#""#)) } } } } #[cfg(test)] mod tests { #[test] fn test_mime_essence() { assert_eq!(mime::APPLICATION_JSON.essence_str(), "application/json"); } }