-
Notifications
You must be signed in to change notification settings - Fork 4.2k
feat(router): Add support for Proxy api #7901
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
861d6d5
ef4ca7a
77e6cdc
dde789a
d5638f5
7156896
6712bf8
5cf3510
51e1919
794ff40
1d10061
3f995d8
2d1ade7
3c73bad
6a78b23
627e6b4
33467f7
552f74c
4d7f97c
9f16eb3
8871c3a
eef8450
93f7242
548bbdc
dd05e03
7238311
2256117
135ffbf
5c33ec2
484cf3c
1db2293
d1f2cb7
8943a24
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
use serde::{Deserialize, Serialize}; | ||
use serde_json::Value; | ||
use utoipa::ToSchema; | ||
|
||
#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] | ||
pub struct ProxyRequest { | ||
/// The request body that needs to be forwarded | ||
pub req_body: Value, | ||
/// The destination URL where the request needs to be forwarded | ||
#[schema(example = "https://api.example.com/endpoint")] | ||
pub destination_url: String, | ||
/// The headers that need to be forwarded | ||
pub headers: Value, | ||
prasunna09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// The vault token that is used to fetch sensitive data from the vault | ||
pub token: String, | ||
/// The type of token that is used to fetch sensitive data from the vault | ||
pub token_type: TokenType | ||
} | ||
|
||
#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] | ||
pub enum TokenType { | ||
TokenizationId, | ||
PaymentMethodId | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When would we use PaymentMethodId here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we ll be using payment method sdk to collect payment method data from customer, then will be stored in payment method table, now merchant can use payment method id for proxy |
||
} | ||
|
||
#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] | ||
pub struct ProxyResponse { | ||
/// The response received from the destination | ||
pub response: Value, | ||
pub status_code: u16, | ||
pub response_headers: Value, | ||
} | ||
|
||
impl common_utils::events::ApiEventMetric for ProxyRequest {} | ||
impl common_utils::events::ApiEventMetric for ProxyResponse {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
pub mod state; | ||
pub mod state; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,262 @@ | ||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] | ||
prasunna09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
use api_models::proxy as proxy_api_models; | ||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] | ||
use common_utils::{ext_traits::BytesExt, request}; | ||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] | ||
use error_stack::ResultExt; | ||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] | ||
use x509_parser::nom::{ | ||
bytes::complete::{tag, take_while1}, | ||
character::complete::{char, multispace0}, | ||
sequence::{delimited, preceded, terminated}, | ||
IResult, | ||
}; | ||
|
||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] | ||
use super::errors::{self, RouterResponse, RouterResult}; | ||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] | ||
use crate::{ | ||
logger, | ||
routes::SessionState, | ||
services::{self, request::Mask}, | ||
types::domain, | ||
}; | ||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] | ||
use serde_json::Value; | ||
|
||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] | ||
#[derive(Debug)] | ||
struct TokenReference { | ||
field: String, | ||
} | ||
|
||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] | ||
fn parse_token(input: &str) -> IResult<&str, TokenReference> { | ||
prasunna09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let (input, field) = delimited( | ||
tag("{{"), | ||
preceded( | ||
multispace0, | ||
preceded( | ||
char('$'), | ||
terminated( | ||
take_while1(|c: char| c.is_alphanumeric() || c == '_'), | ||
multispace0, | ||
), | ||
), | ||
), | ||
tag("}}"), | ||
)(input)?; | ||
|
||
Ok(( | ||
input, | ||
TokenReference { | ||
field: field.to_string(), | ||
}, | ||
)) | ||
} | ||
|
||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] | ||
fn contains_token(s: &str) -> bool { | ||
s.contains("{{") && s.contains("$") && s.contains("}}") | ||
} | ||
|
||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] | ||
async fn process_value( | ||
prasunna09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
state: &SessionState, | ||
merchant_account: &domain::MerchantAccount, | ||
prasunna09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
value: Value, | ||
token: &str, | ||
vault_data: &Value, | ||
) -> RouterResult<Value> { | ||
match value { | ||
Value::Object(obj) => { | ||
let mut new_obj = serde_json::Map::new(); | ||
|
||
for (key, val) in obj { | ||
prasunna09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let processed = Box::pin(process_value( | ||
state, | ||
merchant_account, | ||
val, | ||
token, | ||
vault_data, | ||
)) | ||
.await?; | ||
new_obj.insert(key, processed); | ||
} | ||
|
||
Ok(Value::Object(new_obj)) | ||
} | ||
Value::String(s) => { | ||
if contains_token(&s) { | ||
// Check if string contains multiple tokens | ||
if s.matches("{{").count() > 1 { | ||
let mut result = s.clone(); | ||
let mut tokens_processed = true; | ||
|
||
while result.contains("{{") && result.contains("}}") { | ||
let start = result.find("{{").unwrap(); | ||
prasunna09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let end = result[start..] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we try to use nom parser for multi token parsing logic as well somehow instead of manual parsing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, will be taking this multi token parsing in following pr, for now, reverted back these changes |
||
.find("}}") | ||
.map(|pos| start + pos + 2) | ||
.unwrap_or(result.len()); | ||
|
||
if let Ok((_, token_ref)) = parse_token(&result[start..end]) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. instead of slicing, can use get methods to avoid any kind of panics? |
||
if let Ok(field_value) = | ||
extract_field_from_vault_data(vault_data, &token_ref.field) | ||
{ | ||
let value_str = match field_value { | ||
Value::String(s) => s, | ||
_ => field_value.to_string(), | ||
}; | ||
result = result[0..start].to_string() + &value_str + &result[end..]; | ||
} else { | ||
tokens_processed = false; | ||
break; | ||
} | ||
} else { | ||
tokens_processed = false; | ||
break; | ||
} | ||
} | ||
|
||
if tokens_processed { | ||
Ok(Value::String(result)) | ||
} else { | ||
Ok(Value::String(s)) | ||
} | ||
} else { | ||
if let Ok((_, token_ref)) = parse_token(&s) { | ||
extract_field_from_vault_data(vault_data, &token_ref.field) | ||
} else { | ||
Ok(Value::String(s)) | ||
} | ||
} | ||
} else { | ||
Ok(Value::String(s)) | ||
} | ||
} | ||
_ => Ok(value), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should handle There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, will handle it in separate pr |
||
} | ||
} | ||
|
||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] | ||
fn extract_field_from_vault_data(vault_data: &Value, field_name: &str) -> RouterResult<Value> { | ||
let result = match vault_data { | ||
Value::Object(obj) => obj.get(field_name).cloned().or_else(|| { | ||
obj.values() | ||
.filter_map(|val| { | ||
if let Value::Object(inner_obj) = val { | ||
prasunna09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
inner_obj.get(field_name).cloned().or_else(|| { | ||
inner_obj | ||
.values() | ||
.filter_map(|deeper_val| { | ||
if let Value::Object(deepest_obj) = deeper_val { | ||
deepest_obj.get(field_name).cloned() | ||
} else { | ||
None | ||
} | ||
}) | ||
.next() | ||
}) | ||
} else { | ||
None | ||
} | ||
}) | ||
.next() | ||
}), | ||
_ => None, | ||
}; | ||
|
||
match result { | ||
prasunna09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Some(value) => Ok(value), | ||
None => { | ||
logger::debug!( | ||
"Field '{}' not found in vault data: {:?}", | ||
field_name, | ||
vault_data | ||
); | ||
Err(errors::ApiErrorResponse::InternalServerError) | ||
.attach_printable(format!("Field '{}' not found", field_name)) | ||
prasunna09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
} | ||
|
||
#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] | ||
pub async fn proxy_core( | ||
state: SessionState, | ||
merchant_account: domain::MerchantAccount, | ||
req: proxy_api_models::ProxyRequest, | ||
) -> RouterResponse<proxy_api_models::ProxyResponse> { | ||
let token = &req.token; | ||
//TODO: match on token type, | ||
//if token_type is tokenization id then fetch vault id from tokenization table | ||
//else if token_type is payment method id then fetch vault id from payment method table | ||
let vault_id = domain::VaultId::generate(token.clone()); | ||
|
||
let vault_response = super::payment_methods::vault::retrieve_payment_method_from_vault( | ||
&state, | ||
&merchant_account, | ||
&vault_id, | ||
) | ||
.await | ||
.map_err(|_| errors::ApiErrorResponse::InternalServerError)?; | ||
prasunna09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
let vault_data = serde_json::to_value(&vault_response.data) | ||
prasunna09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.map_err(|_| errors::ApiErrorResponse::InternalServerError)?; | ||
|
||
let processed_body = | ||
process_value(&state, &merchant_account, req.req_body, token, &vault_data).await?; | ||
|
||
let mut request = services::Request::new(services::Method::Post, &req.destination_url); | ||
prasunna09 marked this conversation as resolved.
Show resolved
Hide resolved
prasunna09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
request.set_body(request::RequestContent::Json(Box::new(processed_body))); | ||
|
||
if let Value::Object(headers) = req.headers { | ||
headers.iter().for_each(|(key, value)| { | ||
let header_value = match value { | ||
Value::String(value_str) => value_str.clone(), | ||
_ => value.to_string(), | ||
} | ||
.into_masked(); | ||
request.add_header(key, header_value); | ||
}); | ||
} | ||
|
||
let response = services::call_connector_api(&state, request, "proxy") | ||
.await | ||
.change_context(errors::ApiErrorResponse::InternalServerError); | ||
prasunna09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let res = response | ||
.change_context(errors::ApiErrorResponse::InternalServerError) | ||
.attach_printable("Error while receiving response") | ||
.and_then(|inner| match inner { | ||
Err(err_res) => | ||
{ | ||
logger::error!("Response Deserialization Failed: {err_res:?}"); | ||
prasunna09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Ok(err_res) | ||
} | ||
Ok(res) => Ok(res), | ||
}) | ||
.inspect_err(|_| {})?; | ||
prasunna09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
let response_body: Value = res | ||
prasunna09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.response | ||
.parse_struct("ProxyResponse") | ||
.change_context(errors::ApiErrorResponse::InternalServerError)?; | ||
prasunna09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
let status_code = res.status_code; | ||
let response_headers = res.headers.as_ref() | ||
.map(|h| { | ||
let map: std::collections::BTreeMap<_, _> = h.iter() | ||
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) | ||
.collect(); | ||
serde_json::to_value(map).unwrap_or_else(|_| serde_json::json!({})) | ||
}) | ||
.unwrap_or_else(|| serde_json::json!({})); | ||
|
||
Ok(services::ApplicationResponse::Json( | ||
proxy_api_models::ProxyResponse { | ||
response: response_body, | ||
status_code, | ||
response_headers, | ||
}, | ||
)) | ||
} |
Uh oh!
There was an error while loading. Please reload this page.