diff --git a/docs/_docs/user-guide/eldritch.md b/docs/_docs/user-guide/eldritch.md index ab041e552..361729a12 100644 --- a/docs/_docs/user-guide/eldritch.md +++ b/docs/_docs/user-guide/eldritch.md @@ -420,6 +420,18 @@ 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 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 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. + --- ## 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..e724d3f3a --- /dev/null +++ b/implants/lib/eldritch/src/http/get_impl.rs @@ -0,0 +1,212 @@ +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 query_map = HashMap::new(); + let mut headers_map = HeaderMap::new(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + + if let Some(q) = query_params { + for (k, v) in q { + query_map.insert(k, v); + } + } + + 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(uri, query_map, headers_map)) +} + +async fn handle_get( + uri: String, + query_params: HashMap, + 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) + .query(&query_params); + 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_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 request returned correctly + assert_eq!(contents, "test body"); + + Ok(()) + } + + #[test] + fn test_get_empty_params_and_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, Some(SmallMap::new()), Some(SmallMap::new()))?; + + // check request returned 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 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 = get(url, Some(params), None)?; + + // check request returned 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 mut params = SmallMap::new(); + params.insert("b".to_string(), "bar".to_string()); + params.insert("c".to_string(), "3".to_string()); + let contents = get(url, Some(params), None)?; + + // check request returned 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 request returned 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 mut params = SmallMap::new(); + params.insert("c".to_string(), "3".to_string()); + let contents = get(url, Some(params), Some(headers))?; + + // 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 97b7857ac..148013415 100644 --- a/implants/lib/eldritch/src/http/mod.rs +++ b/implants/lib/eldritch/src/http/mod.rs @@ -1,6 +1,9 @@ mod download_impl; +mod get_impl; +mod post_impl; use starlark::{ + collections::SmallMap, environment::MethodsBuilder, starlark_module, values::{none::NoneType, starlark_value}, @@ -24,4 +27,14 @@ 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) + } + + #[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..091d32b88 --- /dev/null +++ b/implants/lib/eldritch/src/http/post_impl.rs @@ -0,0 +1,228 @@ +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 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); + } + } + + if body.is_some() { + return runtime.block_on(handle_post(uri, body, None, headers_map)); + } + + if let Some(f) = form { + let mut form_map = HashMap::new(); + for (k, v) in f { + 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 client = reqwest::Client::new().post(uri).headers(headers); + if let Some(b) = body { + let resp = client.body(b).send().await?.text().await?; + return Ok(resp); + } + if let Some(f) = form { + let resp = client.form(&f).send().await?.text().await?; + return Ok(resp); + } + 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 899f70b5f..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"]"#), + want_text: format!("{}\n", r#"["download", "get", "post"]"#), want_error: None, }, }