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 ..crate::ClientSettings::default()
206 };
207
208 let response = backend.send_request(request, settings).await?;
209
210 if !response.is_success() {
211 return Err(ViaductError::OhttpConfigFetchFailed(format!(
212 "Failed to fetch config from {}: HTTP {}",
213 config_url, response.status
214 )));
215 }
216
217 let config_data = response.body;
218 if config_data.is_empty() {
219 return Err(ViaductError::OhttpConfigFetchFailed(
220 "Empty config received from gateway".to_string(),
221 ));
222 }
223
224 crate::trace!("Successfully fetched {} bytes", config_data.len());
225 Ok(config_data)
226}
227
228pub async fn process_ohttp_request(
230 request: Request,
231 channel: &str,
232 settings: crate::ClientSettings,
233) -> Result<Response> {
234 let overall_start = std::time::Instant::now();
235 crate::trace!(
236 "=== Starting OHTTP request processing for channel: '{}' ===",
237 channel
238 );
239 crate::trace!("Target URL: {} {}", request.method, request.url);
240
241 let config = get_ohttp_config(channel)?;
242 crate::trace!(
243 "Retrieved OHTTP config - relay: {}, gateway: {}",
244 config.relay_url,
245 config.gateway_host
246 );
247
248 crate::trace!(
250 "Step 1: Fetching gateway encryption keys from: {}",
251 config.gateway_host
252 );
253 let gateway_config_start = std::time::Instant::now();
254 let gateway_config_data = fetch_gateway_config(&config.gateway_host).await?;
255 let gateway_config_duration = gateway_config_start.elapsed();
256 crate::trace!(
257 "Gateway config fetched: {} bytes in {:?}",
258 gateway_config_data.len(),
259 gateway_config_duration
260 );
261
262 crate::trace!("Step 2: Creating OHTTP session with gateway keys...");
264 let session_start = std::time::Instant::now();
265 let ohttp_session = OhttpSession::new(&gateway_config_data).map_err(|e| {
266 crate::error!("Failed to create OHTTP session: {}", e);
267 ViaductError::OhttpRequestError(format!("Failed to create OHTTP session: {}", e))
268 })?;
269 let session_duration = session_start.elapsed();
270 crate::trace!(
271 "OHTTP session created successfully in {:?}",
272 session_duration
273 );
274
275 let method = request.method.as_str();
277 let scheme = request.url.scheme();
278 let authority = request.url.host_str().unwrap_or("");
279 let path_and_query = {
280 let mut path = request.url.path().to_string();
281 if let Some(query) = request.url.query() {
282 path.push('?');
283 path.push_str(query);
284 }
285 path
286 };
287 let headers_map: HashMap<String, String> = request.headers.clone().into();
288 let payload = request.body.unwrap_or_default();
289
290 crate::trace!(
291 "Step 3: Preparing request - {} {}://{}{}",
292 method,
293 scheme,
294 authority,
295 path_and_query
296 );
297 crate::trace!("Request headers: {} total", headers_map.len());
298 crate::trace!("Request payload: {} bytes", payload.len());
299
300 crate::trace!("Step 4: Encapsulating request with OHTTP...");
302 let encap_start = std::time::Instant::now();
303 let encrypted_request = ohttp_session
304 .encapsulate(
305 method,
306 scheme,
307 authority,
308 &path_and_query,
309 headers_map,
310 &payload,
311 )
312 .map_err(|e| {
313 crate::error!("Failed to encapsulate request: {}", e);
314 ViaductError::OhttpRequestError(format!("Failed to encapsulate request: {}", e))
315 })?;
316 let encap_duration = encap_start.elapsed();
317 crate::trace!(
318 "Request encapsulated: {} bytes → {} bytes encrypted in {:?}",
319 payload.len(),
320 encrypted_request.len(),
321 encap_duration
322 );
323
324 let relay_url = Url::parse(&config.relay_url)?;
326 crate::trace!("Step 5: Sending encrypted request to relay: {}", relay_url);
327
328 let mut relay_headers = Headers::new();
329 relay_headers.insert("Content-Type", "message/ohttp-req")?;
330
331 let relay_request = Request {
332 method: Method::Post,
333 url: relay_url.clone(),
334 headers: relay_headers,
335 body: Some(encrypted_request),
336 };
337
338 crate::trace!("Sending to relay with timeout: {}ms", settings.timeout);
340 let relay_start = std::time::Instant::now();
341 let backend = crate::new_backend::get_backend()?;
342 let relay_response = backend.send_request(relay_request, settings).await?;
343 let relay_duration = relay_start.elapsed();
344
345 crate::trace!(
346 "Relay responded: HTTP {} in {:?}",
347 relay_response.status,
348 relay_duration
349 );
350
351 if !relay_response.is_success() {
353 crate::error!(
354 "OHTTP relay {} returned error: HTTP {} - {}",
355 relay_url,
356 relay_response.status,
357 String::from_utf8_lossy(&relay_response.body)
358 );
359 return Err(ViaductError::OhttpRequestError(format!(
360 "OHTTP relay returned error: HTTP {} - {}",
361 relay_response.status,
362 String::from_utf8_lossy(&relay_response.body)
363 )));
364 }
365
366 if let Some(content_type) = relay_response.headers.get("content-type") {
368 if content_type != "message/ohttp-res" {
369 crate::warn!(
370 "OHTTP relay returned unexpected content-type: {} (expected: message/ohttp-res)",
371 content_type
372 );
373 } else {
374 crate::trace!("Relay response content-type verified: {}", content_type);
375 }
376 } else {
377 crate::warn!("OHTTP relay response missing content-type header");
378 }
379
380 crate::trace!(
382 "Step 6: Decapsulating response ({} bytes from relay)...",
383 relay_response.body.len()
384 );
385 let decap_start = std::time::Instant::now();
386 let ohttp_response = ohttp_session
387 .decapsulate(&relay_response.body)
388 .map_err(|e| {
389 crate::error!("Failed to decapsulate OHTTP response: {}", e);
390 ViaductError::OhttpResponseError(format!("Failed to decapsulate OHTTP response: {}", e))
391 })?;
392 let decap_duration = decap_start.elapsed();
393
394 let (status, headers_map, body) = ohttp_response.into_parts();
396 let final_headers = Headers::try_from_hashmap(headers_map)?;
397
398 let final_response = Response {
399 request_method: request.method,
400 url: request.url,
401 status,
402 headers: final_headers,
403 body,
404 };
405
406 let overall_duration = overall_start.elapsed();
407 crate::trace!(
408 "=== OHTTP request completed successfully for channel '{}' ===",
409 channel
410 );
411 crate::trace!(
412 "Final result: HTTP {} with {} bytes (total time: {:?})",
413 final_response.status,
414 final_response.body.len(),
415 overall_duration
416 );
417 crate::trace!(
418 "Timing breakdown - Config: {:?}, Session: {:?}, Encap: {:?}, Relay: {:?}, Decap: {:?}",
419 gateway_config_duration,
420 session_duration,
421 encap_duration,
422 relay_duration,
423 decap_duration
424 );
425
426 Ok(final_response)
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn test_channel_configuration() {
435 clear_ohttp_channels();
436
437 let config = OhttpConfig {
438 relay_url: "https://relay.example.com".to_string(),
439 gateway_host: "gateway.example.com".to_string(),
440 };
441
442 configure_ohttp_channel("test".to_string(), config.clone()).unwrap();
443
444 assert!(is_ohttp_channel_configured("test"));
445 assert!(!is_ohttp_channel_configured("nonexistent"));
446
447 let retrieved = get_ohttp_config("test").unwrap();
448 assert_eq!(retrieved.relay_url, config.relay_url);
449 assert_eq!(retrieved.gateway_host, config.gateway_host);
450
451 let channels = list_ohttp_channels();
452 assert_eq!(channels, vec!["test"]);
453
454 clear_ohttp_channels();
455 assert!(!is_ohttp_channel_configured("test"));
456 }
457
458 #[test]
459 fn test_headers_conversion() {
460 let mut headers = Headers::new();
461 headers.insert("Content-Type", "application/json").unwrap();
462 headers.insert("Authorization", "Bearer token").unwrap();
463
464 let map: HashMap<String, String> = headers.clone().into();
465
466 assert_eq!(map.len(), 2);
467 assert_eq!(map.get("content-type").unwrap(), "application/json");
468 assert_eq!(map.get("authorization").unwrap(), "Bearer token");
469
470 let headers_back = Headers::try_from_hashmap(map).unwrap();
471
472 assert_eq!(
473 headers_back.get("Content-Type").unwrap(),
474 "application/json"
475 );
476 assert_eq!(headers_back.get("Authorization").unwrap(), "Bearer token");
477 }
478}