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        ..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
228/// Process an OHTTP request using the OHTTP client component
229pub 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    // Fetch gateway config (encryption keys)
249    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    // Create OHTTP session using the gateway's encryption keys
263    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    // Prepare request components - these come from the actual request URL (target)
276    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    // Encapsulate the request using the OHTTP session
301    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    // Create HTTP request to send to the relay
325    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    // Send the encrypted request to the relay using the backend
339    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    // Check if the relay responded successfully
352    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    // Verify the response content type
367    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    // Decapsulate the encrypted response using the OHTTP session
381    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    // Convert the OHTTP response back to a viaduct Response
395    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}