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