lib.rs (11674B)
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 5 use error_support::{info, warn}; 6 use futures_channel::oneshot; 7 use std::{ffi::{CStr, c_char}, ptr, slice, sync::Arc}; 8 use url::Url; 9 use viaduct::{ 10 init_backend, Backend, ClientSettings, Method, Request, Response, Result, ViaductError, 11 }; 12 13 const NULL: char = '\0'; 14 15 /// Request for the C++ backend 16 #[repr(C)] 17 pub struct FfiRequest { 18 pub timeout: u32, 19 pub redirect_limit: u32, 20 pub method: Method, 21 pub url: *mut u8, 22 pub headers: *mut FfiHeader, 23 pub header_count: usize, 24 pub body: *mut u8, 25 pub body_len: usize, 26 } 27 28 #[repr(C)] 29 pub struct FfiHeader { 30 pub key: *mut u8, 31 pub value: *mut u8, 32 } 33 34 /// Result from the backend 35 /// 36 /// This is built-up piece by piece using the extern "C" API. 37 pub struct FfiResult { 38 // oneshot sender that the Rust code is awaiting. If `Ok(())` is sent, then the Rust code 39 // should return the response. If an error is sent, then that should be returned instead. 40 sender: Option<oneshot::Sender<Result<Response>>>, 41 response: Response, 42 // Owned values stored in the [FfiRequest]. These are copied from the request. By storing 43 // them in the result, we ensure they stay alive while the C code may access them. 44 pub url: String, 45 pub headers: Vec<(String, String)>, 46 pub body: Option<Vec<u8>>, 47 // The request struct that we pass to C++. This must be kept alive as long as the C++ code is 48 // using it. 49 pub request: FfiRequest, 50 pub ffi_headers: Vec<FfiHeader>, 51 } 52 53 // Functions that the C++ library exports for us 54 extern "C" { 55 fn viaduct_necko_backend_init(); 56 57 #[allow(improper_ctypes)] 58 fn viaduct_necko_backend_send_request(request: *const FfiRequest, result: *mut FfiResult); 59 } 60 61 // Functions that we provide to the C++ library 62 63 /// Set the URL for a result 64 /// 65 /// # Safety 66 /// 67 /// - `result` must be valid. 68 /// - `url` and `length` must refer to a valid byte string. 69 /// 70 /// Note: URLs are expected to be ASCII. Non-ASCII URLs will be logged and skipped. 71 #[no_mangle] 72 pub unsafe extern "C" fn viaduct_necko_result_set_url( 73 result: *mut FfiResult, 74 url: *const u8, 75 length: usize, 76 ) { 77 let result = unsafe { &mut *result }; 78 79 // Safety: Creating a slice from raw parts is safe if the backend passes valid pointers and lengths 80 let url_bytes = unsafe { slice::from_raw_parts(url, length) }; 81 82 // Validate that the URL is ASCII before converting to String 83 if !url_bytes.is_ascii() { 84 warn!( 85 "Non-ASCII URL received - length: {} - skipping URL update", 86 length 87 ); 88 return; 89 } 90 91 // Safety: We just verified the bytes are ASCII, which is valid UTF-8 92 let url_str = unsafe { std::str::from_utf8_unchecked(url_bytes) }; 93 94 match Url::parse(url_str) { 95 Ok(url) => { 96 result.response.url = url; 97 } 98 Err(e) => { 99 warn!("Error parsing URL from C backend: {e}") 100 } 101 } 102 } 103 104 /// Set the status code for a result 105 /// 106 /// # Safety 107 /// 108 /// `result` must be valid. 109 #[no_mangle] 110 pub unsafe extern "C" fn viaduct_necko_result_set_status_code(result: *mut FfiResult, code: u16) { 111 let result = unsafe { &mut *result }; 112 result.response.status = code; 113 } 114 115 /// Set a header for a result 116 /// 117 /// # Safety 118 /// 119 /// - `result` must be valid. 120 /// - `key` and `key_length` must refer to a valid byte string. 121 /// - `value` and `value_length` must refer to a valid byte string. 122 /// 123 /// Note: HTTP headers are expected to be ASCII. Non-ASCII headers will be logged and skipped. 124 #[no_mangle] 125 pub unsafe extern "C" fn viaduct_necko_result_add_header( 126 result: *mut FfiResult, 127 key: *const u8, 128 key_length: usize, 129 value: *const u8, 130 value_length: usize, 131 ) { 132 let result = unsafe { &mut *result }; 133 134 // Safety: Creating slices from raw parts is safe if the backend passes valid pointers and lengths 135 let key_bytes = unsafe { slice::from_raw_parts(key, key_length) }; 136 let value_bytes = unsafe { slice::from_raw_parts(value, value_length) }; 137 138 // Validate that headers are ASCII before converting to String 139 // HTTP headers should be ASCII per best practices, though the spec technically allows other encodings 140 if !key_bytes.is_ascii() || !value_bytes.is_ascii() { 141 warn!( 142 "Non-ASCII HTTP header received - key_len: {}, value_len: {} - skipping header", 143 key_length, value_length 144 ); 145 return; 146 } 147 148 // Safety: We just verified the bytes are ASCII, which is valid UTF-8 149 let (key, value) = unsafe { 150 ( 151 String::from_utf8_unchecked(key_bytes.to_vec()), 152 String::from_utf8_unchecked(value_bytes.to_vec()), 153 ) 154 }; 155 156 let _ = result.response.headers.insert(key, value); 157 } 158 159 /// Append data to a result body 160 /// 161 /// This method can be called multiple times to build up the body in chunks. 162 /// 163 /// # Safety 164 /// 165 /// - `result` must be valid. 166 /// - `data` and `length` must refer to a binary string. 167 #[no_mangle] 168 pub unsafe extern "C" fn viaduct_necko_result_extend_body( 169 result: *mut FfiResult, 170 data: *const u8, 171 length: usize, 172 ) { 173 let result = unsafe { &mut *result }; 174 // Safety: this is safe as long as the backend passes us valid data 175 result 176 .response 177 .body 178 .extend_from_slice(unsafe { slice::from_raw_parts(data, length) }); 179 } 180 181 /// Complete a result 182 /// 183 /// # Safety 184 /// 185 /// `result` must be valid. After calling this function it must not be used again. 186 #[no_mangle] 187 pub unsafe extern "C" fn viaduct_necko_result_complete(result: *mut FfiResult) { 188 let mut result = unsafe { Box::from_raw(result) }; 189 match result.sender.take() { 190 Some(sender) => { 191 // Ignore any errors when sending the result. This happens when the receiver is 192 // closed, which happens when a future is cancelled. 193 let _ = sender.send(Ok(result.response)); 194 } 195 None => warn!("viaduct-necko: result completed twice"), 196 } 197 } 198 199 /// Complete a result with an error message 200 /// 201 /// # Safety 202 /// 203 /// - `result` must be valid. After calling this function it must not be used again. 204 /// - `message` and `length` must refer to a valid UTF-8 string. 205 #[no_mangle] 206 pub unsafe extern "C" fn viaduct_necko_result_complete_error( 207 result: *mut FfiResult, 208 error_code: u32, 209 message: *const u8, 210 ) { 211 let mut result = unsafe { Box::from_raw(result) }; 212 // Safety: this is safe as long as the backend passes us valid data 213 let msg_str = unsafe { 214 CStr::from_ptr(message as *const c_char) 215 .to_string_lossy() 216 .into_owned() 217 }; 218 let msg = format!("{} (0x{:08x})", msg_str, error_code); 219 match result.sender.take() { 220 Some(sender) => { 221 // Ignore any errors when sending the result. This happens when the receiver is 222 // closed, which happens when a future is cancelled. 223 let _ = sender.send(Err(ViaductError::BackendError(msg))); 224 } 225 None => warn!("viaduct-necko: result completed twice"), 226 } 227 } 228 229 // The Necko backend is a zero-sized type, since all the backend functionality is statically linked 230 struct NeckoBackend; 231 232 /// Initialize the Necko backend 233 /// 234 /// This should be called once at startup before any HTTP requests are made. 235 pub fn init_necko_backend() -> Result<()> { 236 info!("Initializing viaduct Necko backend"); 237 // Safety: this is safe as long as the C++ code is correct. 238 unsafe { viaduct_necko_backend_init() }; 239 init_backend(Arc::new(NeckoBackend)) 240 } 241 242 #[async_trait::async_trait] 243 impl Backend for NeckoBackend { 244 async fn send_request(&self, request: Request, settings: ClientSettings) -> Result<Response> { 245 // Convert the request for the backend 246 let mut url = request.url.to_string(); 247 url.push(NULL); 248 249 // Convert headers to null-terminated strings for C++ 250 // Note: Headers iterates over Header objects, not tuples 251 let header_strings: Vec<(String, String)> = request 252 .headers 253 .iter() 254 .map(|h| { 255 let mut key_str = h.name().to_string(); 256 key_str.push(NULL); 257 let mut value_str = h.value().to_string(); 258 value_str.push(NULL); 259 (key_str, value_str) 260 }) 261 .collect(); 262 263 // Prepare an FfiResult with an empty response 264 let (sender, receiver) = oneshot::channel(); 265 let mut result = Box::new(FfiResult { 266 sender: Some(sender), 267 response: Response { 268 request_method: request.method, 269 url: request.url.clone(), 270 status: 0, 271 headers: viaduct::Headers::new(), 272 body: Vec::default(), 273 }, 274 url, 275 headers: header_strings, 276 body: request.body, 277 request: FfiRequest { 278 timeout: settings.timeout, 279 redirect_limit: settings.redirect_limit, 280 method: request.method, 281 url: ptr::null_mut(), 282 headers: ptr::null_mut(), 283 header_count: 0, 284 body: ptr::null_mut(), 285 body_len: 0, 286 }, 287 ffi_headers: Vec::new(), 288 }); 289 290 // Now that we have the result box, we can set up the pointers in the request. 291 // By doing this after creating the box, we minimize the chance that a value moves after a pointer is created. 292 result.ffi_headers = result 293 .headers 294 .iter_mut() 295 .map(|(key, value)| FfiHeader { 296 key: key.as_mut_ptr(), 297 value: value.as_mut_ptr(), 298 }) 299 .collect(); 300 301 let (body_ptr, body_len) = match &result.body { 302 Some(body) => (body.as_ptr() as *mut u8, body.len()), 303 None => (ptr::null_mut(), 0), 304 }; 305 306 result.request.url = result.url.as_mut_ptr(); 307 result.request.headers = result.ffi_headers.as_mut_ptr(); 308 result.request.header_count = result.ffi_headers.len(); 309 result.request.body = body_ptr; 310 result.request.body_len = body_len; 311 312 let request_ptr = &result.request as *const FfiRequest; 313 314 // Safety: this is safe if the C backend implements the API correctly. 315 unsafe { 316 viaduct_necko_backend_send_request(request_ptr, Box::into_raw(result)); 317 }; 318 319 receiver.await.unwrap_or_else(|_| { 320 Err(ViaductError::BackendError( 321 "Error receiving result from C++ backend".to_string(), 322 )) 323 }) 324 } 325 } 326 327 // Mark FFI types as Send to allow them to be used across an await point. This is safe as long as 328 // the backend code uses them correctly. 329 unsafe impl Send for FfiRequest {} 330 unsafe impl Send for FfiResult {} 331 unsafe impl Send for FfiHeader {} 332 333 #[cfg(test)] 334 mod tests { 335 use super::*; 336 337 #[test] 338 fn test_method_layout() { 339 // Assert that the viaduct::Method enum matches the layout expected by the C++ backend. 340 // See ViaductMethod in backend.h 341 assert_eq!(Method::Get as u8, 0); 342 assert_eq!(Method::Head as u8, 1); 343 assert_eq!(Method::Post as u8, 2); 344 assert_eq!(Method::Put as u8, 3); 345 assert_eq!(Method::Delete as u8, 4); 346 assert_eq!(Method::Connect as u8, 5); 347 assert_eq!(Method::Options as u8, 6); 348 assert_eq!(Method::Trace as u8, 7); 349 assert_eq!(Method::Patch as u8, 8); 350 } 351 }