diff options
| author | Mason Reed <mason@vector35.com> | 2025-10-18 20:16:39 -0400 |
|---|---|---|
| committer | Mason Reed <mason@vector35.com> | 2025-10-22 00:36:25 -0400 |
| commit | ea914efbe24c80b6e1523471a7369d79ac07deb0 (patch) | |
| tree | 61c0fcd05a49dc72bb83e81dd7340e59a41f2ed5 /plugins/warp/src/container/network | |
| parent | 92656ce40229cd9962febe57678d7ae5378b31a0 (diff) | |
[WARP] Use download provider API instead of reqwest
Unfortunately we cannot use reqwest because enterprise servers provide invalid certificates, so we must use the enterprise download provider to bypass certain certificate validations
Diffstat (limited to 'plugins/warp/src/container/network')
| -rw-r--r-- | plugins/warp/src/container/network/client.rs | 386 |
1 files changed, 173 insertions, 213 deletions
diff --git a/plugins/warp/src/container/network/client.rs b/plugins/warp/src/container/network/client.rs index 39e7640a..d2217bce 100644 --- a/plugins/warp/src/container/network/client.rs +++ b/plugins/warp/src/container/network/client.rs @@ -3,9 +3,8 @@ use crate::container::{ ContainerSearchItem, ContainerSearchItemKind, ContainerSearchQuery, ContainerSearchResponse, SourceId, SourcePath, SourceTag, }; -use reqwest::blocking::Client; -use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; -use reqwest::StatusCode; +use base64::Engine; +use binaryninja::download::DownloadProvider; use serde::Deserialize; use serde_json::json; use std::collections::HashMap; @@ -23,42 +22,23 @@ use warp::WarpFile; /// NOTE: **All requests are blocking**. #[derive(Clone, Debug)] pub struct NetworkClient { - client: Client, + provider: DownloadProvider, + headers: Vec<(String, String)>, pub server_url: String, } impl NetworkClient { - pub fn new( - server_url: String, - server_token: Option<String>, - https_proxy: Option<String>, - ) -> reqwest::Result<Self> { - let version_info = binaryninja::version_info(); - // TODO: IIRC we had a user agent format already for some other thing. - let client_agent = format!( - "Binary Ninja/{}.{}.{}", - version_info.major, version_info.minor, version_info.build - ); + pub fn new(server_url: String, server_token: Option<String>) -> Self { // TODO: This might want to be kept for the request header? - let mut headers = HeaderMap::new(); + let mut headers: Vec<(String, String)> = vec![]; if let Some(token) = &server_token { - headers.insert( - AUTHORIZATION, - HeaderValue::from_str(&format!("Bearer {}", token)).unwrap(), - ); + headers.push(("authorization".to_string(), format!("Bearer {}", token))); } - // TODO: Configurable timeout? - let mut client_builder = Client::builder() - .connect_timeout(std::time::Duration::from_secs(10)) - .default_headers(headers) - .user_agent(client_agent); - if let Some(https_proxy) = https_proxy { - client_builder = client_builder.proxy(reqwest::Proxy::all(&https_proxy)?); - } - Ok(Self { - client: client_builder.build()?, + Self { + provider: DownloadProvider::try_default().unwrap(), + headers, server_url, - }) + } } /// Check to see the status of the server. @@ -67,10 +47,14 @@ impl NetworkClient { /// server that is unresponsive. /// /// Route: `api/v1/status` - pub fn status(&self) -> reqwest::Result<StatusCode> { + pub fn status(&self) -> Result<(), String> { let status_url = format!("{}/api/v1/status", self.server_url); - let resp = self.client.get(&status_url).send()?; - Ok(resp.status()) + let mut inst = self.provider.create_instance().unwrap(); + let resp = inst.get(&status_url, self.headers.clone())?; + match resp.is_success() { + true => Ok(()), + false => Err(format!("Server returned an error: {}", resp.status_code)), + } } /// Query the logged in user. @@ -78,8 +62,9 @@ impl NetworkClient { /// NOTE: **THIS IS BLOCKING** /// /// Route: `api/v1/users/me` (TODO: Comment about the query) - pub fn current_user(&self) -> reqwest::Result<(i32, String)> { + pub fn current_user(&self) -> Result<(i32, String), String> { let current_user_url = format!("{}/api/v1/users/me", self.server_url); + let mut inst = self.provider.create_instance().unwrap(); #[derive(Deserialize)] struct CurrentUser { @@ -87,12 +72,14 @@ impl NetworkClient { id: i32, } - let resp = self - .client - .get(¤t_user_url) - .send()? - .error_for_status()?; - let user: CurrentUser = resp.json()?; + let resp = inst.get(¤t_user_url, self.headers.clone())?; + if !resp.is_success() { + return Err(format!( + "'{}' returned {}", + current_user_url, resp.status_code + )); + } + let user: CurrentUser = resp.json().map_err(|e| e.to_string())?; Ok((user.id, user.username)) } @@ -101,16 +88,20 @@ impl NetworkClient { /// NOTE: **THIS IS BLOCKING** /// /// Route: `api/v1/users/me` (TODO: Comment about the query) - pub fn source_name(&self, id: SourceId) -> reqwest::Result<String> { + pub fn source_name(&self, id: SourceId) -> Result<String, String> { let source_url = format!("{}/api/v1/sources/{}", self.server_url, id); + let mut inst = self.provider.create_instance().unwrap(); #[derive(Deserialize)] struct Source { name: String, } - let resp = self.client.get(&source_url).send()?.error_for_status()?; - let src: Source = resp.json()?; + let resp = inst.get(&source_url, self.headers.clone())?; + if !resp.is_success() { + return Err(format!("'{}' returned {}", source_url, resp.status_code)); + } + let src: Source = resp.json().map_err(|e| e.to_string())?; Ok(src.name) } @@ -123,8 +114,9 @@ impl NetworkClient { /// NOTE: **THIS IS BLOCKING** /// /// Route: `api/v1/sources/` - pub fn create_source(&self, name: &str) -> reqwest::Result<SourceId> { + pub fn create_source(&self, name: &str) -> Result<SourceId, String> { let source_url = format!("{}/api/v1/sources", self.server_url); + let mut inst = self.provider.create_instance().unwrap(); let body = json!({ "name": name, @@ -137,14 +129,11 @@ impl NetworkClient { id: Uuid, } - let resp = self - .client - .post(&source_url) - .json(&body) - .send()? - .error_for_status()?; - - let parsed: CreateSourceResponse = resp.json()?; + let resp = inst.post_json(&source_url, self.headers.clone(), &body)?; + if !resp.is_success() { + return Err(format!("'{}' returned {}", source_url, resp.status_code)); + } + let parsed: CreateSourceResponse = resp.json().map_err(|e| e.to_string())?; Ok(SourceId(parsed.id)) } @@ -153,8 +142,9 @@ impl NetworkClient { /// NOTE: **THIS IS BLOCKING** /// /// Route: `api/v1/sources/query` (TODO: Comment about the query) - pub fn query_sources(&self, user_id: Option<i32>) -> reqwest::Result<Vec<SourceId>> { + pub fn query_sources(&self, user_id: Option<i32>) -> Result<Vec<SourceId>, String> { let sources_url = format!("{}/api/v1/sources/query", self.server_url); + let mut inst = self.provider.create_instance().unwrap(); #[derive(Deserialize)] struct SourceItem { @@ -170,16 +160,12 @@ impl NetworkClient { if let Some(user_id) = user_id { query.insert("user_id", user_id); } - let query_str = json!(query).to_string(); - let resp = self - .client - .post(&sources_url) - .body(query_str) - .header("Content-Type", "application/json") - .send()? - .error_for_status()?; - let parsed: SourcesQueryResponse = resp.json()?; + let resp = inst.post_json(&sources_url, self.headers.clone(), &json!(query))?; + if !resp.is_success() { + return Err(format!("'{}' returned {}", sources_url, resp.status_code)); + } + let parsed: SourcesQueryResponse = resp.json().map_err(|e| e.to_string())?; Ok(parsed.items.into_iter().map(|it| SourceId(it.id)).collect()) } @@ -190,6 +176,12 @@ impl NetworkClient { /// Route: `api/v1/targets/query` (TODO: Comment about the query) pub fn query_target_id(&self, target: &Target) -> Option<NetworkTargetId> { let query_target_url = format!("{}/api/v1/targets/query", self.server_url); + let mut inst = self.provider.create_instance().unwrap(); + + #[derive(Deserialize)] + struct TargetQueryResponse { + id: NetworkTargetId, + } let mut query = HashMap::new(); if let Some(platform) = &target.platform { @@ -198,25 +190,13 @@ impl NetworkClient { if let Some(architecture) = &target.architecture { query.insert("arch", architecture); } - let query_str = json!(query).to_string(); - - #[derive(Deserialize)] - struct TargetQueryResponse { - id: NetworkTargetId, - } - // NOTE: This is blocking. - let response = self - .client - .post(query_target_url) - .body(query_str) - .header("Content-Type", "application/json") - .send() + let resp = inst + .post_json(&query_target_url, self.headers.clone(), &json!(query)) .ok()?; - // Assuming the first response is the one we want. // TODO: Handle multiple responses, or error out. - let json_response: Vec<TargetQueryResponse> = response.json().ok()?; + let json_response: Vec<TargetQueryResponse> = resp.json().ok()?; let first_response = json_response.first()?; Some(first_response.id) @@ -257,27 +237,22 @@ impl NetworkClient { target: Option<NetworkTargetId>, source: Option<SourceId>, guids: &[FunctionGUID], - ) -> Option<WarpFile<'static>> { + ) -> Result<WarpFile<'static>, String> { let query_functions_url = format!("{}/api/v1/functions/query", self.server_url); // TODO: Allow for source tags? We really only need this in query_functions_source as that // TODO: is what prevents a undesired source from being "known" to the container. let payload = Self::query_functions_body(target, source, &[], guids); + let mut inst = self.provider.create_instance().unwrap(); - // Make the POST request - let response = self - .client - .post(&query_functions_url) - .json(&payload) - .send() - .ok()?; - if !response.status().is_success() { - log::error!("Failed to query functions: {}", response.status()); - return None; + let resp = inst.post_json(&query_functions_url, self.headers.clone(), &payload)?; + if !resp.is_success() { + return Err(format!( + "'{}' returned {}", + query_functions_url, resp.status_code + )); } - // Get response bytes and convert to WarpFile - let bytes = response.bytes().ok()?; - WarpFile::from_owned_bytes(bytes.to_vec()) + WarpFile::from_owned_bytes(resp.data).ok_or("Failed to parse WARP data".to_string()) } /// Query the functions, returning the sources and the corresponding function guids. @@ -290,25 +265,21 @@ impl NetworkClient { target: Option<NetworkTargetId>, tags: &[SourceTag], guids: &[FunctionGUID], - ) -> Option<HashMap<SourceId, Vec<FunctionGUID>>> { + ) -> Result<HashMap<SourceId, Vec<FunctionGUID>>, String> { let query_functions_source_url = format!("{}/api/v1/functions/query/source", self.server_url); let payload = Self::query_functions_body(target, None, tags, guids); + let mut inst = self.provider.create_instance().unwrap(); - // Make the POST request - let response = self - .client - .post(&query_functions_source_url) - .json(&payload) - .send() - .ok()?; - if !response.status().is_success() { - log::error!("Failed to query functions source: {}", response.status()); - return None; + let resp = inst.post_json(&query_functions_source_url, self.headers.clone(), &payload)?; + if !resp.is_success() { + return Err(format!( + "'{}' returned {}", + query_functions_source_url, resp.status_code + )); } - // Mapping of source id to function guids - let json_response: HashMap<String, Vec<String>> = response.json().ok()?; + let json_response: HashMap<String, Vec<String>> = resp.json().map_err(|e| e.to_string())?; let mapped_function_guids = json_response .into_iter() .filter_map(|(source_str, guid_strs)| { @@ -321,85 +292,79 @@ impl NetworkClient { }) .collect(); - Some(mapped_function_guids) + Ok(mapped_function_guids) } - /// Pushes the file to the remote source. + /// Pushes the file to the remote source, returning the commit id. /// /// NOTE: **THIS IS BLOCKING** /// - /// Route: `api/v1/files/{source}` - pub fn push_file(&self, source_id: SourceId, file: &WarpFile, name: &str) -> bool { - let push_file_url = format!("{}/api/v1/files", self.server_url); - - // Convert WarpFile to bytes - let file_bytes = file.to_bytes(); + /// Route: `api/v1/files/json` + pub fn push_file( + &self, + source_id: SourceId, + file: &WarpFile, + name: &str, + ) -> Result<i32, String> { + let push_file_url = format!("{}/api/v1/files/json", self.server_url); + // Convert WarpFile to base64 encoded bytes + let file_bytes_base64 = base64::engine::general_purpose::STANDARD.encode(&file.to_bytes()); + let mut inst = self.provider.create_instance().unwrap(); - let Ok(file_part) = reqwest::blocking::multipart::Part::bytes(file_bytes) - .file_name("data.warp") - .mime_str("application/octet-stream") - else { - log::error!("Failed to create file part"); - return false; - }; + #[derive(Deserialize)] + struct UploadResponse { + commit_id: i32, + } - let form = reqwest::blocking::multipart::Form::new() - .part("file", file_part) - .text("name", name.to_string()) - .text("source", source_id.to_string()); + let body = json!({ + "file": file_bytes_base64, + "name": name, + "source": source_id.to_string(), + "description": serde_json::Value::Null, + }); - // Send the request - match self.client.post(&push_file_url).multipart(form).send() { - Ok(response) => { - if response.status().is_success() { - true - } else { - log::error!("Failed to push file: {}", response.status()); - false - } - } - Err(e) => { - log::error!("Failed to send push request: {}", e); - false - } + let resp = inst.post_json(&push_file_url, self.headers.clone(), &body)?; + if !resp.is_success() { + return Err(format!("'{}' returned {}", push_file_url, resp.status_code)); } + let out: UploadResponse = resp.json().map_err(|e| e.to_string())?; + Ok(out.commit_id) } - pub fn function_data(&self, id: i32) -> Option<Function> { + pub fn function_data(&self, id: i32) -> Result<Function, String> { let function_data_url = format!("{}/api/v1/functions/{}/data", self.server_url, id); - let response = self.client.get(&function_data_url).send().ok()?; - if !response.status().is_success() { - log::error!( - "Failed to fetch function data for {}: {}", - id, - response.status() - ); - return None; + let mut inst = self.provider.create_instance().unwrap(); + + let resp = inst.get(&function_data_url, self.headers.clone())?; + if !resp.is_success() { + return Err(format!( + "'{}' returned {}", + function_data_url, resp.status_code + )); } - let bytes = response.bytes().ok()?; - Function::from_bytes(bytes.as_ref()) + Function::from_bytes(&resp.data) + .ok_or_else(|| format!("Failed to parse function data for function {}", id,)) } - pub fn function_datas(&self, ids: &[i32]) -> Option<Vec<Function>> { + pub fn function_datas(&self, ids: &[i32]) -> Result<Vec<Function>, String> { if ids.is_empty() { - return Some(Vec::new()); + return Ok(Vec::new()); } let function_data_url = format!("{}/api/v1/functions/data", self.server_url); + let mut inst = self.provider.create_instance().unwrap(); + let body = json!({ "ids": ids, }); - let response = self - .client - .post(&function_data_url) - .json(&body) - .send() - .ok()?; - if !response.status().is_success() { - log::error!("Failed to fetch function data: {}", response.status()); - return None; + let resp = inst.post_json(&function_data_url, self.headers.clone(), &body)?; + if !resp.is_success() { + return Err(format!( + "'{}' returned {}", + function_data_url, resp.status_code + )); } - let bytes = response.bytes().ok()?; - let file = WarpFile::from_bytes(bytes.as_ref())?; + let file = WarpFile::from_bytes(&resp.data) + .ok_or_else(|| format!("Failed to parse function data for functions {:?}", ids))?; let mut functions = Vec::with_capacity(ids.len()); for chunk in file.chunks { let ChunkKind::Signature(sc) = chunk.kind else { @@ -407,39 +372,37 @@ impl NetworkClient { }; functions.extend(sc.functions()); } - Some(functions) + Ok(functions) } - pub fn type_data(&self, guid: TypeGUID) -> Option<Type> { + pub fn type_data(&self, guid: TypeGUID) -> Result<Type, String> { let type_data_url = format!("{}/api/v1/types/{}/data", self.server_url, guid.to_string()); - let response = self.client.get(&type_data_url).send().ok()?; - if !response.status().is_success() { - log::error!( - "Failed to fetch type data for {}: {}", - guid.to_string(), - response.status() - ); - return None; + let mut inst = self.provider.create_instance().unwrap(); + + let resp = inst.get(&type_data_url, self.headers.clone())?; + if !resp.is_success() { + return Err(format!("'{}' returned {}", type_data_url, resp.status_code)); } - let bytes = response.bytes().ok()?; - Type::from_bytes(bytes.as_ref()) + Type::from_bytes(&resp.data) + .ok_or_else(|| format!("Failed to parse type data for type {}", guid)) } - pub fn type_datas(&self, guids: &[TypeGUID]) -> Option<Vec<ComputedType>> { + pub fn type_datas(&self, guids: &[TypeGUID]) -> Result<Vec<ComputedType>, String> { if guids.is_empty() { - return Some(Vec::new()); + return Ok(Vec::new()); } let type_data_url = format!("{}/api/v1/types/data", self.server_url); + let mut inst = self.provider.create_instance().unwrap(); + let body = json!({ "ids": guids.iter().map(|g| g.to_string()).collect::<Vec<_>>(), }); - let response = self.client.post(&type_data_url).json(&body).send().ok()?; - if !response.status().is_success() { - log::error!("Failed to fetch type data: {}", response.status()); - return None; + let resp = inst.post_json(&type_data_url, self.headers.clone(), &body)?; + if !resp.is_success() { + return Err(format!("'{}' returned {}", type_data_url, resp.status_code)); } - let bytes = response.bytes().ok()?; - let file = WarpFile::from_bytes(bytes.as_ref())?; + let file = WarpFile::from_bytes(&resp.data) + .ok_or_else(|| format!("Failed to parse type data for types {:?}", guids))?; let mut types = Vec::with_capacity(guids.len()); for chunk in file.chunks { let ChunkKind::Type(tc) = chunk.kind else { @@ -447,12 +410,10 @@ impl NetworkClient { }; types.extend(tc.types()); } - Some(types) + Ok(types) } - pub fn search(&self, query: &ContainerSearchQuery) -> Option<ContainerSearchResponse> { - let search_url = format!("{}/api/v1/search", self.server_url); - + pub fn search(&self, query: &ContainerSearchQuery) -> Result<ContainerSearchResponse, String> { #[derive(serde::Serialize)] struct SearchRequest<'a> { #[serde(rename = "q")] @@ -468,6 +429,26 @@ impl NetworkClient { #[serde(skip_serializing_if = "Option::is_none")] retrieve_data: Option<bool>, } + let source_id_str = query.source.map(|s| s.to_string()); + let request = SearchRequest { + q: &query.query, + limit: query.limit, + offset: query.offset, + source_id: source_id_str, + source_tags: match query.tags.is_empty() { + true => None, + false => Some(query.tags.clone()), + }, + // This must be passed to retrieve the function and type data. + retrieve_data: Some(true), + }; + let request_qs = serde_qs::to_string(&request).map_err(|e| e.to_string())?; + let search_url = match request_qs.is_empty() { + true => format!("{}/api/v1/search", self.server_url), + false => format!("{}/api/v1/search?{}", self.server_url, request_qs), + }; + + let mut inst = self.provider.create_instance().unwrap(); #[derive(serde::Deserialize)] struct SearchResponse { @@ -488,32 +469,11 @@ impl NetworkClient { data: Option<Vec<u8>>, } - let source_id_str = query.source.map(|s| s.to_string()); - let request = SearchRequest { - q: &query.query, - limit: query.limit, - offset: query.offset, - source_id: source_id_str, - source_tags: match query.tags.is_empty() { - true => None, - false => Some(query.tags.clone()), - }, - // This must be passed to retrieve the function and type data. - retrieve_data: Some(true), - }; - - let resp = match self.client.get(search_url).query(&request).send() { - Ok(r) => r, - Err(err) => { - log::error!("Failed to send search request: {}", err); - return None; - } - }; - - let Ok(parsed) = resp.json::<SearchResponse>() else { - log::error!("Failed to parse search response"); - return None; - }; + let resp = inst.get(&search_url, self.headers.clone())?; + if !resp.is_success() { + return Err(format!("'{}' returned {}", search_url, resp.status_code)); + } + let parsed: SearchResponse = resp.json().map_err(|e| e.to_string())?; // TODO: This is quite scuffed, but it works for now. (Mostly just that it looks bad and queries a lot) // TODO: Here I think would be a good place to sort it so sources always come first. @@ -580,7 +540,7 @@ impl NetworkClient { }); } - Some(ContainerSearchResponse { + Ok(ContainerSearchResponse { items, total: parsed.total, offset: parsed.offset, |
