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