use std::{env, fmt, path::PathBuf, str::FromStr, time::Duration};
use bytes::Bytes;
use clap::crate_version;
use reqwest::{
header::{HeaderMap, HeaderValue, CONTENT_TYPE, USER_AGENT},
Response, StatusCode,
};
use serde::de::DeserializeOwned;
use serde::ser::Serialize;
use crate::{
commons::file,
constants::{
HTTP_CLIENT_TIMEOUT_SECS, KRILL_CLI_API_ENV,
KRILL_HTTPS_ROOT_CERTS_ENV,
},
};
use crate::api::admin::Token;
use crate::api::status::ErrorResponse;
const JSON_CONTENT: &str = "application/json";
fn report_get_and_exit(uri: &str, token: Option<&Token>) {
println!("GET:\n {uri}");
if let Some(token) = token {
println!("Headers:\n Authorization: Bearer {token}");
}
std::process::exit(0);
}
fn report_post_and_exit(
uri: &str,
content_type: Option<&str>,
token: Option<&Token>,
body: &str,
) {
println!("POST:\n {uri}");
if content_type.is_some() || token.is_some() {
println!("Headers:");
}
if let Some(content_type) = content_type {
println!(" content-type: {content_type}");
}
if let Some(token) = token {
println!(" Authorization: Bearer {token}");
}
println!("Body:\n{body}");
std::process::exit(0);
}
fn report_delete(
uri: &str,
content_type: Option<&str>,
token: Option<&Token>,
) {
if env::var(KRILL_CLI_API_ENV).is_ok() {
println!("DELETE:\n {uri}");
if content_type.is_some() || token.is_some() {
println!("Headers:");
}
if let Some(content_type) = content_type {
println!(" content-type: {content_type}");
}
if let Some(token) = token {
println!(" Authorization: Bearer {token}");
}
std::process::exit(0);
}
}
pub fn get_bearer_token(
request: &hyper::Request<hyper::body::Incoming>,
) -> Option<Token> {
request
.headers()
.get(hyper::header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|header_string| {
header_string
.strip_prefix("Bearer ")
.map(|s| Token::from(s.trim()))
})
}
pub async fn get_json<T: DeserializeOwned>(
uri: &str,
token: Option<&Token>,
) -> Result<T, Error> {
if env::var(KRILL_CLI_API_ENV).is_ok() {
report_get_and_exit(uri, token);
}
let headers = headers(uri, Some(JSON_CONTENT), token)?;
let res = client(uri)?
.get(uri)
.headers(headers)
.send()
.await
.map_err(|e| Error::execute(uri, e))?;
process_json_response(uri, res).await
}
pub async fn get_text(
uri: &str,
token: Option<&Token>,
) -> Result<String, Error> {
if env::var(KRILL_CLI_API_ENV).is_ok() {
report_get_and_exit(uri, token);
}
let headers = headers(uri, None, token)?;
let res = client(uri)?
.get(uri)
.headers(headers)
.send()
.await
.map_err(|e| Error::execute(uri, e))?;
text_response(uri, res).await
}
pub async fn get_ok(uri: &str, token: Option<&Token>) -> Result<(), Error> {
if env::var(KRILL_CLI_API_ENV).is_ok() {
report_get_and_exit(uri, token);
}
let headers = headers(uri, None, token)?;
let res = client(uri)?
.get(uri)
.headers(headers)
.send()
.await
.map_err(|e| Error::execute(uri, e))?;
opt_text_response(uri, res).await?; Ok(())
}
pub async fn post_json(
uri: &str,
data: impl Serialize,
token: Option<&Token>,
) -> Result<(), Error> {
let body = serde_json::to_string_pretty(&data)
.map_err(|e| Error::request_build_json(uri, e))?;
if env::var(KRILL_CLI_API_ENV).is_ok() {
report_post_and_exit(uri, Some(JSON_CONTENT), token, &body);
}
let headers = headers(uri, Some(JSON_CONTENT), token)?;
let res = client(uri)?
.post(uri)
.headers(headers)
.body(body)
.send()
.await
.map_err(|e| Error::execute(uri, e))?;
empty_response(uri, res).await
}
pub async fn post_json_with_response<T: DeserializeOwned>(
uri: &str,
data: impl Serialize,
token: Option<&Token>,
) -> Result<T, Error> {
match post_json_with_opt_response(uri, data, token).await? {
None => Err(Error::response(uri, "expected JSON response")),
Some(res) => Ok(res),
}
}
pub async fn post_json_with_opt_response<T: DeserializeOwned>(
uri: &str,
data: impl Serialize,
token: Option<&Token>,
) -> Result<Option<T>, Error> {
let body = serde_json::to_string_pretty(&data)
.map_err(|e| Error::request_build_json(uri, e))?;
if env::var(KRILL_CLI_API_ENV).is_ok() {
report_post_and_exit(uri, Some(JSON_CONTENT), token, &body);
}
let headers = headers(uri, Some(JSON_CONTENT), token)?;
let res = client(uri)?
.post(uri)
.headers(headers)
.body(body)
.send()
.await
.map_err(|e| Error::execute(uri, e))?;
process_opt_json_response(uri, res).await
}
pub async fn post_empty(
uri: &str,
token: Option<&Token>,
) -> Result<(), Error> {
let res = do_empty_post(uri, token).await?;
empty_response(uri, res).await
}
pub async fn post_empty_with_response<T: DeserializeOwned>(
uri: &str,
token: Option<&Token>,
) -> Result<T, Error> {
let res = do_empty_post(uri, token).await?;
process_json_response(uri, res).await
}
async fn do_empty_post(
uri: &str,
token: Option<&Token>,
) -> Result<Response, Error> {
if env::var(KRILL_CLI_API_ENV).is_ok() {
report_post_and_exit(uri, None, token, "<empty>");
}
let headers = headers(uri, Some(JSON_CONTENT), token)?;
client(uri)?
.post(uri)
.headers(headers)
.send()
.await
.map_err(|e| Error::execute(uri, e))
}
pub async fn post_binary_with_full_ua(
uri: &str,
data: &Bytes,
content_type: &str,
timeout: u64,
) -> Result<Bytes, Error> {
let body = data.to_vec();
let mut headers = HeaderMap::new();
let ua_string = concat!("krill/{}", crate_version!());
let user_agent_value = HeaderValue::from_str(ua_string)
.map_err(|e| Error::request_build(uri, e))?;
let content_type_value = HeaderValue::from_str(content_type)
.map_err(|e| Error::request_build(uri, e))?;
headers.insert(USER_AGENT, user_agent_value);
headers.insert(CONTENT_TYPE, content_type_value);
let client = reqwest::ClientBuilder::new()
.timeout(Duration::from_secs(timeout))
.danger_accept_invalid_certs(true)
.build()
.map_err(|e| Error::request_build(uri, e))?;
let res = client
.post(uri)
.headers(headers)
.body(body)
.send()
.await
.map_err(|e| Error::execute(uri, e))?;
match res.status() {
StatusCode::OK => {
let bytes = res.bytes().await.map_err(|e| {
Error::response(uri, format!("cannot get body: {e}"))
})?;
Ok(bytes)
}
_ => Err(Error::from_res(uri, res).await),
}
}
pub async fn delete(uri: &str, token: Option<&Token>) -> Result<(), Error> {
report_delete(uri, None, token);
let headers = headers(uri, None, token)?;
let res = client(uri)?
.delete(uri)
.headers(headers)
.send()
.await
.map_err(|e| Error::execute(uri, e))?;
match res.status() {
StatusCode::OK => Ok(()),
_ => Err(Error::from_res(uri, res).await),
}
}
#[allow(clippy::result_large_err)]
fn load_root_cert(path_str: &str) -> Result<reqwest::Certificate, Error> {
let path = PathBuf::from_str(path_str)
.map_err(|e| Error::request_build_https_cert(path_str, e))?;
let file = file::read(&path)
.map_err(|e| Error::request_build_https_cert(path_str, e))?;
reqwest::Certificate::from_pem(file.as_ref())
.map_err(|e| Error::request_build_https_cert(path_str, e))
}
#[allow(clippy::result_large_err)]
fn client(uri: &str) -> Result<reqwest::Client, Error> {
client_with_tweaks(
uri,
Duration::from_secs(HTTP_CLIENT_TIMEOUT_SECS),
true,
)
}
#[allow(clippy::result_large_err)]
pub fn client_with_tweaks(
uri: &str,
timeout: Duration,
allow_redirects: bool,
) -> Result<reqwest::Client, Error> {
let mut builder = reqwest::ClientBuilder::new().timeout(timeout);
if !allow_redirects {
builder = builder.redirect(reqwest::redirect::Policy::none());
}
if let Ok(cert_list) = env::var(KRILL_HTTPS_ROOT_CERTS_ENV) {
for path in cert_list.split(':') {
let cert = load_root_cert(path)?;
builder = builder.add_root_certificate(cert);
}
}
if uri.starts_with("https://localhost")
|| uri.starts_with("https://127.0.0.1")
{
builder.danger_accept_invalid_certs(true).build()
} else {
builder.build()
}
.map_err(|e| Error::request_build(uri, e))
}
#[allow(clippy::result_large_err)]
fn headers(
uri: &str,
content_type: Option<&str>,
token: Option<&Token>,
) -> Result<HeaderMap, Error> {
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static("krill"));
if let Some(content_type) = content_type {
headers.insert(
CONTENT_TYPE,
HeaderValue::from_str(content_type)
.map_err(|e| Error::request_build(uri, e))?,
);
}
if let Some(token) = token {
headers.insert(
hyper::header::AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {token}"))
.map_err(|e| Error::request_build(uri, e))?,
);
}
Ok(headers)
}
async fn process_json_response<T: DeserializeOwned>(
uri: &str,
res: Response,
) -> Result<T, Error> {
match process_opt_json_response(uri, res).await? {
None => Err(Error::response(uri, "got empty response body")),
Some(res) => Ok(res),
}
}
async fn process_opt_json_response<T: DeserializeOwned>(
uri: &str,
res: Response,
) -> Result<Option<T>, Error> {
match opt_text_response(uri, res).await? {
None => Ok(None),
Some(s) => {
let res: T = serde_json::from_str(&s).map_err(|e| {
Error::response(
uri,
format!("could not parse JSON response: {e}"),
)
})?;
Ok(Some(res))
}
}
}
async fn empty_response(uri: &str, res: Response) -> Result<(), Error> {
match opt_text_response(uri, res).await? {
None => Ok(()),
Some(_) => Err(Error::response(uri, "expected empty response")),
}
}
async fn text_response(uri: &str, res: Response) -> Result<String, Error> {
match opt_text_response(uri, res).await? {
None => Err(Error::response(uri, "expected response body")),
Some(s) => Ok(s),
}
}
async fn opt_text_response(
uri: &str,
res: Response,
) -> Result<Option<String>, Error> {
match res.status() {
StatusCode::OK => match res.text().await.ok() {
None => Ok(None),
Some(s) => {
if s.is_empty() {
Ok(None)
} else {
Ok(Some(s))
}
}
},
StatusCode::FORBIDDEN => Err(Error::Forbidden(uri.to_string())),
_ => Err(Error::from_res(uri, res).await),
}
}
type ErrorUri = String;
type RootCertPath = String;
type ErrorMessage = String;
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum Error {
RequestBuild(ErrorUri, ErrorMessage),
RequestBuildHttpsCert(RootCertPath, ErrorMessage),
RequestExecute(ErrorUri, ErrorMessage),
Response(ErrorUri, ErrorMessage),
Forbidden(ErrorUri),
ErrorResponse(ErrorUri, StatusCode),
ErrorResponseWithBody(ErrorUri, StatusCode, String),
ErrorResponseWithJson(ErrorUri, StatusCode, Box<ErrorResponse>),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::RequestBuild(uri, msg) => write!(
f,
"Issue creating request for URI: {uri}, error: {msg}"
),
Error::RequestBuildHttpsCert(path, msg) => {
write!(
f,
"Cannot use configured HTTPS root cert '{path}'. Error: {msg}"
)
}
Error::RequestExecute(uri, msg) => {
write!(f, "Issue accessing URI: {uri}, error: {msg}")
}
Error::Response(uri, msg) => write!(
f,
"Issue processing response from URI: {uri}, error: {msg}"
),
Error::Forbidden(uri) => {
write!(f, "Got 'Forbidden' response for URI: {uri}")
}
Error::ErrorResponse(uri, code) => {
write!(
f,
"Issue processing response from URI: {uri}, \
error: unexpected status code {code}"
)
}
Error::ErrorResponseWithBody(uri, code, e) => {
write!(
f,
"Error response from URI: {uri}, Status: {code}, Error: {e}"
)
}
Error::ErrorResponseWithJson(uri, code, res) => write!(
f,
"Error response from URI: {uri}, Status: {code}, ErrorResponse: {res}"
),
}
}
}
impl Error {
pub fn request_build(uri: &str, msg: impl fmt::Display) -> Self {
Error::RequestBuild(uri.to_string(), msg.to_string())
}
pub fn request_build_json(uri: &str, e: impl fmt::Display) -> Self {
Error::RequestBuild(
uri.to_string(),
format!("could not serialize type to JSON: {e}"),
)
}
pub fn request_build_https_cert(
path: &str,
msg: impl fmt::Display,
) -> Self {
Error::RequestBuildHttpsCert(path.to_string(), msg.to_string())
}
pub fn execute(uri: &str, msg: impl fmt::Display) -> Self {
Error::RequestExecute(uri.to_string(), msg.to_string())
}
pub fn response(uri: &str, msg: impl fmt::Display) -> Self {
Error::Response(uri.to_string(), msg.to_string())
}
pub fn forbidden(uri: &str) -> Self {
Error::Forbidden(uri.to_string())
}
pub fn response_unexpected_status(uri: &str, status: StatusCode) -> Self {
Error::ErrorResponse(uri.to_string(), status)
}
pub async fn from_res(uri: &str, res: Response) -> Error {
let status = res.status();
match res.text().await {
Ok(body) => {
if body.is_empty() {
Self::response_unexpected_status(uri, status)
} else {
match serde_json::from_str::<ErrorResponse>(&body) {
Ok(res) => Error::ErrorResponseWithJson(
uri.to_string(),
status,
Box::new(res),
),
Err(_) => Error::ErrorResponseWithBody(
uri.to_string(),
status,
body,
),
}
}
}
_ => Self::response_unexpected_status(uri, status),
}
}
pub fn status_code(&self) -> Option<StatusCode> {
match self {
Self::ErrorResponse(_, code)
| Self::ErrorResponseWithBody(_, code, _)
| Self::ErrorResponseWithJson(_, code, _) =>
{
Some(*code)
}
_ => None
}
}
}