use std::{env, fmt, process};
use std::borrow::Cow;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;
use percent_encoding::{CONTROLS, AsciiSet, utf8_percent_encode};
use reqwest::{Response, StatusCode};
use reqwest::header::{CONTENT_TYPE, USER_AGENT, HeaderMap, HeaderValue};
use rpki::uri;
use rpki::crypto::KeyIdentifier;
use rpki::ca::idexchange;
use rpki::ca::csr::BgpsecCsr;
use rpki::ca::idcert::IdCert;
use rpki::ca::idexchange::{
CaHandle, ChildHandle, ParentHandle, PublisherHandle
};
use rpki::repository::resources::{Asn, ResourceSet};
use rpki::repository::x509::Time;
use serde::de::DeserializeOwned;
use serde::ser::Serialize;
use crate::api;
use crate::api::admin::Token;
use crate::api::pubd::RepoStats;
use crate::api::status::Success;
use crate::api::ta::{
ApiTrustAnchorSignedRequest, TrustAnchorSignedResponse,
TrustAnchorSignerInfo,
};
use crate::commons::file;
use crate::commons::httpclient::Error;
use crate::constants::{
HTTP_CLIENT_TIMEOUT_SECS, KRILL_CLI_API_ENV, KRILL_HTTPS_ROOT_CERTS_ENV,
};
#[derive(Clone, Debug)]
pub struct KrillClient {
base_uri: String,
http_client: reqwest::Client,
report_and_exit: bool,
}
impl KrillClient {
pub fn new(
base_uri: ServerUri, token: Option<Token>
) -> Result<Self, Error> {
let mut builder = reqwest::ClientBuilder::new()
.timeout(Duration::from_secs(HTTP_CLIENT_TIMEOUT_SECS));
if let Ok(cert_list) = env::var(KRILL_HTTPS_ROOT_CERTS_ENV) {
for path in cert_list.split(':') {
let cert = Self::load_root_cert(path)?;
builder = builder.add_root_certificate(cert);
}
}
let base_uri = match base_uri {
ServerUri::Http(uri) => {
if uri.starts_with("https://localhost")
|| uri.starts_with("https://127.0.0.1")
{
builder = builder.danger_accept_invalid_certs(true);
}
uri
}
#[cfg(unix)]
ServerUri::Unix(socket_path) => {
builder = builder.unix_socket(socket_path);
builder = builder.danger_accept_invalid_certs(true);
String::from("http://localhost")
},
};
let mut headers: HeaderMap = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static("krill"));
if let Some(token) = &token {
headers.insert(
hyper::header::AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {token}"))
.map_err(|e| Error::request_build(base_uri.as_str(), e))?
);
}
builder = builder.default_headers(headers);
let http_client = builder.build().map_err(|e| {
Error::request_build(base_uri.as_str(), e)
})?;
Ok(Self {
base_uri,
http_client,
report_and_exit: env::var(KRILL_CLI_API_ENV).is_ok(),
})
}
pub fn base_uri(&self) -> &str {
&self.base_uri
}
pub async fn get_ok<'a>(
&self, path: impl IntoIterator<Item = Cow<'a, str>>
) -> Result<Success, Error> {
let uri = self.create_uri(path);
self.report_get(&uri);
let res = self.http_client.get(&uri).headers(
Self::headers(None)
).send().await.map_err(|e| Error::execute(&uri, e))?;
Self::opt_text_response(&uri, res).await?;
Ok(Success)
}
pub async fn get_json<'a, T: DeserializeOwned>(
&self, path: impl IntoIterator<Item = Cow<'a, str>>
) -> Result<T, Error> {
let uri = self.create_uri(path);
self.report_get(&uri);
let response = self.http_client
.get(&uri).headers(Self::headers(Some(Self::JSON)))
.send().await
.map_err(|e| Error::execute(&uri, e))?;
Self::process_json_response(&uri, response).await
}
pub async fn get_text(
&self, path: impl IntoIterator<Item = Cow<'_, str>>
) -> Result<String, Error> {
let uri = self.create_uri(path);
self.report_get(&uri);
let response = self.http_client
.get(&uri).headers(Self::headers(Some(Self::JSON)))
.send().await
.map_err(|e| Error::execute(&uri, e))?;
Self::text_response(&uri, response).await
}
pub async fn post_empty<'a>(
&self, path: impl IntoIterator<Item = Cow<'a, str>>
) -> Result<Success, Error> {
let uri = self.create_uri(path);
Self::empty_response(
&uri, self.do_empty_post(&uri).await?
).await
}
pub async fn post_empty_with_response<'a, T: DeserializeOwned>(
&self,
path: impl IntoIterator<Item = Cow<'a, str>>,
) -> Result<T, Error> {
let uri = self.create_uri(path);
Self::process_json_response(
&uri, self.do_empty_post(&uri).await?
).await
}
async fn do_empty_post(
&self,
uri: &str,
) -> Result<Response, Error> {
self.report_post(uri, None);
self.http_client.post(uri).headers(
Self::headers(Some(Self::JSON))
).send().await.map_err(|e| Error::execute(uri, e))
}
pub async fn post_json<'a>(
&self,
path: impl IntoIterator<Item = Cow<'a, str>>,
data: impl Serialize,
) -> Result<Success, Error> {
let uri = self.create_uri(path);
Self::empty_response(
&uri, self.do_post(&uri, data).await?
).await
}
pub async fn post_json_with_response<'a, T: DeserializeOwned>(
&self,
path: impl IntoIterator<Item = Cow<'a, str>>,
data: impl Serialize,
) -> Result<T, Error> {
let uri = self.create_uri(path);
Self::process_json_response(
&uri, self.do_post(&uri, data).await?
).await
}
pub async fn post_json_with_opt_response<'a, T: DeserializeOwned>(
&self,
path: impl IntoIterator<Item = Cow<'a, str>>,
data: impl Serialize,
) -> Result<Option<T>, Error> {
let uri = self.create_uri(path);
Self::process_opt_json_response(
&uri, self.do_post(&uri, data).await?
).await
}
async fn do_post(
&self,
uri: &str,
data: impl Serialize,
) -> Result<Response, Error> {
let body = serde_json::to_string_pretty(&data).map_err(|e| {
Error::request_build_json(uri, e)
})?;
self.report_post(uri, Some(&body));
self.http_client.post(uri).headers(
Self::headers(Some(Self::JSON))
).body(body).send().await.map_err(|e| Error::execute(uri, e))
}
pub async fn delete<'a>(
&self, path: impl IntoIterator<Item = Cow<'a, str>>
) -> Result<Success, Error> {
let uri = self.create_uri(path);
self.report_delete(&uri);
let res = self.http_client.delete(&uri).headers(
Self::headers(None)
).send().await.map_err(|e| Error::execute(&uri, e))?;
match res.status() {
StatusCode::OK => Ok(Success),
_ => Err(Error::from_res(&uri, res).await),
}
}
fn create_uri<'a>(
&self, path: impl IntoIterator<Item = Cow<'a, str>>
) -> String {
let mut res = String::from(&self.base_uri);
for item in path {
if !res.ends_with('/') {
res.push('/');
}
res.push_str(&item);
}
res
}
#[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))
}
}
impl KrillClient {
pub async fn authorized(&self) -> Result<Success, Error> {
self.get_ok(once("api/v1/authorized")).await
}
pub async fn info(&self) -> Result<api::admin::ServerInfo, Error> {
self.get_json(once("stats/info")).await
}
pub async fn bulk_issues(
&self
) -> Result<api::ca::AllCertAuthIssues, Error> {
self.get_json(once("api/v1/bulk/cas/issues")).await
}
pub async fn bulk_sync_parents(&self) -> Result<Success, Error> {
self.post_empty(once("api/v1/bulk/cas/sync/parent")).await
}
pub async fn bulk_sync_repo(&self) -> Result<Success, Error> {
self.post_empty(once("api/v1/bulk/cas/sync/repo")).await
}
pub async fn bulk_publish(&self) -> Result<Success, Error> {
self.post_empty(once("api/v1/bulk/cas/publish")).await
}
pub async fn bulk_force_publish(&self) -> Result<Success, Error> {
self.post_empty(once("api/v1/bulk/cas/force_publish")).await
}
pub async fn bulk_suspend(&self) -> Result<Success, Error> {
self.post_empty(once("api/v1/bulk/cas/suspend")).await
}
pub async fn bulk_import(
&self, structure: api::import::Structure
) -> Result<Success, Error> {
self.post_json(once("api/v1/bulk/cas/import"), structure).await
}
pub async fn cas_list(&self) -> Result<api::ca::CertAuthList, Error> {
self.get_json(once("api/v1/cas")).await
}
pub async fn ca_add(
&self, handle: CaHandle
) -> Result<Success, Error> {
self.post_json(
once("api/v1/cas"),
api::admin::CertAuthInit { handle }
).await
}
pub async fn ca_details(
&self, ca: &CaHandle
) -> Result<api::ca::CertAuthInfo, Error> {
self.get_json(ca_path(ca)).await
}
pub async fn ca_delete(
&self, ca: &CaHandle
) -> Result<Success, Error> {
self.delete(ca_path(ca)).await
}
pub async fn ca_issues(
&self, ca: &CaHandle
) -> Result<api::ca::CertAuthIssues, Error> {
self.get_json(ca_path(ca).into_iter().chain(once("issues"))).await
}
pub async fn ca_history_commands(
&self, ca: &CaHandle,
rows: Option<u64>, offset: Option<u64>,
after: Option<Time>, before: Option<Time>
) -> Result<api::history::CommandHistory, Error> {
let path = {
if let Some(before) = before {
let after =
after.map(|t| t.timestamp()).unwrap_or_else(|| 0);
format!(
"{}/{}/{}/{}",
rows.unwrap_or(100),
offset.unwrap_or_default(),
after,
before.timestamp()
)
}
else if let Some(after) = after {
format!(
"{}/{}/{}",
rows.unwrap_or(100),
offset.unwrap_or_default(),
after.timestamp()
)
}
else if let Some(offset) = offset {
format!("{}/{}", rows.unwrap_or(100), offset)
}
else if let Some(rows) = rows {
format!("{rows}")
}
else {
String::new()
}
};
self.get_json(
ca_path(ca).into_iter().chain(
["history/commands".into(), path.into()]
)
).await
}
pub async fn ca_history_details(
&self, ca: &CaHandle, key: &str
) -> Result<api::history::CommandDetails, Error> {
self.get_json(
ca_path(ca).into_iter().chain(
["history/details".into(), encode(key)]
)
).await
}
pub async fn ca_init_keyroll(
&self, ca: &CaHandle,
) -> Result<Success, Error> {
self.post_empty(
ca_path(ca).into_iter().chain(once("keys/roll_init"))
).await
}
pub async fn ca_activate_keyroll(
&self, ca: &CaHandle,
) -> Result<Success, Error> {
self.post_empty(
ca_path(ca).into_iter().chain(once("keys/roll_activate"))
).await
}
pub async fn ca_update_id(
&self, ca: &CaHandle,
) -> Result<Success, Error> {
self.post_empty(
ca_path(ca).into_iter().chain(once("id"))
).await
}
pub async fn ca_sync_parents(
&self, ca: &CaHandle,
) -> Result<Success, Error> {
self.post_empty(
ca_path(ca).into_iter().chain(once("sync/parents"))
).await
}
pub async fn child_connections(
&self, ca: &CaHandle
) -> Result<api::ca::ChildrenConnectionStats, Error> {
self.get_json(
ca_path(ca).into_iter().chain(once("stats/children/connections"))
).await
}
pub async fn child_add(
&self,
ca: &CaHandle, child: ChildHandle,
resources: ResourceSet,
id_cert: IdCert
) -> Result<idexchange::ParentResponse, Error> {
self.post_json_with_response(
ca_path(ca).into_iter().chain(once("children")),
api::admin::AddChildRequest {
handle: child,
resources,
id_cert
}
).await
}
pub async fn child_import(
&self, ca: &CaHandle, import: api::import::ImportChild
) -> Result<Success, Error> {
self.post_json(
ca_path(ca).into_iter().chain(
[
"children".into(),
encode(import.name.clone().as_str()),
"import".into()
]
),
import
).await
}
pub async fn child_details(
&self, ca: &CaHandle, child: &ChildHandle,
) -> Result<api::ca::ChildCaInfo, Error> {
self.get_json(child_path(ca, child)).await
}
pub async fn child_update(
&self,
ca: &CaHandle, child: &ChildHandle,
update: api::admin::UpdateChildRequest
) -> Result<Success, Error> {
self.post_json(child_path(ca, child), update).await
}
pub async fn child_contact(
&self, ca: &CaHandle, child: &ChildHandle,
) -> Result<idexchange::ParentResponse, Error> {
self.get_json(
child_path(ca, child).into_iter().chain(once("contact"))
).await
}
pub async fn child_export(
&self, ca: &CaHandle, child: &ChildHandle,
) -> Result<api::import::ImportChild, Error> {
self.get_json(
child_path(ca, child).into_iter().chain(once("export"))
).await
}
pub async fn child_delete(
&self, ca: &CaHandle, child: &ChildHandle,
) -> Result<Success, Error> {
self.delete(child_path(ca, child)).await
}
pub async fn child_request(
&self, ca: &CaHandle
) -> Result<idexchange::ChildRequest, Error> {
self.get_json(
ca_path(ca).into_iter().chain(once("id/child_request.json"))
).await
}
pub async fn parent_list(
&self, ca: &CaHandle,
) -> Result<api::ca::ParentStatuses, Error> {
self.get_json(
ca_path(ca).into_iter().chain(once("parents")),
).await
}
pub async fn parent_add(
&self, ca: &CaHandle, request: api::admin::ParentCaReq,
) -> Result<Success, Error> {
self.post_json(
ca_path(ca).into_iter().chain(once("parents")),
request,
).await
}
pub async fn parent_refresh(
&self, ca: &CaHandle,
) -> Result<Success, Error> {
self.post_empty(
ca_path(ca).into_iter().chain(once("sync/parents")),
).await
}
pub async fn parent_details(
&self, ca: &CaHandle, parent: &ParentHandle
) -> Result<api::admin::ParentCaContact, Error> {
self.get_json(parent_path(ca, parent)).await
}
pub async fn parent_delete(
&self, ca: &CaHandle, parent: &ParentHandle
) -> Result<Success, Error> {
self.delete(parent_path(ca, parent)).await
}
pub async fn repo_request(
&self, ca: &CaHandle,
) -> Result<idexchange::PublisherRequest, Error> {
self.get_json(
ca_path(ca).into_iter().chain(once("id/publisher_request.json"))
).await
}
pub async fn repo_details(
&self, ca: &CaHandle,
) -> Result<api::ca::CaRepoDetails, Error> {
self.get_json(ca_path(ca).into_iter().chain(once("repo"))).await
}
pub async fn repo_status(
&self, ca: &CaHandle,
) -> Result<api::ca::RepoStatus, Error> {
self.get_json(
ca_path(ca).into_iter().chain(once("repo/status"))
).await
}
pub async fn repo_update(
&self,
ca: &CaHandle,
repository_response: idexchange::RepositoryResponse,
) -> Result<Success, Error> {
self.post_json(
ca_path(ca).into_iter().chain(once("repo")),
api::admin::ApiRepositoryContact { repository_response },
).await
}
pub async fn roas_list(
&self, ca: &CaHandle
) -> Result<api::roa::ConfiguredRoas, Error> {
self.get_json(ca_path(ca).into_iter().chain(once("routes"))).await
}
pub async fn roas_update(
&self, ca: &CaHandle, updates: api::roa::RoaConfigurationUpdates
) -> Result<Success, Error> {
self.post_json(
ca_path(ca).into_iter().chain(once("routes")), updates
).await
}
pub async fn roas_try_update(
&self, ca: &CaHandle, updates: api::roa::RoaConfigurationUpdates
) -> Result<Option<api::bgp::BgpAnalysisAdvice>, Error> {
self.post_json_with_opt_response(
ca_path(ca).into_iter().chain(once("routes/try")), updates
).await
}
pub async fn roas_dryrun_update(
&self, ca: &CaHandle, updates: api::roa::RoaConfigurationUpdates
) -> Result<api::bgp::BgpAnalysisReport, Error> {
self.post_json_with_response(
ca_path(ca).into_iter().chain(
once("routes/analysis/dryrun")
),
updates
).await
}
pub async fn roas_analyze(
&self, ca: &CaHandle
) -> Result<api::bgp::BgpAnalysisReport, Error> {
self.get_json(
ca_path(ca).into_iter().chain(once("routes/analysis/full"))
).await
}
pub async fn roas_suggest(
&self, ca: &CaHandle, resources: Option<ResourceSet>
) -> Result<api::bgp::BgpAnalysisSuggestion, Error> {
match resources {
Some(resources) => {
self.post_json_with_response(
ca_path(ca).into_iter().chain(
once("routes/analysis/suggest")
),
resources,
).await
}
None => {
self.get_json(
ca_path(ca).into_iter().chain(
once("routes/analysis/suggest")
)
).await
}
}
}
pub async fn bgpsec_list(
&self, ca: &CaHandle
) -> Result<api::bgpsec::BgpSecCsrInfoList, Error> {
self.get_json(ca_path(ca).into_iter().chain(once("bgpsec"))).await
}
pub async fn bgpsec_update(
&self, ca: &CaHandle, updates: api::bgpsec::BgpSecDefinitionUpdates
) -> Result<Success, Error> {
self.post_json(
ca_path(ca).into_iter().chain(once("bgpsec")), updates,
).await
}
pub async fn bgpsec_add_single(
&self, ca: &CaHandle, asn: Asn, csr: BgpsecCsr,
) -> Result<Success, Error> {
self.bgpsec_update(
ca,
api::bgpsec::BgpSecDefinitionUpdates {
add: vec![api::bgpsec::BgpSecDefinition { asn, csr } ],
remove: Vec::new(),
}
).await
}
pub async fn bgpsec_delete_single(
&self, ca: &CaHandle, asn: Asn, key: KeyIdentifier,
) -> Result<Success, Error> {
self.bgpsec_update(
ca,
api::bgpsec::BgpSecDefinitionUpdates {
add: Vec::new(),
remove: vec![api::bgpsec::BgpSecAsnKey { asn, key }],
}
).await
}
pub async fn aspas_list(
&self, ca: &CaHandle
) -> Result<api::aspa::AspaDefinitionList, Error> {
self.get_json(ca_path(ca).into_iter().chain(once("aspas"))).await
}
pub async fn aspas_update(
&self, ca: &CaHandle, updates: api::aspa::AspaDefinitionUpdates,
) -> Result<Success, Error> {
self.post_json(
ca_path(ca).into_iter().chain(once("aspas")), updates
).await
}
pub async fn aspas_add_single(
&self, ca: &CaHandle, aspa: api::aspa::AspaDefinition
) -> Result<Success, Error> {
self.aspas_update(
ca,
api::aspa::AspaDefinitionUpdates {
add_or_replace: vec![aspa],
remove: Vec::new(),
}
).await
}
pub async fn aspas_delete_single(
&self, ca: &CaHandle, customer: Asn,
) -> Result<Success, Error> {
self.aspas_update(
ca,
api::aspa::AspaDefinitionUpdates {
add_or_replace: Vec::new(),
remove: vec![customer]
}
).await
}
pub async fn aspas_update_single(
&self,
ca: &CaHandle, customer: Asn,
update: api::aspa::AspaProvidersUpdate
) -> Result<Success, Error> {
self.post_json(
ca_path(ca).into_iter().chain(
[ "aspas".into(), "as".into(), customer.to_string().into()]),
update,
).await
}
pub async fn publishers_list(
&self
) -> Result<api::admin::PublisherList, Error> {
self.get_json(once("api/v1/pubd/publishers")).await
}
pub async fn publishers_stale(
&self, seconds: u64,
) -> Result<api::admin::PublisherList, Error> {
self.get_json(
["api/v1/pubd/stale".into(), seconds.to_string().into()]
).await
}
pub async fn publishers_add(
&self, request: idexchange::PublisherRequest
) -> Result<idexchange::RepositoryResponse, Error> {
self.post_json_with_response(
once("api/v1/pubd/publishers"),
request
).await
}
pub async fn publisher_details(
&self, publisher: &PublisherHandle
) -> Result<api::admin::PublisherDetails, Error> {
self.get_json(publisher_path(publisher)).await
}
pub async fn publisher_response(
&self, publisher: &PublisherHandle
) -> Result<idexchange::RepositoryResponse, Error> {
self.get_json(
publisher_path(publisher).into_iter().chain(
once("response.json")
)
).await
}
pub async fn publisher_delete(
&self, publisher: &PublisherHandle
) -> Result<Success, Error> {
self.delete(publisher_path(publisher)).await
}
pub async fn pubserver_init(
&self, rrdp: uri::Https, rsync: uri::Rsync
) -> Result<Success, Error> {
self.post_json(
once("api/v1/pubd/init"),
api::admin::PublicationServerUris {
rrdp_base_uri: rrdp,
rsync_jail: rsync
},
).await
}
pub async fn pubserver_delete_files(
&self, base_uri: uri::Rsync
) -> Result<Success, Error> {
self.post_json(
once("api/v1/pubd/delete"),
api::admin::RepoFileDeleteCriteria { base_uri }
).await
}
pub async fn pubserver_stats(&self) -> Result<RepoStats, Error> {
self.get_json(once("stats/repo")).await
}
pub async fn pubserver_session_reset(
&self
) -> Result<Success, Error> {
self.post_empty(once("api/v1/pubd/session_reset")).await
}
pub async fn pubserver_clear(
&self
) -> Result<Success, Error> {
self.delete(once("api/v1/pubd/init")).await
}
}
impl KrillClient {
pub async fn testbed_enabled(&self) -> Result<Success, Error> {
self.get_ok(once("testbed/enabled")).await
}
pub async fn testbed_child_add(
&self,
child: ChildHandle,
resources: ResourceSet,
id_cert: IdCert
) -> Result<idexchange::ParentResponse, Error> {
self.post_json_with_response(
once("testbed/children"),
api::admin::AddChildRequest {
handle: child, resources, id_cert
}
).await
}
pub async fn testbed_child_response(
&self, child: &ChildHandle,
) -> Result<String, Error> {
self.get_text(
[
"testbed/children".into(),
encode(child.as_str()),
"parent_response.xml".into()
],
).await
}
pub async fn testbed_child_delete(
&self, ca: &CaHandle
) -> Result<Success, Error> {
self.delete(
["testbed/children".into(), encode(ca.as_str())]
).await.map(|_| Success)
}
pub async fn testbed_publishers_add(
&self, request: idexchange::PublisherRequest
) -> Result<idexchange::RepositoryResponse, Error> {
self.post_json_with_response(
once("testbed/publishers"),
request,
).await
}
pub async fn testbed_publisher_delete(
&self, ca: &CaHandle
) -> Result<Success, Error> {
self.delete(
["testbed/publishers".into(), ca.as_str().into()]
).await.map(|_| Success)
}
pub async fn testbed_tal(&self) -> Result<String, Error> {
self.get_text(
once("ta/ta.tal"),
).await
}
pub async fn testbed_renamed_tal(&self) -> Result<String, Error> {
self.get_text(
once("testbed.tal"),
).await
}
}
impl KrillClient {
pub async fn ta_proxy_init(&self) -> Result<Success, Error> {
self.post_empty(once("api/v1/ta/proxy/init")).await
}
pub async fn ta_proxy_id(&self) -> Result<api::ca::IdCertInfo, Error> {
self.get_json(once("api/v1/ta/proxy/id")).await
}
pub async fn ta_proxy_repo_request(
&self
) -> Result<idexchange::PublisherRequest, Error> {
self.get_json(once("api/v1/ta/proxy/repo/request.json")).await
}
pub async fn ta_proxy_repo_contact(
&self
) -> Result<api::admin::RepositoryContact, Error> {
self.get_json(once("api/v1/ta/proxy/repo")).await
}
pub async fn ta_proxy_repo_configure(
&self, repository_response: idexchange::RepositoryResponse,
) -> Result<Success, Error> {
self.post_json(
once("api/v1/ta/proxy/repo"),
api::admin::ApiRepositoryContact { repository_response },
).await
}
pub async fn ta_proxy_signer_add(
&self, info: TrustAnchorSignerInfo
) -> Result<Success, Error> {
self.post_json(once("api/v1/ta/proxy/signer/add"), info).await
}
pub async fn ta_proxy_signer_update(
&self, info: TrustAnchorSignerInfo
) -> Result<Success, Error> {
self.post_json(once("api/v1/ta/proxy/signer/update"), info).await
}
pub async fn ta_proxy_signer_make_request(
&self
) -> Result<ApiTrustAnchorSignedRequest, Error> {
self.post_empty_with_response(
once("api/v1/ta/proxy/signer/request")
).await
}
pub async fn ta_proxy_signer_show_request(
&self
) -> Result<ApiTrustAnchorSignedRequest, Error> {
self.get_json(once("api/v1/ta/proxy/signer/request")).await
}
pub async fn ta_proxy_signer_response(
&self, response: TrustAnchorSignedResponse
) -> Result<Success, Error> {
self.post_json(
once("api/v1/ta/proxy/signer/response"),
response,
).await
}
pub async fn ta_proxy_children_add(
&self, child: api::admin::AddChildRequest,
) -> Result<idexchange::ParentResponse, Error> {
self.post_json_with_response(
once("api/v1/ta/proxy/children"),
child,
).await
}
pub async fn ta_proxy_child_response(
&self, child: &ChildHandle
) -> Result<idexchange::ParentResponse, Error> {
self.get_json([
"api/v1/ta/proxy/children".into(),
encode(child.as_str()),
"parent_response.json".into(),
]).await
}
}
impl KrillClient {
const JSON: &str = "application/json";
fn report_get(&self, uri: &str) {
if self.report_and_exit {
println!("GET:\n {uri}");
process::exit(0);
}
}
fn report_post(
&self, uri: &str, body: Option<&str>,
) {
if self.report_and_exit {
println!("POST:\n {uri}");
if let Some(body) = body {
println!("Body:\n{body}");
}
std::process::exit(0);
}
}
fn report_delete(&self, uri: &str) {
if self.report_and_exit {
println!("DELETE:\n {uri}");
std::process::exit(0);
}
}
fn headers(content_type: Option<&'static str>) -> HeaderMap {
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_static(content_type)
);
}
headers
}
async fn process_json_response<T: DeserializeOwned>(
uri: &str,
res: Response,
) -> Result<T, Error> {
match Self::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 Self::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<Success, Error> {
match Self::opt_text_response(uri, res).await? {
None => Ok(Success),
Some(_) => Err(Error::response(uri, "expected empty response")),
}
}
async fn text_response(
uri: &str, res: Response
) -> Result<String, Error> {
match Self::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),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ServerUri {
Http(String),
#[cfg(unix)]
Unix(PathBuf),
}
impl TryFrom<String> for ServerUri {
type Error = &'static str;
fn try_from(mut value: String) -> Result<Self, Self::Error> {
if let Some(scheme) = value.as_bytes().get(0..7) {
if scheme.eq_ignore_ascii_case(b"http://") {
return Ok(Self::Http(value))
}
#[cfg(unix)]
if scheme.eq_ignore_ascii_case(b"unix://") {
return Ok(Self::Unix(value.split_off(7).into()))
}
}
if
let Some(scheme) = value.as_bytes().get(0..8)
&& scheme.eq_ignore_ascii_case(b"https://")
{
return Ok(Self::Http(value))
}
Err("unsupported URI scheme")
}
}
impl FromStr for ServerUri {
type Err = &'static str;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::try_from(String::from(value))
}
}
impl fmt::Display for ServerUri {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Http(string) => string.fmt(f),
#[cfg(unix)]
Self::Unix(path) => {
write!(f, "unix://{}", path.display())
}
}
}
}
fn ca_path(ca: &CaHandle) -> impl IntoIterator<Item = Cow<'_, str>> {
["api/v1/cas".into(), encode(ca.as_str())]
}
fn child_path<'s>(
ca: &'s CaHandle, child: &'s ChildHandle
) -> impl IntoIterator<Item = Cow<'s, str>> {
ca_path(ca).into_iter().chain(["children".into(), encode(child.as_str())])
}
fn parent_path<'s>(
ca: &'s CaHandle, parent: &'s ParentHandle
) -> impl IntoIterator<Item = Cow<'s, str>> {
ca_path(ca).into_iter().chain(["parents".into(), encode(parent.as_str())])
}
fn publisher_path(
publisher: &PublisherHandle
) -> impl IntoIterator<Item = Cow<'_, str>> {
["api/v1/pubd/publishers".into(), encode(publisher.as_str())]
}
fn once(s: &str) -> impl Iterator<Item = Cow<'_, str>> {
std::iter::once(s.into())
}
const PATH_ENCODE_SET: &AsciiSet =
&CONTROLS
.add(b' ').add(b'"').add(b'#').add(b'%').add(b'/').add(b'<').add(b'>')
.add(b'?').add(b'[').add(b'\\').add(b']').add(b'^').add(b'`').add(b'{')
.add(b'|').add(b'}').add(b'\x7f');
fn encode(s: &str) -> Cow<'_, str> {
utf8_percent_encode(s, PATH_ENCODE_SET).into()
}