From 3796114499c39db536a04194ec3e14a4e8c40c60 Mon Sep 17 00:00:00 2001 From: Nicholas O'Brien Date: Tue, 20 Feb 2024 21:39:55 +0000 Subject: [PATCH 1/6] added the get function with query params and headers as optional params --- docs/_docs/user-guide/eldritch.md | 6 + implants/lib/eldritch/src/http/get_impl.rs | 239 +++++++++++++++++++++ implants/lib/eldritch/src/http/mod.rs | 9 +- implants/lib/eldritch/src/runtime/mod.rs | 2 +- 4 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 implants/lib/eldritch/src/http/get_impl.rs diff --git a/docs/_docs/user-guide/eldritch.md b/docs/_docs/user-guide/eldritch.md index ab041e552..efbd60d11 100644 --- a/docs/_docs/user-guide/eldritch.md +++ b/docs/_docs/user-guide/eldritch.md @@ -420,6 +420,12 @@ The file.find method finds all files matching the used parameters. Return The http.download method downloads a file at the URI specified in `uri` to the path specified in `dst`. If a file already exists at that location, it will be overwritten. +### http.get + +`http.get(uri: str, query_params: Option>, headers: Option>) -> str` + +The http.get method sends an HTTP GET request to the URI specified in `uri` with the optional query paramters specified in `query_params` and headers specified in `headers`, then return the result as a string. Note: in order to conform with HTTP2+ all header names are transmuted to lowercase. + --- ## Pivot diff --git a/implants/lib/eldritch/src/http/get_impl.rs b/implants/lib/eldritch/src/http/get_impl.rs new file mode 100644 index 000000000..07de2eaa9 --- /dev/null +++ b/implants/lib/eldritch/src/http/get_impl.rs @@ -0,0 +1,239 @@ +use anyhow::Result; +use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; +use starlark::collections::SmallMap; +use starlark::values::Value; + +pub fn get( + uri: String, + query_params: Option>, + headers: Option>, +) -> Result { + let mut full_uri = uri.clone(); + let mut headers_map = HeaderMap::new(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + + if query_params.is_some() { + full_uri = append_query_params_to_uri(full_uri, query_params.unwrap())?; + } + + if headers.is_some() { + for (k, v) in headers.unwrap() { + let name = HeaderName::from_bytes(k.as_bytes())?; + let value = HeaderValue::from_bytes(v.as_bytes())?; + headers_map.append(name, value); + } + } + + runtime.block_on(handle_get(full_uri, headers_map)) +} + +fn append_query_params_to_uri( + mut uri: String, + query_params: SmallMap, +) -> Result { + let mut after_first_param = false; + if !uri.contains('?') { + uri.push('?') + } else { + after_first_param = true; + } + for (k, v) in query_params { + if after_first_param { + uri.push('&'); + } + let val: String; + if v.unpack_bool().is_some() { + val = format!("{}", v.unpack_bool().unwrap()); + } else if v.unpack_i32().is_some() { + val = format!("{}", v.unpack_i32().unwrap()); + } else if v.unpack_str().is_some() { + val = v.unpack_str().unwrap().to_string(); + } else { + return Err(anyhow::anyhow!( + "query_param values had an unknown type (not bool, i32, or string)" + )); + } + uri.push_str(format!("{}={}", k.as_str(), val.as_str()).as_str()); + after_first_param = true; + } + Ok(uri) +} + +async fn handle_get(uri: String, headers: HeaderMap) -> Result { + #[cfg(debug_assertions)] + log::info!( + "eldritch sending HTTP GET request to '{}' with headers '{:#?}'", + uri, + headers + ); + + let client = reqwest::Client::new().get(uri).headers(headers); + let resp = client.send().await?.text().await?; + Ok(resp) +} + +#[cfg(test)] +mod tests { + + use super::*; + use httptest::{matchers::*, responders::*, Expectation, Server}; + use starlark::const_frozen_string; + use starlark::{collections::SmallMap, values::Heap}; + + #[test] + fn test_get_no_params_or_headers() -> anyhow::Result<()> { + // running test http server + let server = Server::run(); + server.expect( + Expectation::matching(request::method_path("GET", "/foo")) + .respond_with(status_code(200).body("test body")), + ); + + // reference test server uri + let url = server.url("/foo").to_string(); + + // run our code + let contents = get(url, None, None)?; + + // check file written correctly + assert_eq!(contents, "test body"); + + Ok(()) + } + + #[test] + fn test_get_empty_params() -> anyhow::Result<()> { + // running test http server + let server = Server::run(); + server.expect( + Expectation::matching(request::method_path("GET", "/foo")) + .respond_with(status_code(200).body("test body")), + ); + + // reference test server uri + let url = server.url("/foo").to_string(); + + // run our code + let contents = get(url, Some(SmallMap::new()), None)?; + + // check file written correctly + assert_eq!(contents, "test body"); + + Ok(()) + } + + #[test] + fn test_get_with_params() -> anyhow::Result<()> { + // running test http server + let server = Server::run(); + let m = all_of![ + request::method_path("GET", "/foo"), + request::query(url_decoded(contains(("a", "true")))), + request::query(url_decoded(contains(("b", "bar")))), + request::query(url_decoded(contains(("c", "3")))), + ]; + server.expect(Expectation::matching(m).respond_with(status_code(200).body("test body"))); + + // reference test server uri + let url = server.url("/foo").to_string(); + + // run our code + let heap = Heap::new(); + let mut params = SmallMap::new(); + params.insert("a".to_string(), starlark::values::Value::new_bool(true)); + params.insert("b".to_string(), const_frozen_string!("bar").to_value()); + params.insert("c".to_string(), heap.alloc(3)); + let contents = get(url, Some(params), None)?; + + // check file written correctly + assert_eq!(contents, "test body"); + + Ok(()) + } + + #[test] + fn test_get_with_hybrid_params() -> anyhow::Result<()> { + // running test http server + let server = Server::run(); + let m = all_of![ + request::method_path("GET", "/foo"), + request::query(url_decoded(contains(("a", "true")))), + request::query(url_decoded(contains(("b", "bar")))), + request::query(url_decoded(contains(("c", "3")))), + ]; + server.expect(Expectation::matching(m).respond_with(status_code(200).body("test body"))); + + // reference test server uri + let url = server.url("/foo?a=true").to_string(); + + // run our code + let heap = Heap::new(); + let mut params = SmallMap::new(); + params.insert("b".to_string(), const_frozen_string!("bar").to_value()); + params.insert("c".to_string(), heap.alloc(3)); + let contents = get(url, Some(params), None)?; + + // check file written correctly + assert_eq!(contents, "test body"); + + Ok(()) + } + + #[test] + fn test_get_with_headers() -> anyhow::Result<()> { + // running test http server + let server = Server::run(); + let m = all_of![ + request::method_path("GET", "/foo"), + request::headers(contains(("a", "TRUE"))), + request::headers(contains(("b", "bar"))), + ]; + server.expect(Expectation::matching(m).respond_with(status_code(200).body("test body"))); + + // reference test server uri + let url = server.url("/foo").to_string(); + + // run our code + let mut headers = SmallMap::new(); + headers.insert("A".to_string(), "TRUE".to_string()); + headers.insert("b".to_string(), "bar".to_string()); + let contents = get(url, None, Some(headers))?; + + // check file written correctly + assert_eq!(contents, "test body"); + + Ok(()) + } + + #[test] + fn test_get_with_params_and_headers() -> anyhow::Result<()> { + // running test http server + let server = Server::run(); + let m = all_of![ + request::method_path("GET", "/foo"), + request::headers(contains(("a", "TRUE"))), + request::headers(contains(("b", "bar"))), + request::query(url_decoded(contains(("c", "3")))), + ]; + server.expect(Expectation::matching(m).respond_with(status_code(200).body("test body"))); + + // reference test server uri + let url = server.url("/foo").to_string(); + + // run our code + let mut headers = SmallMap::new(); + headers.insert("A".to_string(), "TRUE".to_string()); + headers.insert("b".to_string(), "bar".to_string()); + let heap = Heap::new(); + let mut params = SmallMap::new(); + params.insert("c".to_string(), heap.alloc(3)); + let contents = get(url, Some(params), Some(headers))?; + + // check file written correctly + assert_eq!(contents, "test body"); + + Ok(()) + } +} diff --git a/implants/lib/eldritch/src/http/mod.rs b/implants/lib/eldritch/src/http/mod.rs index 97b7857ac..0b9e5d5a0 100644 --- a/implants/lib/eldritch/src/http/mod.rs +++ b/implants/lib/eldritch/src/http/mod.rs @@ -1,9 +1,11 @@ mod download_impl; +mod get_impl; use starlark::{ + collections::SmallMap, environment::MethodsBuilder, starlark_module, - values::{none::NoneType, starlark_value}, + values::{none::NoneType, starlark_value, Value}, }; /* @@ -24,4 +26,9 @@ fn methods(builder: &mut MethodsBuilder) { download_impl::download(uri, dst)?; Ok(NoneType{}) } + + #[allow(unused_variables)] + fn get(this: &HTTPLibrary, uri: String, query_params: Option>, headers: Option>) -> anyhow::Result { + get_impl::get(uri, query_params, headers) + } } diff --git a/implants/lib/eldritch/src/runtime/mod.rs b/implants/lib/eldritch/src/runtime/mod.rs index 899f70b5f..976023d5c 100644 --- a/implants/lib/eldritch/src/runtime/mod.rs +++ b/implants/lib/eldritch/src/runtime/mod.rs @@ -179,7 +179,7 @@ mod tests { parameters: HashMap::new(), file_names: Vec::new(), }, - want_text: format!("{}\n", r#"["download"]"#), + want_text: format!("{}\n", r#"["download", "get"]"#), want_error: None, }, } From cb890b4662f2364c9bb3b8aec4a438f2c15f5029 Mon Sep 17 00:00:00 2001 From: Nicholas O'Brien Date: Tue, 20 Feb 2024 22:01:50 +0000 Subject: [PATCH 2/6] only strings for query params --- docs/_docs/user-guide/eldritch.md | 2 +- implants/lib/eldritch/src/http/get_impl.rs | 37 ++++++---------------- implants/lib/eldritch/src/http/mod.rs | 2 +- 3 files changed, 12 insertions(+), 29 deletions(-) diff --git a/docs/_docs/user-guide/eldritch.md b/docs/_docs/user-guide/eldritch.md index efbd60d11..f57a55c41 100644 --- a/docs/_docs/user-guide/eldritch.md +++ b/docs/_docs/user-guide/eldritch.md @@ -422,7 +422,7 @@ The http.download method downloads a file at the URI specified in `uri` t ### http.get -`http.get(uri: str, query_params: Option>, headers: Option>) -> str` +`http.get(uri: str, query_params: Option>, headers: Option>) -> str` The http.get method sends an HTTP GET request to the URI specified in `uri` with the optional query paramters specified in `query_params` and headers specified in `headers`, then return the result as a string. Note: in order to conform with HTTP2+ all header names are transmuted to lowercase. diff --git a/implants/lib/eldritch/src/http/get_impl.rs b/implants/lib/eldritch/src/http/get_impl.rs index 07de2eaa9..3f5b5d0cc 100644 --- a/implants/lib/eldritch/src/http/get_impl.rs +++ b/implants/lib/eldritch/src/http/get_impl.rs @@ -1,11 +1,10 @@ use anyhow::Result; use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use starlark::collections::SmallMap; -use starlark::values::Value; pub fn get( uri: String, - query_params: Option>, + query_params: Option>, headers: Option>, ) -> Result { let mut full_uri = uri.clone(); @@ -31,7 +30,7 @@ pub fn get( fn append_query_params_to_uri( mut uri: String, - query_params: SmallMap, + query_params: SmallMap, ) -> Result { let mut after_first_param = false; if !uri.contains('?') { @@ -43,19 +42,7 @@ fn append_query_params_to_uri( if after_first_param { uri.push('&'); } - let val: String; - if v.unpack_bool().is_some() { - val = format!("{}", v.unpack_bool().unwrap()); - } else if v.unpack_i32().is_some() { - val = format!("{}", v.unpack_i32().unwrap()); - } else if v.unpack_str().is_some() { - val = v.unpack_str().unwrap().to_string(); - } else { - return Err(anyhow::anyhow!( - "query_param values had an unknown type (not bool, i32, or string)" - )); - } - uri.push_str(format!("{}={}", k.as_str(), val.as_str()).as_str()); + uri.push_str(format!("{}={}", k.as_str(), v.as_str()).as_str()); after_first_param = true; } Ok(uri) @@ -79,8 +66,7 @@ mod tests { use super::*; use httptest::{matchers::*, responders::*, Expectation, Server}; - use starlark::const_frozen_string; - use starlark::{collections::SmallMap, values::Heap}; + use starlark::collections::SmallMap; #[test] fn test_get_no_params_or_headers() -> anyhow::Result<()> { @@ -140,11 +126,10 @@ mod tests { let url = server.url("/foo").to_string(); // run our code - let heap = Heap::new(); let mut params = SmallMap::new(); - params.insert("a".to_string(), starlark::values::Value::new_bool(true)); - params.insert("b".to_string(), const_frozen_string!("bar").to_value()); - params.insert("c".to_string(), heap.alloc(3)); + params.insert("a".to_string(), "true".to_string()); + params.insert("b".to_string(), "bar".to_string()); + params.insert("c".to_string(), "3".to_string()); let contents = get(url, Some(params), None)?; // check file written correctly @@ -169,10 +154,9 @@ mod tests { let url = server.url("/foo?a=true").to_string(); // run our code - let heap = Heap::new(); let mut params = SmallMap::new(); - params.insert("b".to_string(), const_frozen_string!("bar").to_value()); - params.insert("c".to_string(), heap.alloc(3)); + params.insert("b".to_string(), "bar".to_string()); + params.insert("c".to_string(), "3".to_string()); let contents = get(url, Some(params), None)?; // check file written correctly @@ -226,9 +210,8 @@ mod tests { let mut headers = SmallMap::new(); headers.insert("A".to_string(), "TRUE".to_string()); headers.insert("b".to_string(), "bar".to_string()); - let heap = Heap::new(); let mut params = SmallMap::new(); - params.insert("c".to_string(), heap.alloc(3)); + params.insert("c".to_string(), "3".to_string()); let contents = get(url, Some(params), Some(headers))?; // check file written correctly diff --git a/implants/lib/eldritch/src/http/mod.rs b/implants/lib/eldritch/src/http/mod.rs index 0b9e5d5a0..e5dac7e4b 100644 --- a/implants/lib/eldritch/src/http/mod.rs +++ b/implants/lib/eldritch/src/http/mod.rs @@ -28,7 +28,7 @@ fn methods(builder: &mut MethodsBuilder) { } #[allow(unused_variables)] - fn get(this: &HTTPLibrary, uri: String, query_params: Option>, headers: Option>) -> anyhow::Result { + fn get(this: &HTTPLibrary, uri: String, query_params: Option>, headers: Option>) -> anyhow::Result { get_impl::get(uri, query_params, headers) } } From 34c35b65b7e89e91fc88da8c173d32032caaa2cf Mon Sep 17 00:00:00 2001 From: Nicholas O'Brien Date: Tue, 20 Feb 2024 23:48:20 +0000 Subject: [PATCH 3/6] and now post as well --- docs/_docs/user-guide/eldritch.md | 6 + implants/lib/eldritch/src/http/get_impl.rs | 16 +- implants/lib/eldritch/src/http/mod.rs | 8 +- implants/lib/eldritch/src/http/post_impl.rs | 226 ++++++++++++++++++++ implants/lib/eldritch/src/runtime/mod.rs | 2 +- 5 files changed, 248 insertions(+), 10 deletions(-) create mode 100644 implants/lib/eldritch/src/http/post_impl.rs diff --git a/docs/_docs/user-guide/eldritch.md b/docs/_docs/user-guide/eldritch.md index f57a55c41..fa37cc45d 100644 --- a/docs/_docs/user-guide/eldritch.md +++ b/docs/_docs/user-guide/eldritch.md @@ -426,6 +426,12 @@ The http.download method downloads a file at the URI specified in `uri` t The http.get method sends an HTTP GET request to the URI specified in `uri` with the optional query paramters specified in `query_params` and headers specified in `headers`, then return the result as a string. Note: in order to conform with HTTP2+ all header names are transmuted to lowercase. +### http.post + +`http.post(uri: str, body: Option, form: Option>, headers: Option>) -> str` + +The http.post method sends an HTTP POST request to the URI specified in `uri` with the optional request body specified by `body`, form paramters specified in `form`, and headers specified in `headers`, then return the result as a string. Note: in order to conform with HTTP2+ all header names are transmuted to lowercase. Other Note: if a `body` and a `form` are supplied the value of `body` will be used. + --- ## Pivot diff --git a/implants/lib/eldritch/src/http/get_impl.rs b/implants/lib/eldritch/src/http/get_impl.rs index 3f5b5d0cc..6094b4c51 100644 --- a/implants/lib/eldritch/src/http/get_impl.rs +++ b/implants/lib/eldritch/src/http/get_impl.rs @@ -83,14 +83,14 @@ mod tests { // run our code let contents = get(url, None, None)?; - // check file written correctly + // check request returned correctly assert_eq!(contents, "test body"); Ok(()) } #[test] - fn test_get_empty_params() -> anyhow::Result<()> { + fn test_get_empty_params_and_headers() -> anyhow::Result<()> { // running test http server let server = Server::run(); server.expect( @@ -102,9 +102,9 @@ mod tests { let url = server.url("/foo").to_string(); // run our code - let contents = get(url, Some(SmallMap::new()), None)?; + let contents = get(url, Some(SmallMap::new()), Some(SmallMap::new()))?; - // check file written correctly + // check request returned correctly assert_eq!(contents, "test body"); Ok(()) @@ -132,7 +132,7 @@ mod tests { params.insert("c".to_string(), "3".to_string()); let contents = get(url, Some(params), None)?; - // check file written correctly + // check request returned correctly assert_eq!(contents, "test body"); Ok(()) @@ -159,7 +159,7 @@ mod tests { params.insert("c".to_string(), "3".to_string()); let contents = get(url, Some(params), None)?; - // check file written correctly + // check request returned correctly assert_eq!(contents, "test body"); Ok(()) @@ -185,7 +185,7 @@ mod tests { headers.insert("b".to_string(), "bar".to_string()); let contents = get(url, None, Some(headers))?; - // check file written correctly + // check request returned correctly assert_eq!(contents, "test body"); Ok(()) @@ -214,7 +214,7 @@ mod tests { params.insert("c".to_string(), "3".to_string()); let contents = get(url, Some(params), Some(headers))?; - // check file written correctly + // check request returned correctly assert_eq!(contents, "test body"); Ok(()) diff --git a/implants/lib/eldritch/src/http/mod.rs b/implants/lib/eldritch/src/http/mod.rs index e5dac7e4b..148013415 100644 --- a/implants/lib/eldritch/src/http/mod.rs +++ b/implants/lib/eldritch/src/http/mod.rs @@ -1,11 +1,12 @@ mod download_impl; mod get_impl; +mod post_impl; use starlark::{ collections::SmallMap, environment::MethodsBuilder, starlark_module, - values::{none::NoneType, starlark_value, Value}, + values::{none::NoneType, starlark_value}, }; /* @@ -31,4 +32,9 @@ fn methods(builder: &mut MethodsBuilder) { fn get(this: &HTTPLibrary, uri: String, query_params: Option>, headers: Option>) -> anyhow::Result { get_impl::get(uri, query_params, headers) } + + #[allow(unused_variables)] + fn post(this: &HTTPLibrary, uri: String, body: Option, form: Option>, headers: Option>) -> anyhow::Result { + post_impl::post(uri, body, form, headers) + } } diff --git a/implants/lib/eldritch/src/http/post_impl.rs b/implants/lib/eldritch/src/http/post_impl.rs new file mode 100644 index 000000000..876870041 --- /dev/null +++ b/implants/lib/eldritch/src/http/post_impl.rs @@ -0,0 +1,226 @@ +use anyhow::Result; +use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; +use starlark::collections::SmallMap; +use std::collections::HashMap; + +pub fn post( + uri: String, + body: Option, + form: Option>, + headers: Option>, +) -> Result { + let mut headers_map = HeaderMap::new(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + + if headers.is_some() { + for (k, v) in headers.unwrap() { + let name = HeaderName::from_bytes(k.as_bytes())?; + let value = HeaderValue::from_bytes(v.as_bytes())?; + headers_map.append(name, value); + } + } + + if body.is_some() { + return runtime.block_on(handle_post(uri, body, None, headers_map)); + } + + if form.is_some() { + let mut form_map = HashMap::new(); + for (k, v) in form.unwrap() { + form_map.insert(k, v); + } + + return runtime.block_on(handle_post(uri, None, Some(form_map), headers_map)); + } + + runtime.block_on(handle_post(uri, None, None, headers_map)) +} + +async fn handle_post( + uri: String, + body: Option, + form: Option>, + headers: HeaderMap, +) -> Result { + #[cfg(debug_assertions)] + log::info!( + "eldritch sending HTTP POST request to '{}' with headers '{:#?}'", + uri, + headers + ); + + let mut client = reqwest::Client::new().post(uri).headers(headers); + if body.is_some() { + client = client.body(body.unwrap()); + } + if form.is_some() { + client = client.form(&form.unwrap()); + } + let resp = client.send().await?.text().await?; + Ok(resp) +} + +#[cfg(test)] +mod tests { + + use super::*; + use httptest::{matchers::*, responders::*, Expectation, Server}; + use starlark::collections::SmallMap; + + #[test] + fn test_post_no_body_or_params_or_headers() -> anyhow::Result<()> { + // running test http server + let server = Server::run(); + server.expect( + Expectation::matching(request::method_path("POST", "/foo")) + .respond_with(status_code(200).body("test body")), + ); + + // reference test server uri + let url = server.url("/foo").to_string(); + + // run our code + let contents = post(url, None, None, None)?; + + // check request returned correctly + assert_eq!(contents, "test body"); + + Ok(()) + } + + #[test] + fn test_post_empty_params_and_headers() -> anyhow::Result<()> { + // running test http server + let server = Server::run(); + server.expect( + Expectation::matching(request::method_path("POST", "/foo")) + .respond_with(status_code(200).body("test body")), + ); + + // reference test server uri + let url = server.url("/foo").to_string(); + + // run our code + let contents = post(url, None, Some(SmallMap::new()), Some(SmallMap::new()))?; + + // check request returned correctly + assert_eq!(contents, "test body"); + + Ok(()) + } + + #[test] + fn test_post_with_params() -> anyhow::Result<()> { + // running test http server + let server = Server::run(); + let m = all_of![ + request::method_path("POST", "/foo"), + request::body(url_decoded(contains(("a", "true")))), + request::body(url_decoded(contains(("b", "bar")))), + request::body(url_decoded(contains(("c", "3")))), + ]; + server.expect(Expectation::matching(m).respond_with(status_code(200).body("test body"))); + + // reference test server uri + let url = server.url("/foo").to_string(); + + // run our code + let mut params = SmallMap::new(); + params.insert("a".to_string(), "true".to_string()); + params.insert("b".to_string(), "bar".to_string()); + params.insert("c".to_string(), "3".to_string()); + let contents = post(url, None, Some(params), None)?; + + // check request returned correctly + assert_eq!(contents, "test body"); + + Ok(()) + } + + #[test] + fn test_post_with_headers() -> anyhow::Result<()> { + // running test http server + let server = Server::run(); + let m = all_of![ + request::method_path("POST", "/foo"), + request::headers(contains(("a", "TRUE"))), + request::headers(contains(("b", "bar"))), + ]; + server.expect(Expectation::matching(m).respond_with(status_code(200).body("test body"))); + + // reference test server uri + let url = server.url("/foo").to_string(); + + // run our code + let mut headers = SmallMap::new(); + headers.insert("A".to_string(), "TRUE".to_string()); + headers.insert("b".to_string(), "bar".to_string()); + let contents = post(url, None, None, Some(headers))?; + + // check request returned correctly + assert_eq!(contents, "test body"); + + Ok(()) + } + + #[test] + fn test_post_with_params_and_headers() -> anyhow::Result<()> { + // running test http server + let server = Server::run(); + let m = all_of![ + request::method_path("POST", "/foo"), + request::headers(contains(("a", "TRUE"))), + request::headers(contains(("b", "bar"))), + request::body(url_decoded(contains(("c", "3")))), + ]; + server.expect(Expectation::matching(m).respond_with(status_code(200).body("test body"))); + + // reference test server uri + let url = server.url("/foo").to_string(); + + // run our code + let mut headers = SmallMap::new(); + headers.insert("A".to_string(), "TRUE".to_string()); + headers.insert("b".to_string(), "bar".to_string()); + let mut params = SmallMap::new(); + params.insert("c".to_string(), "3".to_string()); + let contents = post(url, None, Some(params), Some(headers))?; + + // check request returned correctly + assert_eq!(contents, "test body"); + + Ok(()) + } + + #[test] + fn test_post_with_body_and_header() -> anyhow::Result<()> { + // running test http server + let server = Server::run(); + let m = all_of![ + request::method_path("POST", "/foo"), + request::headers(contains(("a", "TRUE"))), + request::body("the quick brown fox jumps over the lazy dog"), + ]; + server.expect(Expectation::matching(m).respond_with(status_code(200).body("test body"))); + + // reference test server uri + let url = server.url("/foo").to_string(); + + // run our code + let mut headers = SmallMap::new(); + headers.insert("A".to_string(), "TRUE".to_string()); + let contents = post( + url, + Some(String::from("the quick brown fox jumps over the lazy dog")), + None, + Some(headers), + )?; + + // check request returned correctly + assert_eq!(contents, "test body"); + + Ok(()) + } +} diff --git a/implants/lib/eldritch/src/runtime/mod.rs b/implants/lib/eldritch/src/runtime/mod.rs index 976023d5c..1a8d7b449 100644 --- a/implants/lib/eldritch/src/runtime/mod.rs +++ b/implants/lib/eldritch/src/runtime/mod.rs @@ -179,7 +179,7 @@ mod tests { parameters: HashMap::new(), file_names: Vec::new(), }, - want_text: format!("{}\n", r#"["download", "get"]"#), + want_text: format!("{}\n", r#"["download", "get", "post"]"#), want_error: None, }, } From 91032c4369aa2ae0a66663acd648bdaed16aeef0 Mon Sep 17 00:00:00 2001 From: Nicholas O'Brien Date: Tue, 20 Feb 2024 23:50:09 +0000 Subject: [PATCH 4/6] typo --- docs/_docs/user-guide/eldritch.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/user-guide/eldritch.md b/docs/_docs/user-guide/eldritch.md index fa37cc45d..431f6c32b 100644 --- a/docs/_docs/user-guide/eldritch.md +++ b/docs/_docs/user-guide/eldritch.md @@ -428,7 +428,7 @@ The http.get method sends an HTTP GET request to the URI specified in `ur ### http.post -`http.post(uri: str, body: Option, form: Option>, headers: Option>) -> str` +`http.post(uri: str, body: Option, form: Option>, headers: Option>) -> str` The http.post method sends an HTTP POST request to the URI specified in `uri` with the optional request body specified by `body`, form paramters specified in `form`, and headers specified in `headers`, then return the result as a string. Note: in order to conform with HTTP2+ all header names are transmuted to lowercase. Other Note: if a `body` and a `form` are supplied the value of `body` will be used. From 7a1bb45aa124b6af6fd54be61e8ae82d885b039d Mon Sep 17 00:00:00 2001 From: Nicholas O'Brien Date: Tue, 20 Feb 2024 23:52:58 +0000 Subject: [PATCH 5/6] logic fix --- implants/lib/eldritch/src/http/post_impl.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/implants/lib/eldritch/src/http/post_impl.rs b/implants/lib/eldritch/src/http/post_impl.rs index 876870041..09418b21c 100644 --- a/implants/lib/eldritch/src/http/post_impl.rs +++ b/implants/lib/eldritch/src/http/post_impl.rs @@ -51,12 +51,14 @@ async fn handle_post( headers ); - let mut client = reqwest::Client::new().post(uri).headers(headers); + let client = reqwest::Client::new().post(uri).headers(headers); if body.is_some() { - client = client.body(body.unwrap()); + let resp = client.body(body.unwrap()).send().await?.text().await?; + return Ok(resp); } if form.is_some() { - client = client.form(&form.unwrap()); + let resp = client.form(&form.unwrap()).send().await?.text().await?; + return Ok(resp); } let resp = client.send().await?.text().await?; Ok(resp) From 420b1a2b205ca6f6975eef95735c4c3a1b88b86a Mon Sep 17 00:00:00 2001 From: Nicholas O'Brien Date: Wed, 21 Feb 2024 00:22:24 +0000 Subject: [PATCH 6/6] fixed sugestions --- docs/_docs/user-guide/eldritch.md | 4 +- implants/lib/eldritch/src/http/get_impl.rs | 44 ++++++++------------- implants/lib/eldritch/src/http/post_impl.rs | 16 ++++---- 3 files changed, 27 insertions(+), 37 deletions(-) diff --git a/docs/_docs/user-guide/eldritch.md b/docs/_docs/user-guide/eldritch.md index 431f6c32b..361729a12 100644 --- a/docs/_docs/user-guide/eldritch.md +++ b/docs/_docs/user-guide/eldritch.md @@ -424,13 +424,13 @@ The http.download method downloads a file at the URI specified in `uri` t `http.get(uri: str, query_params: Option>, headers: Option>) -> str` -The http.get method sends an HTTP GET request to the URI specified in `uri` with the optional query paramters specified in `query_params` and headers specified in `headers`, then return the result as a string. Note: in order to conform with HTTP2+ all header names are transmuted to lowercase. +The http.get method sends an HTTP GET request to the URI specified in `uri` with the optional query paramters specified in `query_params` and headers specified in `headers`, then return the response body as a string. Note: in order to conform with HTTP2+ all header names are transmuted to lowercase. ### http.post `http.post(uri: str, body: Option, form: Option>, headers: Option>) -> str` -The http.post method sends an HTTP POST request to the URI specified in `uri` with the optional request body specified by `body`, form paramters specified in `form`, and headers specified in `headers`, then return the result as a string. Note: in order to conform with HTTP2+ all header names are transmuted to lowercase. Other Note: if a `body` and a `form` are supplied the value of `body` will be used. +The http.post method sends an HTTP POST request to the URI specified in `uri` with the optional request body specified by `body`, form paramters specified in `form`, and headers specified in `headers`, then return the response body as a string. Note: in order to conform with HTTP2+ all header names are transmuted to lowercase. Other Note: if a `body` and a `form` are supplied the value of `body` will be used. --- diff --git a/implants/lib/eldritch/src/http/get_impl.rs b/implants/lib/eldritch/src/http/get_impl.rs index 6094b4c51..e724d3f3a 100644 --- a/implants/lib/eldritch/src/http/get_impl.rs +++ b/implants/lib/eldritch/src/http/get_impl.rs @@ -1,54 +1,41 @@ use anyhow::Result; use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use starlark::collections::SmallMap; +use std::collections::HashMap; pub fn get( uri: String, query_params: Option>, headers: Option>, ) -> Result { - let mut full_uri = uri.clone(); + let mut query_map = HashMap::new(); let mut headers_map = HeaderMap::new(); let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build()?; - if query_params.is_some() { - full_uri = append_query_params_to_uri(full_uri, query_params.unwrap())?; + if let Some(q) = query_params { + for (k, v) in q { + query_map.insert(k, v); + } } - if headers.is_some() { - for (k, v) in headers.unwrap() { + if let Some(h) = headers { + for (k, v) in h { let name = HeaderName::from_bytes(k.as_bytes())?; let value = HeaderValue::from_bytes(v.as_bytes())?; headers_map.append(name, value); } } - runtime.block_on(handle_get(full_uri, headers_map)) + runtime.block_on(handle_get(uri, query_map, headers_map)) } -fn append_query_params_to_uri( - mut uri: String, - query_params: SmallMap, +async fn handle_get( + uri: String, + query_params: HashMap, + headers: HeaderMap, ) -> Result { - let mut after_first_param = false; - if !uri.contains('?') { - uri.push('?') - } else { - after_first_param = true; - } - for (k, v) in query_params { - if after_first_param { - uri.push('&'); - } - uri.push_str(format!("{}={}", k.as_str(), v.as_str()).as_str()); - after_first_param = true; - } - Ok(uri) -} - -async fn handle_get(uri: String, headers: HeaderMap) -> Result { #[cfg(debug_assertions)] log::info!( "eldritch sending HTTP GET request to '{}' with headers '{:#?}'", @@ -56,7 +43,10 @@ async fn handle_get(uri: String, headers: HeaderMap) -> Result { headers ); - let client = reqwest::Client::new().get(uri).headers(headers); + let client = reqwest::Client::new() + .get(uri) + .headers(headers) + .query(&query_params); let resp = client.send().await?.text().await?; Ok(resp) } diff --git a/implants/lib/eldritch/src/http/post_impl.rs b/implants/lib/eldritch/src/http/post_impl.rs index 09418b21c..091d32b88 100644 --- a/implants/lib/eldritch/src/http/post_impl.rs +++ b/implants/lib/eldritch/src/http/post_impl.rs @@ -14,8 +14,8 @@ pub fn post( .enable_all() .build()?; - if headers.is_some() { - for (k, v) in headers.unwrap() { + if let Some(h) = headers { + for (k, v) in h { let name = HeaderName::from_bytes(k.as_bytes())?; let value = HeaderValue::from_bytes(v.as_bytes())?; headers_map.append(name, value); @@ -26,9 +26,9 @@ pub fn post( return runtime.block_on(handle_post(uri, body, None, headers_map)); } - if form.is_some() { + if let Some(f) = form { let mut form_map = HashMap::new(); - for (k, v) in form.unwrap() { + for (k, v) in f { form_map.insert(k, v); } @@ -52,12 +52,12 @@ async fn handle_post( ); let client = reqwest::Client::new().post(uri).headers(headers); - if body.is_some() { - let resp = client.body(body.unwrap()).send().await?.text().await?; + if let Some(b) = body { + let resp = client.body(b).send().await?.text().await?; return Ok(resp); } - if form.is_some() { - let resp = client.form(&form.unwrap()).send().await?.text().await?; + if let Some(f) = form { + let resp = client.form(&f).send().await?.text().await?; return Ok(resp); } let resp = client.send().await?.text().await?;