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/// Configuration for an OHTTP channel
15#[derive(Debug, Clone, uniffi::Record)]
16pub struct OhttpConfig {
17    /// The relay URL that will proxy requests
18    pub relay_url: String,
19    /// The gateway host that provides encryption keys and decrypts requests
20    pub gateway_host: String,
21}
22
23/// Cached gateway configuration with expiration
24#[derive(Debug, Clone)]
25struct CachedGatewayConfig {
26    config_data: Vec<u8>,
27    expires_at: SystemTime,
28}
29
30/// Global registry of OHTTP channel configurations
31static OHTTP_CHANNELS: Lazy<RwLock<HashMap<String, OhttpConfig>>> =
32    Lazy::new(|| RwLock::new(HashMap::new()));
33
34/// Cache for gateway configurations with async protection
35static CONFIG_CACHE: Lazy<RwLock<HashMap<String, CachedGatewayConfig>>> =
36    Lazy::new(|| RwLock::new(HashMap::new()));
37
38/// Configure an OHTTP channel with the given configuration
39/// If an existing OHTTP config exists with the same name, it will be overwritten
40#[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    // Validate URLs
50    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    // Validate gateway host format
58    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/// Configure default OHTTP channels for common Mozilla services
71/// This sets up:
72/// - "relay1": For general telemetry and services through Mozilla's shared gateway
73/// - "merino": For Firefox Suggest recommendations through Merino's dedicated relay/gateway
74#[uniffi::export]
75pub fn configure_default_ohttp_channels() -> Result<()> {
76    crate::trace!("Configuring default OHTTP channels");
77
78    // Configure relay1 for general purpose OHTTP
79    // Fastly relay forwards to Mozilla's shared gateway
80    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 merino with its dedicated relay and integrated gateway
89    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/// Clear all OHTTP channel configurations
102#[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
109/// Get the configuration for a specific OHTTP channel
110pub 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
135/// Check if an OHTTP channel is configured
136pub fn is_ohttp_channel_configured(channel: &str) -> bool {
137    OHTTP_CHANNELS.read().contains_key(channel)
138}
139
140/// List all configured OHTTP channels
141#[uniffi::export]
142pub fn list_ohttp_channels() -> Vec<String> {
143    OHTTP_CHANNELS.read().keys().cloned().collect()
144}
145
146/// Fetch and cache gateway configuration (encryption keys)
147pub 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    // Could be that multiple threads fetch an already existing config
153    // because we don't double check here. We are currently ok with that
154    // to keep the code simpler
155    let config_data = fetch_config_from_network(gateway_host).await?;
156
157    // Update cache (last writer wins)
158    {
159        let mut cache = CONFIG_CACHE.write();
160        cache.insert(
161            gateway_host.to_string(),
162            CachedGatewayConfig {
163                config_data: config_data.clone(),
164                // Set the cache expiry to 1 day
165                expires_at: SystemTime::now() + Duration::from_secs(60 * 60 * 24),
166            },
167        );
168    }
169
170    Ok(config_data)
171}
172
173/// Read from cache if valid
174fn 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
179/// Check if cache entry exists and is valid
180fn 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
195/// Fetch config from network and update cache
196async 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
229/// Process an OHTTP request using the OHTTP client component
230pub 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    // Fetch gateway config (encryption keys)
250    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    // Create OHTTP session using the gateway's encryption keys
264    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    // Prepare request components - these come from the actual request URL (target)
277    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    // Encapsulate the request using the OHTTP session
302    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    // Create HTTP request to send to the relay
326    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    // Send the encrypted request to the relay using the backend
340    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    // Check if the relay responded successfully
353    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    // Verify the response content type
368    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    // Decapsulate the encrypted response using the OHTTP session
382    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    // Convert the OHTTP response back to a viaduct Response
396    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}