mod client; pub(crate) mod request; use std::{borrow::Cow, collections::HashMap, fmt::Debug, future::Future, str, time::Instant}; use actix_web::{body, HttpRequest, HttpResponse, Responder}; use bytes::Bytes; use error_stack::{report, IntoReport, Report, ResultExt}; use masking::ExposeOptionInterface; use router_env::{ tracing::{self, instrument}, Tag, }; use serde::Serialize; use self::request::{ContentType, HeaderExt, RequestBuilderExt}; pub use self::request::{Method, Request, RequestBuilder}; use crate::{ configs::settings::Connectors, core::{ errors::{self, CustomResult, RouterResponse, RouterResult, StorageErrorExt}, payments, }, db::StorageInterface, logger, routes, routes::AppState, types::{ self, api, storage::{self, enums}, ErrorResponse, Response, }, utils::{self, OptionExt}, }; pub type BoxedConnectorIntegration<'a, T, Req, Resp> = Box<&'a (dyn ConnectorIntegration + Send + Sync)>; pub trait ConnectorIntegrationExt: Send + Sync + 'static { fn get_connector_integration(&self) -> BoxedConnectorIntegration; } impl ConnectorIntegrationExt for S where S: ConnectorIntegration + Send + Sync, { fn get_connector_integration(&self) -> BoxedConnectorIntegration<'_, T, Req, Resp> { Box::new(self) } } pub trait ConnectorIntegration: ConnectorIntegrationExt { fn get_headers( &self, _req: &types::RouterData, ) -> CustomResult, errors::ConnectorError> { Ok(vec![]) } fn get_content_type(&self) -> &'static str { mime::APPLICATION_JSON.essence_str() } 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) } fn build_request( &self, _req: &types::RouterData, _connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(None) } fn handle_response( &self, data: &types::RouterData, _res: Response, ) -> CustomResult, errors::ConnectorError> where T: Clone, Req: Clone, Resp: Clone, { Ok(data.clone()) } fn get_error_response( &self, _res: Bytes, ) -> 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) => { let response = call_connector_api(state, request).await; 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.response)?; router_data.response = Err(error); router_data } }; logger::debug!(?response); Ok(response) } Err(error) => Err(error .change_context(errors::ConnectorError::ProcessingStepFailed(None))), } } None => Ok(router_data), } } } } #[instrument(skip_all)] pub(crate) 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, crate::consts::REQUEST_TIME_OUT, request.certificate, request.certificate_key, )?; let headers = request.headers.construct_header_map()?; match request.method { Method::Get => client.get(url).add_headers(headers).send().await, Method::Post => { let client = client.post(url).add_headers(headers); match request.content_type { Some(ContentType::Json) => client.json(&request.payload).send(), // 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).send() } None => client .body(request.payload.expose_option().unwrap_or_default()) .send(), } .await } Method::Put => client.put(url).add_headers(headers).send().await, Method::Delete => client.delete(url).add_headers(headers).send().await, } .map_err(|error| match error { error if error.is_timeout() => errors::ApiClientError::RequestTimeoutReceived, _ => errors::ApiClientError::RequestNotSent, }) .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 => { 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(Response { response, status_code, })) } status_code @ 500..=599 => { let error = match status_code { 500 => errors::ApiClientError::InternalServerErrorReceived, 502 => errors::ApiClientError::BadGatewayReceived, 503 => errors::ApiClientError::ServiceUnavailableReceived, 504 => errors::ApiClientError::GatewayTimeoutReceived, _ => errors::ApiClientError::UnexpectedServerResponse, }; Err(Report::new(error).attach_printable("Server error response received")) } 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(Response { response: bytes, status_code, })) } _ => Err(report!(errors::ApiClientError::UnexpectedServerResponse) .attach_printable("Unexpected response from server")), } })? .await } #[derive(Debug, Eq, PartialEq)] pub enum BachResponse { Json(R), StatusOk, TextPlain(String), /* redirect form not used https://juspay.atlassian.net/browse/ORCA-301 RedirectResponse(BachRedirectResponse), Form(BachRedirectForm), */ JsonForRedirection(api::RedirectionResponse), // RedirectResponse(BachRedirectResponse), Form(RedirectForm), } #[derive(Debug, Eq, PartialEq, Serialize)] pub struct BachRedirectResponse { pub url: String, } impl From<&storage::PaymentAttempt> for BachRedirectResponse { 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 url: String, pub method: Method, pub form_fields: HashMap, } impl RedirectForm { pub fn new(url: String, method: Method, form_fields: HashMap) -> Self { Self { url, method, form_fields, } } } #[derive(Clone, Debug)] pub enum ApiAuthentication<'a> { Merchant(MerchantAuthentication<'a>), Connector(ConnectorAuthentication<'a>), } #[derive(Clone, Debug)] pub enum MerchantAuthentication<'a> { ApiKey, MerchantId(Cow<'a, str>), AdminApiKey, PublishableKey, } #[derive(Clone, Debug)] pub enum ConnectorAuthentication<'a> { MerchantId(&'a str), } impl<'a> From> for ApiAuthentication<'a> { fn from(merchant_auth: MerchantAuthentication<'a>) -> Self { ApiAuthentication::Merchant(merchant_auth) } } impl<'a> From> for ApiAuthentication<'a> { fn from(connector_auth: ConnectorAuthentication<'a>) -> Self { ApiAuthentication::Connector(connector_auth) } } #[derive(Clone, Copy, PartialEq, Eq)] pub enum AuthFlow { Client, Merchant, } pub(crate) fn get_auth_flow(auth_type: &MerchantAuthentication) -> AuthFlow { match auth_type { MerchantAuthentication::ApiKey => AuthFlow::Merchant, _ => AuthFlow::Client, } } pub(crate) fn get_auth_type(request: &HttpRequest) -> RouterResult { let api_key = get_api_key(request).change_context(errors::ApiErrorResponse::Unauthorized)?; if api_key.starts_with("pk_") { Ok(MerchantAuthentication::PublishableKey) } else { Ok(MerchantAuthentication::ApiKey) } } #[instrument(skip(request, payload, state, func))] pub(crate) async fn server_wrap_util<'a, 'b, T, Q, F, Fut>( state: &'b routes::AppState, request: &'a HttpRequest, payload: T, func: F, api_authentication: ApiAuthentication<'a>, ) -> RouterResult> where F: Fn(&'b routes::AppState, storage::MerchantAccount, T) -> Fut, Fut: Future>, Q: Serialize + Debug + 'a, T: std::fmt::Debug, { let merchant_account = match api_authentication { ApiAuthentication::Merchant(merchant_auth) => { authenticate_merchant(request, &*state.store, merchant_auth).await? } ApiAuthentication::Connector(connector_auth) => { authenticate_connector(request, &*state.store, connector_auth).await? } }; logger::debug!(request=?payload); func(state, merchant_account, payload).await } #[instrument( skip(request, payload, state, func), fields(request_method, request_url_path) )] pub(crate) async fn server_wrap<'a, 'b, A, T, Q, F, Fut>( state: &'b routes::AppState, request: &'a HttpRequest, payload: T, func: F, api_authentication: A, ) -> HttpResponse where A: Into> + Debug, F: Fn(&'b routes::AppState, storage::MerchantAccount, T) -> Fut, Fut: Future>>, Q: Serialize + Debug + 'a, T: std::fmt::Debug, { let api_authentication = api_authentication.into(); 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_authentication).await { Ok(BachResponse::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(BachResponse::StatusOk) => http_response_ok(), Ok(BachResponse::TextPlain(text)) => http_response_plaintext(text), Ok(BachResponse::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(BachResponse::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(crate) fn log_and_return_error_response(error: Report) -> HttpResponse where T: actix_web::ResponseError + error_stack::Context, { logger::error!(?error); error.current_context().error_response() } pub async fn authenticate_merchant<'a>( request: &HttpRequest, store: &dyn StorageInterface, merchant_authentication: MerchantAuthentication<'a>, ) -> RouterResult { match merchant_authentication { MerchantAuthentication::ApiKey => { let api_key = get_api_key(request).change_context(errors::ApiErrorResponse::Unauthorized)?; authenticate_by_api_key(store, api_key).await } MerchantAuthentication::MerchantId(merchant_id) => store .find_merchant_account_by_merchant_id(&merchant_id) .await .map_err(|error| error.to_not_found_response(errors::ApiErrorResponse::Unauthorized)), MerchantAuthentication::AdminApiKey => { let admin_api_key = get_api_key(request).change_context(errors::ApiErrorResponse::Unauthorized)?; if admin_api_key != "test_admin" { Err(report!(errors::ApiErrorResponse::Unauthorized) .attach_printable("Admin Authentication Failure"))?; } Ok(storage::MerchantAccount { id: -1, merchant_id: String::from("juspay"), merchant_name: None, api_key: None, merchant_details: None, return_url: None, webhook_details: None, routing_algorithm: None, custom_routing_rules: None, sub_merchants_enabled: None, parent_merchant_id: None, enable_payment_response_hash: false, payment_response_hash_key: None, redirect_to_merchant_with_http_post: false, publishable_key: None, storage_scheme: enums::MerchantStorageScheme::PostgresOnly, }) } MerchantAuthentication::PublishableKey => { let api_key = get_api_key(request).change_context(errors::ApiErrorResponse::Unauthorized)?; authenticate_by_publishable_key(store, api_key).await } } } pub async fn authenticate_connector<'a>( _request: &HttpRequest, store: &dyn StorageInterface, connector_authentication: ConnectorAuthentication<'a>, ) -> RouterResult { match connector_authentication { ConnectorAuthentication::MerchantId(merchant_id) => store .find_merchant_account_by_merchant_id(merchant_id) .await .map_err(|error| error.to_not_found_response(errors::ApiErrorResponse::Unauthorized)), } } pub(crate) fn get_auth_type_and_check_client_secret

( req: &actix_web::HttpRequest, payload: P, ) -> RouterResult<(P, MerchantAuthentication)> where P: Authenticate, { let auth_type = get_auth_type(req)?; Ok(( payments::helpers::client_secret_auth(payload, &auth_type)?, auth_type, )) } pub(crate) async fn authenticate_eph_key<'a>( req: &'a actix_web::HttpRequest, store: &dyn StorageInterface, customer_id: String, ) -> RouterResult> { let api_key = get_api_key(req)?; if api_key.starts_with("epk") { let ek = store .get_ephemeral_key(api_key) .await .change_context(errors::ApiErrorResponse::Unauthorized)?; utils::when(ek.customer_id.ne(&customer_id), || { Err(report!(errors::ApiErrorResponse::InvalidEphermeralKey)) })?; Ok(MerchantAuthentication::MerchantId(Cow::Owned( ek.merchant_id, ))) } else { Ok(MerchantAuthentication::ApiKey) } } fn get_api_key(req: &HttpRequest) -> RouterResult<&str> { req.headers() .get("api-key") .get_required_value("api-key")? .to_str() .into_report() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to convert API key to string") } 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") } async fn authenticate_by_publishable_key( store: &dyn StorageInterface, publishable_key: &str, ) -> RouterResult { store .find_merchant_account_by_publishable_key(publishable_key) .await .change_context(errors::ApiErrorResponse::Unauthorized) .attach_printable("Merchant not authenticated") } pub(crate) fn http_response_json(response: T) -> HttpResponse { HttpResponse::Ok() .content_type("application/json") .append_header(("Via", "Juspay_router")) .body(response) } pub(crate) fn http_response_plaintext(res: T) -> HttpResponse { HttpResponse::Ok() .content_type("text/plain") .append_header(("Via", "Juspay_router")) .body(res) } pub(crate) fn http_response_ok() -> HttpResponse { HttpResponse::Ok().finish() } pub(crate) fn http_redirect_response( response: T, redirection_response: api::RedirectionResponse, ) -> HttpResponse { HttpResponse::Ok() .content_type("application/json") .append_header(("Via", "Juspay_router")) .append_header(( "Location", redirection_response.return_url_with_query_params, )) .status(http::StatusCode::FOUND) .body(response) } pub(crate) fn http_response_err(response: T) -> HttpResponse { HttpResponse::BadRequest() .content_type("application/json") .append_header(("Via", "Juspay_router")) .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::ListPaymentMethodRequest { 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 { "#loading { -webkit-animation: rotation 2s infinite linear; } @-webkit-keyframes rotation{ from { -webkit-transform: rotate(0deg); } to { -webkit-transform: rotate(359deg); } }" } } body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" { img #loading style="width: 60px; display: block; position: relative; margin-left: auto; margin-right: auto;" src="data:image/png;base64,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"; h3 style="text-align: center;" { "Please wait while we process your payment..." } form action=(PreEscaped(&form.url)) method=(form.method.to_string()) #payment_form { @for (field, value) in &form.form_fields { input type="hidden" name=(field) value=(value); } } (maud::PreEscaped(r#""#)) } } } } #[cfg(test)] mod tests { #[test] fn test_mime_essence() { assert_eq!(mime::APPLICATION_JSON.essence_str(), "application/json"); } }