Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "vss-client-ng"
version = "0.5.0"
version = "0.6.0"
authors = ["Leo Nash <hello@leonash.net>", "Elias Rohrer <dev@tnull.de>"]
rust-version = "1.75.0"
license = "MIT OR Apache-2.0"
Expand Down Expand Up @@ -35,7 +35,6 @@ log = { version = "0.4.29", default-features = false, features = ["std"]}

[target.'cfg(genproto)'.build-dependencies]
prost-build = { version = "0.11.3" }
bitreq = { version = "0.3", default-features = false, features = ["std", "https"] }

[dev-dependencies]
mockito = "0.28.0"
Expand Down
18 changes: 1 addition & 17 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
#[cfg(genproto)]
extern crate prost_build;
#[cfg(genproto)]
use std::io::Write;
#[cfg(genproto)]
use std::{env, fs, fs::File, path::Path};
use std::{env, fs, path::Path};

/// To generate updated proto objects:
/// 1. Place `vss.proto` file in `src/proto/`
Expand All @@ -15,21 +13,7 @@ fn main() {

#[cfg(genproto)]
fn generate_protos() {
download_file(
"https://raw.githubusercontent.com/lightningdevkit/vss-server/022ee5e92debb60516438af0a369966495bfe595/proto/vss.proto",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While doing this on the server is fine, here I'd not want to do it until we have a better solution in place. Maybe we should go through with merging repos now that both are rust codebases, or have both depend on a shared vss-proto crate, to ensure version compat?

"src/proto/vss.proto",
).unwrap();

prost_build::compile_protos(&["src/proto/vss.proto"], &["src/"]).unwrap();
let from_path = Path::new(&env::var("OUT_DIR").unwrap()).join("vss.rs");
fs::copy(from_path, "src/types.rs").unwrap();
}

#[cfg(genproto)]
fn download_file(url: &str, save_to: &str) -> Result<(), Box<dyn std::error::Error>> {
let response = bitreq::get(url).send()?;
fs::create_dir_all(Path::new(save_to).parent().unwrap())?;
let mut out_file = File::create(save_to)?;
out_file.write_all(&response.into_bytes())?;
Ok(())
}
12 changes: 12 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const CONTENT_TYPE: &str = "content-type";
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const MAX_RESPONSE_BODY_SIZE: usize = 1024 * 1024 * 1024; // 1GB
const DEFAULT_CLIENT_CAPACITY: usize = 10;
const PROTOCOL_VERSION_HEADER: &str = "vss-protocol-version";
const PROTOCOL_VERSION: &str = "0";

/// Thin-client to access a hosted instance of Versioned Storage Service (VSS).
/// The provided [`VssClient`] API is minimalistic and is congruent to the VSS server-side API.
Expand Down Expand Up @@ -212,6 +214,16 @@ impl<R: RetryPolicy<E = VssError>> VssClient<R> {
}

let response = self.client.send_async(http_request).await?;
// Return early in case of version mismatch, this issue must be solved first.
if response.headers.get(PROTOCOL_VERSION_HEADER).map(String::as_str)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need / want to maintain backwards compatibility, i.e., interpret absence of the versioning field as version 0? Or intentionally not?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we remain backwards compatible, then we could break the upcoming PaginatedKVStoreAPI, and serve keys not in creation order.

That is why I think the VSS client should reject any VSS servers that don't set the version header.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll sleep on it, strict equality may be too restrictive. That being said in the future, we could ship version 2 in vss-server on a different set of endpoints from the existing /getObject etc..., so that a single instances may serve clients on both versions at the same time.

!= Some(PROTOCOL_VERSION)
{
let mut response = response;
return Err(VssError::VSSVersionMismatchError {
version_served: response.headers.remove(PROTOCOL_VERSION_HEADER),
version_expected: String::from(PROTOCOL_VERSION),
});
}

let status_code = response.status_code;
let payload = response.into_bytes();
Expand Down
23 changes: 23 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ pub enum VssError {
/// There is an unknown error, it could be a client-side bug, unrecognized error-code, network error
/// or something else.
InternalError(String),

/// The VSS server and client speak different versions of the VSS protocol
VSSVersionMismatchError {
/// The VSS protocol version served
version_served: Option<String>,
/// The VSS protocol version expected
version_expected: String,
},
}

impl VssError {
Expand Down Expand Up @@ -62,6 +70,21 @@ impl Display for VssError {
VssError::InternalServerError(message) => {
write!(f, "InternalServerError: {}", message)
},
VssError::VSSVersionMismatchError {
version_served: Some(served),
version_expected,
} => {
write!(
f,
"The VSS server and client speak different versions of the \
VSS protocol, the server serves version {}, client expects \
{}",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, do we need to sanitize this string before printing or bubbling up the error, or are we expecting this to happen at a higher level anyways?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm thank you yes, I'm thinking we keep validation here minimal and just report the mismatch, together with the different header values ? So as to keep Client as lean as possible.

served, version_expected,
)
},
VssError::VSSVersionMismatchError { version_served: None, version_expected: _ } => {
write!(f, "The server did not set the `vss-protocol-version` header")
},
VssError::InternalError(message) => {
write!(f, "InternalError: {}", message)
},
Expand Down
4 changes: 3 additions & 1 deletion src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,14 +212,16 @@ pub struct ListKeyVersionsRequest {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ListKeyVersionsResponse {
/// Fetched keys and versions.
/// Fetched keys and versions, ordered by creation time (newest first).
/// Even though this API reuses the `KeyValue` struct, the `value` sub-field will not be set by the server.
#[prost(message, repeated, tag = "1")]
pub key_versions: ::prost::alloc::vec::Vec<KeyValue>,
/// `next_page_token` is a pagination token, used to retrieve the next page of results.
/// Use this value to query for next-page of paginated `ListKeyVersions` operation, by specifying
/// this value as the `page_token` in the next request.
///
/// Following AIP-158 (<https://google.aip.dev/158>):
///
/// If `next_page_token` is empty (""), then the "last page" of results has been processed and
/// there is no more data to be retrieved.
///
Expand Down
52 changes: 43 additions & 9 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ mod tests {

const APPLICATION_OCTET_STREAM: &str = "application/octet-stream";
const CONTENT_TYPE: &str = "content-type";
const PROTOCOL_VERSION_HEADER: &str = "vss-protocol-version";
const PROTOCOL_VERSION: &str = "0";

const GET_OBJECT_ENDPOINT: &'static str = "/getObject";
const PUT_OBJECT_ENDPOINT: &'static str = "/putObjects";
Expand All @@ -44,6 +46,7 @@ mod tests {
.match_header(CONTENT_TYPE, APPLICATION_OCTET_STREAM)
.match_body(get_request.encode_to_vec())
.with_status(200)
.with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION)
.with_body(mock_response.encode_to_vec())
.create();

Expand Down Expand Up @@ -77,6 +80,7 @@ mod tests {
.match_header("headerkey", "headervalue")
.match_body(get_request.encode_to_vec())
.with_status(200)
.with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION)
.with_body(mock_response.encode_to_vec())
.create();

Expand Down Expand Up @@ -119,6 +123,7 @@ mod tests {
.match_header(CONTENT_TYPE, APPLICATION_OCTET_STREAM)
.match_body(request.encode_to_vec())
.with_status(200)
.with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION)
.with_body(mock_response.encode_to_vec())
.create();

Expand Down Expand Up @@ -154,6 +159,7 @@ mod tests {
.match_header(CONTENT_TYPE, APPLICATION_OCTET_STREAM)
.match_body(request.encode_to_vec())
.with_status(200)
.with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION)
.with_body(mock_response.encode_to_vec())
.create();

Expand Down Expand Up @@ -195,6 +201,7 @@ mod tests {
.match_header(CONTENT_TYPE, APPLICATION_OCTET_STREAM)
.match_body(request.encode_to_vec())
.with_status(200)
.with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION)
.with_body(mock_response.encode_to_vec())
.create();

Expand Down Expand Up @@ -222,6 +229,7 @@ mod tests {
};
let mock_server = mockito::mock("POST", GET_OBJECT_ENDPOINT)
.with_status(404)
.with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION)
.with_body(&error_response.encode_to_vec())
.create();

Expand All @@ -246,6 +254,7 @@ mod tests {
let mock_response = GetObjectResponse { value: None, ..Default::default() };
let mock_server = mockito::mock("POST", GET_OBJECT_ENDPOINT)
.with_status(200)
.with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION)
.with_body(&mock_response.encode_to_vec())
.create();

Expand All @@ -270,6 +279,7 @@ mod tests {
};
let mock_server = mockito::mock("POST", Matcher::Any)
.with_status(400)
.with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION)
.with_body(&error_response.encode_to_vec())
.create();

Expand Down Expand Up @@ -330,6 +340,7 @@ mod tests {
};
let mock_server = mockito::mock("POST", Matcher::Any)
.with_status(401)
.with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION)
.with_body(&error_response.encode_to_vec())
.create();

Expand Down Expand Up @@ -412,6 +423,7 @@ mod tests {
};
let mock_server = mockito::mock("POST", Matcher::Any)
.with_status(409)
.with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION)
.with_body(&error_response.encode_to_vec())
.create();

Expand Down Expand Up @@ -445,6 +457,7 @@ mod tests {
};
let mock_server = mockito::mock("POST", Matcher::Any)
.with_status(500)
.with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION)
.with_body(&error_response.encode_to_vec())
.create();

Expand Down Expand Up @@ -502,6 +515,7 @@ mod tests {
ErrorResponse { error_code: 999, message: "UnknownException".to_string() };
let mut _mock_server = mockito::mock("POST", Matcher::Any)
.with_status(999)
.with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION)
.with_body(&error_response.encode_to_vec())
.create();

Expand Down Expand Up @@ -534,6 +548,7 @@ mod tests {
let malformed_error_response = b"malformed";
_mock_server = mockito::mock("POST", Matcher::Any)
.with_status(409)
.with_header(PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION)
.with_body(&malformed_error_response)
.create();

Expand All @@ -546,17 +561,36 @@ mod tests {
let list_malformed_err_response = vss_client.list_key_versions(&list_request).await;
assert!(matches!(list_malformed_err_response.unwrap_err(), VssError::InternalError { .. }));

// Requests to endpoints are no longer mocked and will result in network error.
// Requests to endpoints are no longer mocked and will result in version mismatch
// errors.
drop(_mock_server);

let get_network_err = vss_client.get_object(&get_request).await;
assert!(matches!(get_network_err.unwrap_err(), VssError::InternalError { .. }));

let put_network_err = vss_client.put_object(&put_request).await;
assert!(matches!(put_network_err.unwrap_err(), VssError::InternalError { .. }));

let list_network_err = vss_client.list_key_versions(&list_request).await;
assert!(matches!(list_network_err.unwrap_err(), VssError::InternalError { .. }));
let get_version_err = vss_client.get_object(&get_request).await;
assert!(matches!(
get_version_err.unwrap_err(),
VssError::VSSVersionMismatchError {
version_served: None,
version_expected: version
} if version == PROTOCOL_VERSION
));

let put_version_err = vss_client.put_object(&put_request).await;
assert!(matches!(
put_version_err.unwrap_err(),
VssError::VSSVersionMismatchError {
version_served: None,
version_expected: version
} if version == PROTOCOL_VERSION
));

let list_version_err = vss_client.list_key_versions(&list_request).await;
assert!(matches!(
list_version_err.unwrap_err(),
VssError::VSSVersionMismatchError {
version_served: None,
version_expected: version
} if version == PROTOCOL_VERSION
));
}

fn retry_policy() -> impl RetryPolicy<E = VssError> {
Expand Down
Loading