viaduct/
ohttp.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use 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/// Send a request using either the new backend or the old backend.
15///
16/// This function provides compatibility with both backend systems:
17/// - If the new backend is initialized, it uses it with the provided settings
18/// - Otherwise, it falls back to the old backend (which uses global settings)
19///
20/// Note: When using the old backend, the `settings` parameter is ignored and
21/// global settings from `GLOBAL_SETTINGS` are used instead.
22async fn send_request(request: Request, settings: crate::ClientSettings) -> Result<Response> {
23    // Try to use the new backend first
24    if let Ok(backend) = crate::new_backend::get_backend() {
25        return backend.send_request(request, settings).await;
26    }
27
28    // Fall back to the old backend (synchronous, uses global settings)
29    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/// Configuration for an OHTTP channel
36#[derive(Debug, Clone, uniffi::Record)]
37pub struct OhttpConfig {
38    /// The relay URL that will proxy requests
39    pub relay_url: String,
40    /// The gateway host that provides encryption keys and decrypts requests
41    pub gateway_host: String,
42}
43
44/// Cached gateway configuration with expiration
45#[derive(Debug, Clone)]
46struct CachedGatewayConfig {
47    config_data: Vec<u8>,
48    expires_at: SystemTime,
49}
50
51/// Global registry of OHTTP channel configurations
52static OHTTP_CHANNELS: Lazy<RwLock<HashMap<String, OhttpConfig>>> =
53    Lazy::new(|| RwLock::new(HashMap::new()));
54
55/// Cache for gateway configurations with async protection
56static CONFIG_CACHE: Lazy<RwLock<HashMap<String, CachedGatewayConfig>>> =
57    Lazy::new(|| RwLock::new(HashMap::new()));
58
59/// Configure an OHTTP channel with the given configuration
60/// If an existing OHTTP config exists with the same name, it will be overwritten
61#[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    // Validate URLs
71    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    // Validate gateway host format
79    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/// Configure default OHTTP channels for common Mozilla services
92/// This sets up:
93/// - "relay1": For general telemetry and services through Mozilla's shared gateway
94/// - "merino": For Firefox Suggest recommendations through Merino's dedicated relay/gateway
95#[uniffi::export]
96pub fn configure_default_ohttp_channels() -> Result<()> {
97    crate::trace!("Configuring default OHTTP channels");
98
99    // Configure relay1 for general purpose OHTTP
100    // Fastly relay forwards to Mozilla's shared gateway
101    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 merino with its dedicated relay and integrated gateway
110    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/// Clear all OHTTP channel configurations
123#[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
130/// Get the configuration for a specific OHTTP channel
131pub 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
156/// Check if an OHTTP channel is configured
157pub fn is_ohttp_channel_configured(channel: &str) -> bool {
158    OHTTP_CHANNELS.read().contains_key(channel)
159}
160
161/// List all configured OHTTP channels
162#[uniffi::export]
163pub fn list_ohttp_channels() -> Vec<String> {
164    OHTTP_CHANNELS.read().keys().cloned().collect()
165}
166
167/// Fetch and cache gateway configuration (encryption keys)
168pub 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    // Could be that multiple threads fetch an already existing config
174    // because we don't double check here. We are currently ok with that
175    // to keep the code simpler
176    let config_data = fetch_config_from_network(gateway_host).await?;
177
178    // Update cache (last writer wins)
179    {
180        let mut cache = CONFIG_CACHE.write();
181        cache.insert(
182            gateway_host.to_string(),
183            CachedGatewayConfig {
184                config_data: config_data.clone(),
185                // Set the cache expiry to 1 day
186                expires_at: SystemTime::now() + Duration::from_secs(60 * 60 * 24),
187            },
188        );
189    }
190
191    Ok(config_data)
192}
193
194/// Read from cache if valid
195fn 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
200/// Check if cache entry exists and is valid
201fn 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
216/// Fetch config from network and update cache
217async 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
248/// Process an OHTTP request using the OHTTP client component
249pub 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    // Fetch gateway config (encryption keys)
269    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    // Create OHTTP session using the gateway's encryption keys
283    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    // Prepare request components - these come from the actual request URL (target)
296    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    // Encapsulate the request using the OHTTP session
321    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    // Create HTTP request to send to the relay
345    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    // Send the encrypted request to the relay using the backend
359    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    // Check if the relay responded successfully
371    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    // Verify the response content type
386    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    // Decapsulate the encrypted response using the OHTTP session
400    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    // Convert the OHTTP response back to a viaduct Response
414    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}