1pub(crate) mod body;
2pub(crate) mod client;
3pub(crate) mod params;
4pub(crate) mod resolve;
5
6#[cfg(test)]
7mod body_tests;
8#[cfg(test)]
9mod client_tests;
10#[cfg(test)]
11mod params_tests;
12#[cfg(test)]
13mod resolve_tests;
14
15use crate::error::OpenApiError;
16use crate::registry::OperationHandle;
17
18pub const MAX_RESPONSE_BYTES: usize = 16 * 1024 * 1024;
19
20pub use client::build_dispatch_client;
21
22pub async fn fetch_url_capped(
23 url: &url::Url,
24 cap: usize,
25 label: &str,
26) -> Result<String, OpenApiError> {
27 let (pinned_client, pinned_ip) = resolve::pinned_client_for(url, label).await?;
28 let response = pinned_client
29 .get(url.clone())
30 .send()
31 .await
32 .map_err(|error| map_send_error(error, label))?;
33 resolve::recheck_peer(&response, pinned_ip, label)?;
34 if !response.status().is_success() {
35 return Err(OpenApiError::SpecParse {
36 label: label.to_string(),
37 });
38 }
39 body::collect_spec_capped(response, cap, label).await
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48enum DispatchTrust {
49 Pinned,
53 AllowlistedHost,
58 #[cfg(test)]
62 UnpinnedStrictBody,
63}
64
65impl DispatchTrust {
66 fn enforce_ssrf(self) -> bool {
67 matches!(self, Self::Pinned)
68 }
69
70 fn lenient_body(self) -> bool {
71 matches!(self, Self::AllowlistedHost)
72 }
73}
74
75pub async fn execute_operation(
76 client: &reqwest::Client,
77 op: &OperationHandle,
78 params: serde_json::Value,
79) -> Result<serde_json::Value, OpenApiError> {
80 execute_operation_inner(client, op, params, DispatchTrust::Pinned).await
81}
82
83pub async fn execute_operation_for_allowlisted_host(
97 client: &reqwest::Client,
98 op: &OperationHandle,
99 params: serde_json::Value,
100) -> Result<serde_json::Value, OpenApiError> {
101 execute_operation_inner(client, op, params, DispatchTrust::AllowlistedHost).await
102}
103
104#[cfg(test)]
105pub(crate) async fn execute_operation_no_ssrf(
106 client: &reqwest::Client,
107 op: &OperationHandle,
108 params: serde_json::Value,
109) -> Result<serde_json::Value, OpenApiError> {
110 execute_operation_inner(client, op, params, DispatchTrust::UnpinnedStrictBody).await
111}
112
113async fn execute_operation_inner(
114 client: &reqwest::Client,
115 op: &OperationHandle,
116 params: serde_json::Value,
117 trust: DispatchTrust,
118) -> Result<serde_json::Value, OpenApiError> {
119 let (used_path_params, url) = params::build_url_with_params(op, ¶ms)?;
120 let (send_client, pinned_ip) = if trust.enforce_ssrf() {
121 let (client, ip) = resolve::pinned_client_for(&url, &op.operation_id).await?;
122 (client, Some(ip))
123 } else {
124 (client.clone(), None)
125 };
126
127 let url = params::apply_query(url, op, ¶ms, &used_path_params);
128 let mut request = send_client.request(op.method.clone(), url);
129 request = params::inject_credential(request, op);
130 request = params::apply_body(request, op, ¶ms, &used_path_params);
131
132 let response = request
133 .send()
134 .await
135 .map_err(|error| map_send_error(error, &op.operation_id))?;
136 if let Some(pinned_ip) = pinned_ip {
137 resolve::recheck_peer(&response, pinned_ip, &op.operation_id)?;
138 }
139
140 if !response.status().is_success() {
141 return Err(OpenApiError::UpstreamRequest {
142 label: op.operation_id.clone(),
143 });
144 }
145 let body =
146 body::collect_response_capped(response, MAX_RESPONSE_BYTES, &op.operation_id).await?;
147 if trust.lenient_body() {
148 let parsed = serde_json::from_str::<serde_json::Value>(&body)
149 .unwrap_or_else(|_| serde_json::json!({ "text": body }));
150 return Ok(parsed);
151 }
152 if body.trim().is_empty() {
153 return Ok(serde_json::Value::Null);
154 }
155 serde_json::from_str(&body).map_err(|_| OpenApiError::UpstreamRequest {
156 label: op.operation_id.clone(),
157 })
158}
159
160pub(crate) fn map_send_error(error: reqwest::Error, label: &str) -> OpenApiError {
161 if error.is_timeout() {
162 OpenApiError::UpstreamTimeout {
163 label: label.to_string(),
164 }
165 } else {
166 OpenApiError::UpstreamRequest {
167 label: label.to_string(),
168 }
169 }
170}