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