1use crate::ohttp_client::OhttpSession;
6use once_cell::sync::Lazy;
7use parking_lot::RwLock;
8use std::collections::HashMap;
9use std::time::{Duration, SystemTime};
10use url::Url;
11
12use crate::{Headers, Method, Request, Response, Result, ViaductError};
13
14async fn send_request(request: Request, settings: crate::ClientSettings) -> Result<Response> {
16 let backend = crate::backend::get_backend()?;
17 backend.send_request(request, settings).await
18}
19
20#[derive(Debug, Clone, uniffi::Record)]
22pub struct OhttpConfig {
23 pub relay_url: String,
25 pub gateway_host: String,
27}
28
29#[derive(Debug, Clone)]
31struct CachedGatewayConfig {
32 config_data: Vec<u8>,
33 expires_at: SystemTime,
34}
35
36static OHTTP_CHANNELS: Lazy<RwLock<HashMap<String, OhttpConfig>>> =
38 Lazy::new(|| RwLock::new(HashMap::new()));
39
40static CONFIG_CACHE: Lazy<RwLock<HashMap<String, CachedGatewayConfig>>> =
42 Lazy::new(|| RwLock::new(HashMap::new()));
43
44#[uniffi::export]
47pub fn configure_ohttp_channel(channel: String, config: OhttpConfig) -> Result<()> {
48 crate::trace!(
49 "Configuring OHTTP channel '{}' with relay: {}, gateway: {}",
50 channel,
51 config.relay_url,
52 config.gateway_host
53 );
54
55 let parsed_relay = Url::parse(&config.relay_url)?;
57 crate::trace!(
58 "Relay URL validated: scheme={}, host={:?}",
59 parsed_relay.scheme(),
60 parsed_relay.host_str()
61 );
62
63 if config.gateway_host.is_empty() {
65 return Err(crate::ViaductError::NetworkError(
66 "Gateway host cannot be empty".to_string(),
67 ));
68 }
69 crate::trace!("Gateway host validated: {}", config.gateway_host);
70
71 OHTTP_CHANNELS.write().insert(channel.clone(), config);
72 crate::trace!("OHTTP channel '{}' configured successfully", channel);
73 Ok(())
74}
75
76#[uniffi::export]
81pub fn configure_default_ohttp_channels() -> Result<()> {
82 crate::trace!("Configuring default OHTTP channels");
83
84 configure_ohttp_channel(
87 "relay1".to_string(),
88 OhttpConfig {
89 relay_url: "https://mozilla-ohttp.fastly-edge.com/".to_string(),
90 gateway_host: "prod.ohttp-gateway.prod.webservices.mozgcp.net".to_string(),
91 },
92 )?;
93
94 configure_ohttp_channel(
96 "merino".to_string(),
97 OhttpConfig {
98 relay_url: "https://ohttp-relay-merino-prod.edgecompute.app/".to_string(),
99 gateway_host: "prod.merino.prod.webservices.mozgcp.net".to_string(),
100 },
101 )?;
102
103 crate::trace!("Default OHTTP channels configured successfully");
104 Ok(())
105}
106
107#[uniffi::export]
109pub fn clear_ohttp_channels() {
110 crate::trace!("Clearing all OHTTP channel configurations");
111 OHTTP_CHANNELS.write().clear();
112 CONFIG_CACHE.write().clear();
113}
114
115pub fn get_ohttp_config(channel: &str) -> Result<OhttpConfig> {
117 crate::trace!("Looking up OHTTP config for channel: {}", channel);
118 let channels = OHTTP_CHANNELS.read();
119 match channels.get(channel) {
120 Some(config) => {
121 crate::trace!(
122 "Found OHTTP config for channel '{}': relay={}, gateway={}",
123 channel,
124 config.relay_url,
125 config.gateway_host
126 );
127 Ok(config.clone())
128 }
129 None => {
130 let available_channels: Vec<_> = channels.keys().collect();
131 crate::error!(
132 "OHTTP channel '{}' not configured. Available channels: {:?}",
133 channel,
134 available_channels
135 );
136 Err(ViaductError::OhttpChannelNotConfigured(channel.to_string()))
137 }
138 }
139}
140
141pub fn is_ohttp_channel_configured(channel: &str) -> bool {
143 OHTTP_CHANNELS.read().contains_key(channel)
144}
145
146#[uniffi::export]
148pub fn list_ohttp_channels() -> Vec<String> {
149 OHTTP_CHANNELS.read().keys().cloned().collect()
150}
151
152pub async fn fetch_gateway_config(gateway_host: &str) -> Result<Vec<u8>> {
154 if let Some(cached) = read_config_from_cache(gateway_host) {
155 return Ok(cached);
156 }
157
158 let config_data = fetch_config_from_network(gateway_host).await?;
162
163 {
165 let mut cache = CONFIG_CACHE.write();
166 cache.insert(
167 gateway_host.to_string(),
168 CachedGatewayConfig {
169 config_data: config_data.clone(),
170 expires_at: SystemTime::now() + Duration::from_secs(60 * 60 * 24),
172 },
173 );
174 }
175
176 Ok(config_data)
177}
178
179fn read_config_from_cache(gateway_host: &str) -> Option<Vec<u8>> {
181 let cache = CONFIG_CACHE.read();
182 check_cache_entry(&cache, gateway_host)
183}
184
185fn check_cache_entry(
187 cache: &HashMap<String, CachedGatewayConfig>,
188 gateway_host: &str,
189) -> Option<Vec<u8>> {
190 cache.get(gateway_host).and_then(|cached| {
191 if cached.expires_at > SystemTime::now() {
192 crate::trace!("Using cached config for gateway: {}", gateway_host);
193 Some(cached.config_data.clone())
194 } else {
195 crate::trace!("Cached config for {} has expired", gateway_host);
196 None
197 }
198 })
199}
200
201async fn fetch_config_from_network(gateway_host: &str) -> Result<Vec<u8>> {
203 let gateway_url = format!("https://{}", gateway_host);
204 let config_url = Url::parse(&gateway_url)?.join("ohttp-configs")?;
205
206 let request = Request::get(config_url.clone());
207 let settings = crate::ClientSettings {
208 timeout: 10000,
209 redirect_limit: 5,
210 ..crate::ClientSettings::default()
211 };
212
213 let response = send_request(request, settings).await?;
214
215 if !response.is_success() {
216 return Err(ViaductError::OhttpConfigFetchFailed(format!(
217 "Failed to fetch config from {}: HTTP {}",
218 config_url, response.status
219 )));
220 }
221
222 let config_data = response.body;
223 if config_data.is_empty() {
224 return Err(ViaductError::OhttpConfigFetchFailed(
225 "Empty config received from gateway".to_string(),
226 ));
227 }
228
229 crate::trace!("Successfully fetched {} bytes", config_data.len());
230 Ok(config_data)
231}
232
233pub async fn process_ohttp_request(
235 request: Request,
236 channel: &str,
237 settings: crate::ClientSettings,
238) -> Result<Response> {
239 let overall_start = std::time::Instant::now();
240 crate::trace!(
241 "=== Starting OHTTP request processing for channel: '{}' ===",
242 channel
243 );
244 crate::trace!("Target URL: {} {}", request.method, request.url);
245
246 let config = get_ohttp_config(channel)?;
247 crate::trace!(
248 "Retrieved OHTTP config - relay: {}, gateway: {}",
249 config.relay_url,
250 config.gateway_host
251 );
252
253 crate::trace!(
255 "Step 1: Fetching gateway encryption keys from: {}",
256 config.gateway_host
257 );
258 let gateway_config_start = std::time::Instant::now();
259 let gateway_config_data = fetch_gateway_config(&config.gateway_host).await?;
260 let gateway_config_duration = gateway_config_start.elapsed();
261 crate::trace!(
262 "Gateway config fetched: {} bytes in {:?}",
263 gateway_config_data.len(),
264 gateway_config_duration
265 );
266
267 crate::trace!("Step 2: Creating OHTTP session with gateway keys...");
269 let session_start = std::time::Instant::now();
270 let ohttp_session = OhttpSession::new(&gateway_config_data).map_err(|e| {
271 crate::error!("Failed to create OHTTP session: {}", e);
272 ViaductError::OhttpRequestError(format!("Failed to create OHTTP session: {}", e))
273 })?;
274 let session_duration = session_start.elapsed();
275 crate::trace!(
276 "OHTTP session created successfully in {:?}",
277 session_duration
278 );
279
280 let method = request.method.as_str();
282 let scheme = request.url.scheme();
283 let authority = request.url.host_str().unwrap_or("");
284 let path_and_query = {
285 let mut path = request.url.path().to_string();
286 if let Some(query) = request.url.query() {
287 path.push('?');
288 path.push_str(query);
289 }
290 path
291 };
292 let headers_map: HashMap<String, String> = request.headers.clone().into();
293 let payload = request.body.unwrap_or_default();
294
295 crate::trace!(
296 "Step 3: Preparing request - {} {}://{}{}",
297 method,
298 scheme,
299 authority,
300 path_and_query
301 );
302 crate::trace!("Request headers: {} total", headers_map.len());
303 crate::trace!("Request payload: {} bytes", payload.len());
304
305 crate::trace!("Step 4: Encapsulating request with OHTTP...");
307 let encap_start = std::time::Instant::now();
308 let encrypted_request = ohttp_session
309 .encapsulate(
310 method,
311 scheme,
312 authority,
313 &path_and_query,
314 headers_map,
315 &payload,
316 )
317 .map_err(|e| {
318 crate::error!("Failed to encapsulate request: {}", e);
319 ViaductError::OhttpRequestError(format!("Failed to encapsulate request: {}", e))
320 })?;
321 let encap_duration = encap_start.elapsed();
322 crate::trace!(
323 "Request encapsulated: {} bytes → {} bytes encrypted in {:?}",
324 payload.len(),
325 encrypted_request.len(),
326 encap_duration
327 );
328
329 let relay_url = Url::parse(&config.relay_url)?;
331 crate::trace!("Step 5: Sending encrypted request to relay: {}", relay_url);
332
333 let mut relay_headers = Headers::new();
334 relay_headers.insert("Content-Type", "message/ohttp-req")?;
335
336 let relay_request = Request {
337 method: Method::Post,
338 url: relay_url.clone(),
339 headers: relay_headers,
340 body: Some(encrypted_request),
341 };
342
343 crate::trace!("Sending to relay with timeout: {}ms", settings.timeout);
345 let relay_start = std::time::Instant::now();
346 let relay_response = send_request(relay_request, settings).await?;
347 let relay_duration = relay_start.elapsed();
348
349 crate::trace!(
350 "Relay responded: HTTP {} in {:?}",
351 relay_response.status,
352 relay_duration
353 );
354
355 if !relay_response.is_success() {
357 crate::error!(
358 "OHTTP relay {} returned error: HTTP {} - {}",
359 relay_url,
360 relay_response.status,
361 String::from_utf8_lossy(&relay_response.body)
362 );
363 return Err(ViaductError::OhttpRequestError(format!(
364 "OHTTP relay returned error: HTTP {} - {}",
365 relay_response.status,
366 String::from_utf8_lossy(&relay_response.body)
367 )));
368 }
369
370 if let Some(content_type) = relay_response.headers.get("content-type") {
372 if content_type != "message/ohttp-res" {
373 crate::warn!(
374 "OHTTP relay returned unexpected content-type: {} (expected: message/ohttp-res)",
375 content_type
376 );
377 } else {
378 crate::trace!("Relay response content-type verified: {}", content_type);
379 }
380 } else {
381 crate::warn!("OHTTP relay response missing content-type header");
382 }
383
384 crate::trace!(
386 "Step 6: Decapsulating response ({} bytes from relay)...",
387 relay_response.body.len()
388 );
389 let decap_start = std::time::Instant::now();
390 let ohttp_response = ohttp_session
391 .decapsulate(&relay_response.body)
392 .map_err(|e| {
393 crate::error!("Failed to decapsulate OHTTP response: {}", e);
394 ViaductError::OhttpResponseError(format!("Failed to decapsulate OHTTP response: {}", e))
395 })?;
396 let decap_duration = decap_start.elapsed();
397
398 let (status, headers_map, body) = ohttp_response.into_parts();
400 let final_headers = Headers::try_from_hashmap(headers_map)?;
401
402 let final_response = Response {
403 request_method: request.method,
404 url: request.url,
405 status,
406 headers: final_headers,
407 body,
408 };
409
410 let overall_duration = overall_start.elapsed();
411 crate::trace!(
412 "=== OHTTP request completed successfully for channel '{}' ===",
413 channel
414 );
415 crate::trace!(
416 "Final result: HTTP {} with {} bytes (total time: {:?})",
417 final_response.status,
418 final_response.body.len(),
419 overall_duration
420 );
421 crate::trace!(
422 "Timing breakdown - Config: {:?}, Session: {:?}, Encap: {:?}, Relay: {:?}, Decap: {:?}",
423 gateway_config_duration,
424 session_duration,
425 encap_duration,
426 relay_duration,
427 decap_duration
428 );
429
430 Ok(final_response)
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn test_channel_configuration() {
439 clear_ohttp_channels();
440
441 let config = OhttpConfig {
442 relay_url: "https://relay.example.com".to_string(),
443 gateway_host: "gateway.example.com".to_string(),
444 };
445
446 configure_ohttp_channel("test".to_string(), config.clone()).unwrap();
447
448 assert!(is_ohttp_channel_configured("test"));
449 assert!(!is_ohttp_channel_configured("nonexistent"));
450
451 let retrieved = get_ohttp_config("test").unwrap();
452 assert_eq!(retrieved.relay_url, config.relay_url);
453 assert_eq!(retrieved.gateway_host, config.gateway_host);
454
455 let channels = list_ohttp_channels();
456 assert_eq!(channels, vec!["test"]);
457
458 clear_ohttp_channels();
459 assert!(!is_ohttp_channel_configured("test"));
460 }
461
462 #[test]
463 fn test_headers_conversion() {
464 let mut headers = Headers::new();
465 headers.insert("Content-Type", "application/json").unwrap();
466 headers.insert("Authorization", "Bearer token").unwrap();
467
468 let map: HashMap<String, String> = headers.clone().into();
469
470 assert_eq!(map.len(), 2);
471 assert_eq!(map.get("content-type").unwrap(), "application/json");
472 assert_eq!(map.get("authorization").unwrap(), "Bearer token");
473
474 let headers_back = Headers::try_from_hashmap(map).unwrap();
475
476 assert_eq!(
477 headers_back.get("Content-Type").unwrap(),
478 "application/json"
479 );
480 assert_eq!(headers_back.get("Authorization").unwrap(), "Bearer token");
481 }
482}