Skip to content
Merged
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
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Names of products and specifications, which read as prose and take no
# backticks in a doc comment.
doc-valid-idents = ["UpdateHub", "OpenAPI", ".."]
25 changes: 24 additions & 1 deletion updatehub-cloud-sdk/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,20 @@ impl serde::ser::Serialize for MetadataValue<'_> {
}

impl UpdatePackage {
/// Parses the raw metadata of an update package.
///
/// # Errors
///
/// Returns an error when `content` is not the JSON document the agent
/// expects.
pub fn parse(content: &[u8]) -> crate::Result<Self> {
let update_package = serde_json::from_slice(content)?;
Ok(UpdatePackage { inner: update_package, raw: content.to_vec() })
}

/// Returns the SHA-256 sum of the raw metadata, which identifies the
/// package.
#[must_use]
pub fn package_uid(&self) -> String {
openssl::sha::sha256(&self.raw).iter().fold(String::new(), |mut output, c| {
let _ = write!(output, "{c:02x}");
Expand All @@ -66,16 +75,30 @@ impl UpdatePackage {
})
}

/// Returns the version the package declares.
#[must_use]
pub fn version(&self) -> &str {
&self.inner.version
}
}

impl Signature {
/// Decodes a signature from its base64 form.
///
/// # Errors
///
/// Returns an error when `bytes` is not valid base64.
pub fn from_base64_str(bytes: &str) -> crate::Result<Self> {
Ok(Signature(openssl::base64::decode_block(bytes)?.to_vec()))
Ok(Signature(openssl::base64::decode_block(bytes)?))
}

/// Checks the signature of `package` against the public key stored at
/// `key`.
///
/// # Errors
///
/// Returns an error when the key does not load, when the check itself
/// fails, or when the signature does not match the package.
pub fn validate(&self, key: &Path, package: &UpdatePackage) -> crate::Result<()> {
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa, sign::Verifier};
let key = PKey::from_rsa(Rsa::public_key_from_pem(&fs::read(key)?)?)?;
Expand Down
86 changes: 62 additions & 24 deletions updatehub-cloud-sdk/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ pub struct Client<'a> {
server: &'a str,
}

/// Downloads the content of `url` into `handle`.
///
/// # Errors
///
/// Returns an error when `url` does not parse, when the request fails, when the
/// server answers with a status other than success, or when the write to
/// `handle` fails.
pub async fn get<W>(url: &str, handle: &mut W) -> Result<()>
where
W: io::AsyncWrite + Unpin,
Expand All @@ -35,21 +42,22 @@ where
return Err(Error::InvalidStatusResponse(resp.status()));
}

let mut written: f32 = 0.;
let mut written: u64 = 0;
let mut threshold = 10;
let length = match resp.headers().get(header::CONTENT_LENGTH) {
Some(v) => usize::from_str(v.to_str()?)?,
Some(v) => u64::from_str(v.to_str()?)?,
None => 0,
};

while let Some(chunk) = resp.chunk().await? {
let read = chunk.len();
let read = chunk.len() as u64;
handle.write_all(&chunk).await?;
if length > 0 {
written += read as f32 / (length as f32 / 100.);
if written as usize >= threshold {
written += read;
let percent = written * 100 / length;
if percent >= threshold {
threshold += 20;
debug!("{}% of the file has been downloaded", std::cmp::min(written as usize, 100));
debug!("{}% of the file has been downloaded", std::cmp::min(percent, 100));
}
}
}
Expand All @@ -59,6 +67,13 @@ where
}

impl<'a> Client<'a> {
/// Constructs a client that talks to the server at `server`.
///
/// # Panics
///
/// Panics when the platform gives no TLS backend to build the HTTP client
/// with.
#[must_use]
pub fn new(server: &'a str) -> Self {
let mut headers = header::HeaderMap::new();
headers.insert(header::USER_AGENT, header::HeaderValue::from_static("updatehub/2.0 Linux"));
Expand All @@ -74,9 +89,16 @@ impl<'a> Client<'a> {
.build()
.unwrap();

Self { server, client }
Self { client, server }
}

/// Asks the server whether an update is available for this device.
///
/// # Errors
///
/// Returns an error when the server address does not parse, when the
/// request fails, when the server answers with an unexpected status, or
/// when the update metadata does not parse.
pub async fn probe(
&self,
num_retries: usize,
Expand All @@ -95,30 +117,40 @@ impl<'a> Client<'a> {
match response.status() {
StatusCode::NOT_FOUND => Ok(api::ProbeResponse::NoUpdate),
StatusCode::OK => {
match response
let extra_poll = response
.headers()
.get("add-extra-poll")
.and_then(|extra_poll| extra_poll.to_str().ok())
.and_then(|extra_poll| extra_poll.parse().ok())
{
Some(extra_poll) => Ok(api::ProbeResponse::ExtraPoll(extra_poll)),
None => {
let signature = response
.headers()
.get("UH-Signature")
.map(TryInto::try_into)
.transpose()?;
Ok(api::ProbeResponse::Update(
api::UpdatePackage::parse(&response.bytes().await?)?,
signature,
))
}
.and_then(|extra_poll| extra_poll.parse().ok());

if let Some(extra_poll) = extra_poll {
Ok(api::ProbeResponse::ExtraPoll(extra_poll))
} else {
let signature = response
.headers()
.get("UH-Signature")
.map(TryInto::try_into)
.transpose()?;
Ok(api::ProbeResponse::Update(
api::UpdatePackage::parse(&response.bytes().await?)?,
signature,
))
}
}
s => Err(Error::InvalidStatusResponse(s)),
}
}

/// Downloads one object of an update package into `download_dir`.
///
/// Downloads that stopped part way continue from the number of bytes
/// already on disk.
///
/// # Errors
///
/// Returns an error when the server address does not parse, when the
/// request fails, when the server answers with a status other than
/// success, or when the write to disk fails.
pub async fn download_object(
&self,
product_uid: &str,
Expand Down Expand Up @@ -152,6 +184,12 @@ impl<'a> Client<'a> {
save_body_to(request.send().await?, &mut file).await
}

/// Reports the current state of the device to the server.
///
/// # Errors
///
/// Returns an error when the server address does not parse or when the
/// request fails.
pub async fn report(
&self,
state: &str,
Expand All @@ -161,8 +199,6 @@ impl<'a> Client<'a> {
error_message: Option<String>,
current_log: Option<String>,
) -> Result<()> {
validate_url(self.server)?;

#[derive(serde::Serialize)]
#[serde(rename_all = "kebab-case")]
struct Payload<'a> {
Expand All @@ -179,6 +215,8 @@ impl<'a> Client<'a> {
current_log: Option<String>,
}

validate_url(self.server)?;

let payload =
Payload { state, firmware, package_uid, previous_state, error_message, current_log };

Expand Down
2 changes: 1 addition & 1 deletion updatehub-package-schema/src/definitions/chunk_size.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ mod test {
serde_json::from_value::<Payload>(json!({ "chunk_size": 313 })).ok(),
Some(Payload { chunk_size: ChunkSize(313) })
);
assert!(serde_json::from_value::<Payload>(json!({ "chunk_size": 0 })).is_err())
assert!(serde_json::from_value::<Payload>(json!({ "chunk_size": 0 })).is_err());
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,6 @@ mod test {
"pattern": "linux-kernel"
}))
.unwrap()
)
);
}
}
2 changes: 2 additions & 0 deletions updatehub-sdk/src/api/info/firmware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@ impl MetadataValue {
self.0.keys()
}

#[must_use]
pub fn is_empty(&self) -> bool {
self.0.len() == 0
}

#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
Expand Down
1 change: 1 addition & 0 deletions updatehub-sdk/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ impl Default for Client {

impl Client {
/// Constructs a new `Client`.
#[must_use]
pub fn new(server_address: &str) -> Self {
Client { server_address: format!("http://{server_address}"), ..Self::default() }
}
Expand Down
6 changes: 3 additions & 3 deletions updatehub-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
//! When running an agent instance, the API provides some methods
//! for communicating with UpdateHub:
//!
//! - [abort_download](Client::abort_download)
//! - [`abort_download`](Client::abort_download)
//! - [info](Client::info)
//! - [local_install](Client::local_install)
//! - [`local_install`](Client::local_install)
//! - [log](Client::log)
//! - [probe](Client::probe)
//! - [remote_install](Client::remote_install)
//! - [`remote_install`](Client::remote_install)

pub mod api;
mod client;
Expand Down
18 changes: 17 additions & 1 deletion updatehub-sdk/src/listener.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,21 @@ pub struct Handler {

impl Handler {
/// Cancels the current action on the agent.
///
/// # Errors
///
/// Returns an error when the write to the agent socket fails.
pub async fn cancel(&mut self) -> Result<()> {
self.stream.lock().await.write_all(b"cancel").await.map_err(Error::Io)
}

/// Tell the agent to proceed with the transition.
///
/// # Errors
///
/// Never returns an error. The agent proceeds when it receives no message
/// at all, so this function only keeps the shape of the other handler
/// commands.
pub async fn proceed(&self) -> Result<()> {
// No message need to be sent to the connection in order to the
// agent to proceed handling the current state.
Expand All @@ -80,6 +90,7 @@ impl Handler {
impl StateChange {
/// Creates a new `StateChange` struct.
#[inline]
#[must_use]
pub fn new() -> Self {
StateChange::default()
}
Expand All @@ -103,10 +114,15 @@ impl StateChange {
F: Fn(Handler) -> Fut + 'static,
Fut: Future<Output = Result<()>> + 'static,
{
self.callbacks.entry(state).or_default().push(Box::new(move |d| Box::pin(f(d))))
self.callbacks.entry(state).or_default().push(Box::new(move |d| Box::pin(f(d))));
}

/// Start the agent to listen for messages on the socket.
///
/// # Errors
///
/// Returns an error when the socket cannot be created or read, or when a
/// registered callback returns an error.
pub async fn listen(&self) -> Result<()> {
let sdk_trigger = Path::new(SDK_TRIGGER_FILENAME);
if !sdk_trigger.exists() {
Expand Down
15 changes: 5 additions & 10 deletions updatehub-sdk/tests/openapi_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ async fn probe_default() {
let client = sdk::Client::new(&addr);
let response = client.probe(None).await;
match dbg!(response) {
Ok(_) => {}
Err(sdk::Error::AgentIsBusy(_)) => {}
Ok(_) | Err(sdk::Error::AgentIsBusy(_)) => {}
Err(e) => panic!("Unexpected Error response: {e}"),
}
}
Expand All @@ -55,8 +54,7 @@ async fn probe_custom() {
let client = sdk::Client::new(&addr);
let response = client.probe(Some(String::from("http://foo.bar"))).await;
match dbg!(response) {
Ok(_) => {}
Err(sdk::Error::AgentIsBusy(_)) => {}
Ok(_) | Err(sdk::Error::AgentIsBusy(_)) => {}
Err(e) => panic!("Unexpected Error response: {e}"),
}
}
Expand All @@ -69,8 +67,7 @@ async fn local_install() {
let response = client.local_install(file.path()).await;

match dbg!(response) {
Ok(_) => {}
Err(sdk::Error::AgentIsBusy(_)) => {}
Ok(_) | Err(sdk::Error::AgentIsBusy(_)) => {}
Err(e) => panic!("Unexpected Error response: {e}"),
}
}
Expand All @@ -81,8 +78,7 @@ async fn remote_install() {
let client = sdk::Client::new(&addr);
let response = client.remote_install("http://foo.bar").await;
match dbg!(response) {
Ok(_) => {}
Err(sdk::Error::AgentIsBusy(_)) => {}
Ok(_) | Err(sdk::Error::AgentIsBusy(_)) => {}
Err(e) => panic!("Unexpected Error response: {e}"),
}
}
Expand All @@ -93,8 +89,7 @@ async fn abort_download() {
let client = sdk::Client::new(&addr);
let response = client.abort_download().await;
match dbg!(response) {
Ok(_) => {}
Err(sdk::Error::AbortDownloadRefused(_)) => {}
Ok(_) | Err(sdk::Error::AbortDownloadRefused(_)) => {}
Err(e) => panic!("Unexpected Error response: {e}"),
}
}
Expand Down
1 change: 0 additions & 1 deletion updatehub/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ derive_more = { version = "2", default-features = false, features = [
easy_process = "0.2"
find-binary-version = "0.5"
futures-util = { version = "0.3", default-features = false }
lazy_static = "1"
logging_content = "0.1"
mockito = { version = "1", optional = true }
ms-converter = "1"
Expand Down
1 change: 1 addition & 0 deletions updatehub/src/build_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
///
/// println!("Running version: {}", updatehub::version());
/// ```
#[must_use]
pub fn version() -> &'static str {
env!("VERSION")
}
Loading