Skip to content

Commit

Permalink
Adds client code for fetching and adding host keys
Browse files Browse the repository at this point in the history
  • Loading branch information
thomastaylor312 committed Jul 7, 2022
1 parent de685f1 commit 1b0af6a
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 12 deletions.
47 changes: 47 additions & 0 deletions bin/client/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,40 @@ async fn run() -> std::result::Result<(), ClientError> {
.map_err(|e| ClientError::Other(e.to_string()))?;
println!("Wrote key to keyring file at {}", keyring_path.display())
}
Keys::Fetch(opts) => {
let new_keys = match opts.key_server {
Some(url) if !opts.use_host => {
println!("Fetching host keys from {}", url);
get_host_keys(url).await?
}
_ => {
println!("Fetching host keys from bindle server");
bindle_client.get_host_keys().await?
}
};

let mut keyring = keyring_path
.load()
.await
.unwrap_or_else(|_| KeyRing::default());
let orig_len = keyring.key.len();
// Have to filter before extending so we finish with the borrow of the current keyring
let filtered_keys: Vec<KeyEntry> = new_keys
.key
.into_iter()
.filter(|k| !keyring.key.iter().any(|current| current.key == k.key))
.collect();
keyring.key.extend(filtered_keys);
keyring_path
.save(&keyring)
.await
.map_err(|e| ClientError::Other(e.to_string()))?;
println!(
"Wrote {} keys to keyring file at {}",
keyring.key.len() - orig_len,
keyring_path.display()
)
}
}
}
SubCommand::Clean(_clean_opts) => {
Expand Down Expand Up @@ -711,3 +745,16 @@ fn parse_roles(roles: String) -> Result<Vec<SignatureRole>> {
})
.collect()
}

async fn get_host_keys(url: url::Url) -> Result<KeyRing> {
let resp = reqwest::get(url).await?;
if resp.status() != reqwest::StatusCode::OK {
return Err(ClientError::Other(format!(
"Unable to fetch host keys. Got status code {} with body content:\n{}",
resp.status(),
String::from_utf8_lossy(&resp.bytes().await?)
)));
}

toml::from_slice(&resp.bytes().await?).map_err(ClientError::from)
}
22 changes: 21 additions & 1 deletion bin/client/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ pub enum Keys {
about = "Print the public key entries for keys from the secret key file. If no '--label' is supplied, public keys for all secret keys are returned."
)]
Print(PrintKey),
// TODO(thomastaylor312): We should probably add an endpoint to bindle servers that allow you to download a host key and add a subcommand to help with it
#[clap(name = "fetch", about = "Fetch keys from a /bindle-keys endpoint")]
Fetch(FetchKeys),
}

#[derive(Parser)]
Expand Down Expand Up @@ -426,6 +427,25 @@ pub struct PrintKey {
pub label_matching: Option<String>,
}

#[derive(Parser)]
pub struct FetchKeys {
#[clap(
long = "key-server",
value_name = "KEY_SERVER",
env = "BINDLE_KEY_SERVER",
conflicts_with = "use-host",
help = "Sets the server address and path to use for fetching keys. Should be a full url path (e.g. https://my.server.com/api/v1/bindle-keys). If this is not set, --host is implied"
)]
pub key_server: Option<url::Url>,
#[clap(
long = "host",
value_name = "USE_HOST",
id = "use-host",
help = "Fetches keys from the bindle server set by BINDLE_URL. This is mutually exclusive with --key-server"
)]
pub use_host: bool,
}

#[derive(Parser)]
pub struct SignInvoice {
#[clap(
Expand Down
40 changes: 29 additions & 11 deletions src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub const INVOICE_ENDPOINT: &str = "_i";
pub const QUERY_ENDPOINT: &str = "_q";
pub const RELATIONSHIP_ENDPOINT: &str = "_r";
pub const LOGIN_ENDPOINT: &str = "login";
pub const BINDLE_KEYS_ENDPOINT: &str = "bindle-keys";
const TOML_MIME_TYPE: &str = "application/toml";

/// A client type for interacting with a Bindle server
Expand All @@ -43,16 +44,6 @@ pub struct Client<T> {
keyring: Arc<KeyRing>,
}

#[derive(Debug)]
/// The operation being performed against a Bindle server.
enum Operation {
Create,
Yank,
Get,
Query,
Login,
}

/// A builder for for setting up a `Client`. Created using `Client::builder`
pub struct ClientBuilder {
http2_prior_knowledge: bool,
Expand Down Expand Up @@ -175,7 +166,7 @@ impl<T: tokens::TokenManager> Client<T> {
method: reqwest::Method,
path: &str,
body: Option<impl Into<reqwest::Body>>,
) -> anyhow::Result<reqwest::Response> {
) -> Result<reqwest::Response> {
let req = self.client.request(method, self.base_url.join(path)?);
let req = self.token_manager.apply_auth_header(req).await?;
let req = match body {
Expand Down Expand Up @@ -514,6 +505,18 @@ impl<T: tokens::TokenManager> Client<T> {
let resp = unwrap_status(resp, Endpoint::Invoice, Operation::Get).await?;
Ok(toml::from_slice::<crate::MissingParcelsResponse>(&resp.bytes().await?)?.missing)
}

//////////////// Bindle Keys Endpoints ////////////////

/// Fetches all the host public keys specified for the bindle server
#[instrument(level = "trace", skip(self))]
pub async fn get_host_keys(&self) -> Result<KeyRing> {
let resp = self
.raw(reqwest::Method::GET, BINDLE_KEYS_ENDPOINT, None::<&str>)
.await?;
let resp = unwrap_status(resp, Endpoint::BindleKeys, Operation::Get).await?;
Ok(toml::from_slice::<KeyRing>(&resp.bytes().await?)?)
}
}

// We implement provider for client because often times (such as in the CLI) we are composing the
Expand Down Expand Up @@ -622,13 +625,24 @@ impl<T: tokens::TokenManager + Send + Sync + 'static> Provider for Client<T> {

// A helper function and related enum to make some reusable code for unwrapping a status code and returning the right error

#[derive(Debug)]
/// The operation being performed against a Bindle server.
enum Operation {
Create,
Yank,
Get,
Query,
Login,
}

enum Endpoint {
Invoice,
Parcel,
Query,
// NOTE: This endpoint currently does nothing, but if we need more specific errors, we can use
// this down the line
Login,
BindleKeys,
}

async fn unwrap_status(
Expand All @@ -653,6 +667,10 @@ async fn unwrap_status(
(StatusCode::CONFLICT, Endpoint::Invoice) => Err(ClientError::InvoiceAlreadyExists),
(StatusCode::CONFLICT, Endpoint::Parcel) => Err(ClientError::ParcelAlreadyExists),
(StatusCode::UNAUTHORIZED, _) => Err(ClientError::Unauthorized),
(StatusCode::BAD_REQUEST, Endpoint::BindleKeys) => Err(ClientError::InvalidRequest {
status_code: resp.status(),
message: parse_error_from_body(resp).await,
}),
(StatusCode::BAD_REQUEST, _) => Err(ClientError::ServerError(Some(format!(
"Bad request: {}",
parse_error_from_body(resp).await.unwrap_or_default()
Expand Down
82 changes: 82 additions & 0 deletions tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,88 @@ async fn test_keyring_add_to_existing() {
);
}

#[tokio::test]
async fn test_fetch_host_keys() {
// Tempdir for keyring
let tempdir = tempfile::tempdir().expect("Unable to create tempdir");
let keyring_file = tempdir.path().join(KEYRING_FILE);

let controller = TestController::new(BINARY_NAME).await;
let output = std::process::Command::new("cargo")
.args(&[
"run",
"--features",
"cli",
"--bin",
"bindle",
"--",
"keys",
"fetch",
])
.env(ENV_BINDLE_URL, &controller.base_url)
.env(ENV_BINDLE_KEYRING, &keyring_file)
.output()
.expect("Should be able to run command");
assert!(
output.status.success(),
"Should be able to fetch host keys Stdout: {}\n Stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);

let keyring = keyring_file
.load()
.await
.expect("Should be able to load keyring file");

assert!(
!keyring.key.is_empty(),
"Should have at least 1 entry in keyring"
);
}

#[tokio::test]
async fn test_fetch_host_keys_from_specific_host() {
// Tempdir for keyring
let tempdir = tempfile::tempdir().expect("Unable to create tempdir");
let keyring_file = tempdir.path().join(KEYRING_FILE);

let controller = TestController::new(BINARY_NAME).await;
let output = std::process::Command::new("cargo")
.args(&[
"run",
"--features",
"cli",
"--bin",
"bindle",
"--",
"keys",
"fetch",
"--key-server",
&format!("{}bindle-keys", controller.base_url),
])
.env(ENV_BINDLE_URL, "http://not-real.com")
.env(ENV_BINDLE_KEYRING, &keyring_file)
.output()
.expect("Should be able to run command");
assert!(
output.status.success(),
"Should be able to fetch host keys Stdout: {}\n Stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);

let keyring = keyring_file
.load()
.await
.expect("Should be able to load keyring file");

assert!(
!keyring.key.is_empty(),
"Should have at least 1 entry in keyring"
);
}

fn create_key(
keyring_file: impl AsRef<OsStr>,
secrets_file: &Path,
Expand Down
15 changes: 15 additions & 0 deletions tests/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,18 @@ async fn test_charset() {
.await
.expect("Content-Type with charset shouldn't fail");
}

#[tokio::test]
async fn test_bindle_keys() {
let controller = TestController::new(BINARY_NAME).await;

let keyring = controller
.client
.get_host_keys()
.await
.expect("Should be able to fetch host keys using client");
assert!(
!keyring.key.is_empty(),
"Keyring should contain at least one key"
);
}

0 comments on commit 1b0af6a

Please sign in to comment.