initial commit
This commit is contained in:
commit
bc6eb2047b
35 changed files with 6921 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
private.rs
|
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"languageToolLinter.languageTool.ignoredWordsInWorkspace": [
|
||||||
|
"libpaket"
|
||||||
|
]
|
||||||
|
}
|
3752
Cargo.lock
generated
Normal file
3752
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
members = [
|
||||||
|
"libpaket",
|
||||||
|
"advices",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
authors = ["Jane Rachinger <libpaket@j4ne.de>"]
|
||||||
|
edition = "2021"
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
version = "0.1.0"
|
76
libpaket/Cargo.toml
Normal file
76
libpaket/Cargo.toml
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
[package]
|
||||||
|
name = "libpaket"
|
||||||
|
authors.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
aes-gcm = { version = "0.10.3", optional = true }
|
||||||
|
ed25519-dalek = { version = "2.1.0", optional = true }
|
||||||
|
hmac = { version = "0.12.1", optional = true }
|
||||||
|
num_enum = { version = "0.7", optional = true }
|
||||||
|
|
||||||
|
# TODO: Consolidate?
|
||||||
|
rand = "0.8.5"
|
||||||
|
random-string = "1.1.0"
|
||||||
|
reqwest = { version = "0.12", features = ["json", "cookies", "gzip", "http2"] }
|
||||||
|
secrecy = { version = "0.8.0", features = ["serde"] }
|
||||||
|
serde = { version = "1.0.195", features = ["derive"] }
|
||||||
|
serde_json = "1.0.111"
|
||||||
|
serde_repr = { version = "0.1.18", optional = true }
|
||||||
|
url = "2.5.0"
|
||||||
|
base64 = "0.22"
|
||||||
|
|
||||||
|
# TODO: consider splitting login.rs refresh_token and authorization_token
|
||||||
|
# (sha2 and urlencoding only used with authorization_token)
|
||||||
|
# sha2 also used in briefankuendigung and packstation_register_regtoken
|
||||||
|
sha2 = "0.10.8"
|
||||||
|
urlencoding = "2.1.3"
|
||||||
|
|
||||||
|
uuid = { version = "1.7.0", features = ["v4", "serde"], optional = true }
|
||||||
|
serde_newtype = "0.1.1"
|
||||||
|
thiserror = "1.0.56"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = [
|
||||||
|
"advices",
|
||||||
|
"locker_all"
|
||||||
|
]
|
||||||
|
|
||||||
|
advices = [
|
||||||
|
#"dep:sha2",
|
||||||
|
"dep:uuid",
|
||||||
|
"dep:aes-gcm",
|
||||||
|
]
|
||||||
|
|
||||||
|
locker_all = [
|
||||||
|
"locker_register_all",
|
||||||
|
"locker_ble",
|
||||||
|
]
|
||||||
|
|
||||||
|
locker_base = [
|
||||||
|
"dep:uuid",
|
||||||
|
"dep:serde_repr",
|
||||||
|
"dep:ed25519-dalek",
|
||||||
|
]
|
||||||
|
|
||||||
|
locker_ble = [
|
||||||
|
"locker_base",
|
||||||
|
"dep:num_enum",
|
||||||
|
]
|
||||||
|
|
||||||
|
locker_register_all = [
|
||||||
|
"locker_register_regtoken",
|
||||||
|
]
|
||||||
|
|
||||||
|
locker_register_base = [
|
||||||
|
"locker_base",
|
||||||
|
"dep:hmac",
|
||||||
|
#"dep:sha2",
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
locker_register_regtoken = [
|
||||||
|
"locker_register_base"
|
||||||
|
]
|
31
libpaket/README.md
Normal file
31
libpaket/README.md
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# libpaket
|
||||||
|
|
||||||
|
This is an unofficial client to various DHL APIs, more specific the ones that the proprietary app `Post & DHL` uses.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Mail Notification (Briefankündigung)
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- app-driven parcel lockers (App-gesteuerte Packstation)
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
In the examples error-handling is ignored for simplicity. You don’t want to do that.
|
||||||
|
|
||||||
|
### Getting mail notifications (Briefankündigung)
|
||||||
|
```rust
|
||||||
|
// Requires a logged-in user.
|
||||||
|
let token: libpaket::login::DHLIdToken;
|
||||||
|
|
||||||
|
let response = libpaket::WebClient::new().advices(&token).await.unwrap();
|
||||||
|
|
||||||
|
let client = libpaket::AdviceClient::new();
|
||||||
|
|
||||||
|
if let Some(current_advice) = response.get_current_advice() {
|
||||||
|
let advices_token = client.access_token(&response),unwrap();
|
||||||
|
let bin: Vec<u8> = client.fetch_advice_image(¤t_advice.list[0], advice_token).await.unwrap();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
154
libpaket/src/advices/briefankuendigung.rs
Normal file
154
libpaket/src/advices/briefankuendigung.rs
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
pub use super::Advice;
|
||||||
|
use super::AdvicesResponse;
|
||||||
|
|
||||||
|
use reqwest::header::HeaderMap;
|
||||||
|
use serde::Serialize;
|
||||||
|
use crate::constants::webview_user_agent;
|
||||||
|
use crate::LibraryResult;
|
||||||
|
|
||||||
|
pub struct AdviceClient {
|
||||||
|
client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UatToken(String);
|
||||||
|
|
||||||
|
impl AdviceClient {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
client: reqwest::ClientBuilder::new()
|
||||||
|
.default_headers(headers())
|
||||||
|
.user_agent(webview_user_agent())
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn access_token<'t>(&self, advices: &AdvicesResponse) -> LibraryResult<UatToken> {
|
||||||
|
mini_assert_inval!(advices.has_any_advices());
|
||||||
|
|
||||||
|
mini_assert_api_eq!(advices.access_token_url.as_ref().unwrap().as_str(), endpoint_access_tokens());
|
||||||
|
|
||||||
|
let req = self
|
||||||
|
.client
|
||||||
|
.post(endpoint_access_tokens())
|
||||||
|
.json(&GrantToken {
|
||||||
|
grant_token: advices.grant_token.as_ref().unwrap().clone(),
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let res = self.client.execute(req).await;
|
||||||
|
|
||||||
|
if let Err(err) = res {
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
let res = res.unwrap();
|
||||||
|
|
||||||
|
for cookie in res.cookies() {
|
||||||
|
if cookie.name() == "UAT" {
|
||||||
|
return Ok(UatToken(cookie.value().to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// FIXME: Parse errors here better (checking if we're unauthorized,...)
|
||||||
|
panic!("NO UAT Token in access_token");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_advice_image(
|
||||||
|
&self,
|
||||||
|
advice: &Advice,
|
||||||
|
uat: &UatToken,
|
||||||
|
) -> LibraryResult<Vec<u8>> {
|
||||||
|
println!("URL: {}", &advice.image_url);
|
||||||
|
|
||||||
|
// get key to "deobfuscate" the data
|
||||||
|
// match behaviour from javascript
|
||||||
|
let uuid = {
|
||||||
|
let uuid_str = advice.image_url.split("/").last().unwrap();
|
||||||
|
let uuid_vec = uuid_str.split("?").collect::<Vec<&str>>();
|
||||||
|
*uuid_vec.first().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (secret_key, iv, aad) = {
|
||||||
|
use sha2::{Digest, Sha512};
|
||||||
|
let hash = {
|
||||||
|
let mut hasher = Sha512::new();
|
||||||
|
hasher.update(uuid.as_bytes());
|
||||||
|
hasher.finalize()
|
||||||
|
};
|
||||||
|
//secretKey: e.subarray(0, 32),
|
||||||
|
//iv: e.subarray(32, 44),
|
||||||
|
//aad: e.subarray(44, 56)
|
||||||
|
(
|
||||||
|
hash[0..32].to_vec(),
|
||||||
|
hash[32..44].to_vec(),
|
||||||
|
hash[44..56].to_vec(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let req = self.client
|
||||||
|
.get(&advice.image_url)
|
||||||
|
.header("Cookie", format!("UAT={}", uat.0))
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
let res = self.client.execute(req).await;
|
||||||
|
if let Err(err) = res {
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
let res = res.unwrap();
|
||||||
|
|
||||||
|
let bytes = res.bytes().await.unwrap();
|
||||||
|
|
||||||
|
let bytes = {
|
||||||
|
use aes_gcm::{
|
||||||
|
aead::Payload,
|
||||||
|
aead::{Aead, KeyInit},
|
||||||
|
Aes256Gcm, Nonce,
|
||||||
|
};
|
||||||
|
let aes = Aes256Gcm::new_from_slice(&secret_key[..]).unwrap();
|
||||||
|
let nonce = Nonce::from_slice(&iv[..]);
|
||||||
|
let payload = Payload {
|
||||||
|
msg: &bytes[..],
|
||||||
|
aad: &aad[..],
|
||||||
|
};
|
||||||
|
aes.decrypt(nonce, payload).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct GrantToken {
|
||||||
|
grant_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn headers() -> HeaderMap {
|
||||||
|
let aaa = vec![
|
||||||
|
("Host", "briefankuendigung.dhl.de"),
|
||||||
|
("Connection", "keep-alive"),
|
||||||
|
("Connect-Type", "application/json"),
|
||||||
|
("Pragma", "no-cache"),
|
||||||
|
("Cache-Control", "no-cache,no-store,must-revalidate"),
|
||||||
|
("Expires", "0"),
|
||||||
|
("Accept", "*/*"),
|
||||||
|
("Origin", "https://www.dhl.de"),
|
||||||
|
("X-Requested-With", "de.dhl.paket"),
|
||||||
|
("Sec-Fetch-Site", "same-site"),
|
||||||
|
("Sec-Fetch-Mode", "cors"),
|
||||||
|
("Sec-Fetch-Dest", "empty"),
|
||||||
|
("Referer", "https://www.dhl.de/"),
|
||||||
|
("Accept-Language", "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7"),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut map = HeaderMap::new();
|
||||||
|
for bbb in aaa {
|
||||||
|
map.append(bbb.0, bbb.1.parse().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn endpoint_access_tokens() -> &'static str {
|
||||||
|
"https://briefankuendigung.dhl.de/pdapp-web/access-tokens"
|
||||||
|
}
|
5
libpaket/src/advices/mod.rs
Normal file
5
libpaket/src/advices/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
mod www;
|
||||||
|
mod briefankuendigung;
|
||||||
|
|
||||||
|
pub use www::*;
|
||||||
|
pub use briefankuendigung::*;
|
113
libpaket/src/advices/www.rs
Normal file
113
libpaket/src/advices/www.rs
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_newtype::newtype;
|
||||||
|
|
||||||
|
use crate::utils::{as_url, CookieHeaderValueBuilder};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct Advice {
|
||||||
|
pub campaign_link_text: Option<String>,
|
||||||
|
pub campaign_url: Option<String>,
|
||||||
|
pub image_url: String,
|
||||||
|
pub thumbnail_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct AdvicesList {
|
||||||
|
#[serde(rename = "advices")]
|
||||||
|
pub list: Vec<Advice>,
|
||||||
|
pub date: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
newtype! {
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub AdviceAccessTokenUrl: String[
|
||||||
|
|string| {
|
||||||
|
let res = url::Url::try_from(string.as_str());
|
||||||
|
res.is_ok()
|
||||||
|
}, ""
|
||||||
|
] = "".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct AdvicesResponse {
|
||||||
|
// access_token_url, basic_auth, grant_token is null if no advices are available
|
||||||
|
#[serde(rename = "accessTokenUrl")]
|
||||||
|
pub(super) access_token_url: Option<AdviceAccessTokenUrl>,
|
||||||
|
#[serde(rename = "basicAuth")]
|
||||||
|
pub(super) basic_auth: Option<String>,
|
||||||
|
#[serde(rename = "currentAdvice")]
|
||||||
|
current_advice: Option<AdvicesList>,
|
||||||
|
#[serde(rename = "grantToken")]
|
||||||
|
pub(super) grant_token: Option<String>,
|
||||||
|
|
||||||
|
#[serde(rename = "oldAdvices")]
|
||||||
|
old_advices: Vec<AdvicesList>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdvicesResponse {
|
||||||
|
pub fn has_any_advices(&self) -> bool {
|
||||||
|
self.has_current_advice() || self.has_old_advices()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_current_advice(&self) -> bool {
|
||||||
|
self.current_advice.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_old_advices(&self) -> bool {
|
||||||
|
self.old_advices.len() > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_advice(&self) -> Option<&AdvicesList> {
|
||||||
|
self.current_advice.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_old_advices(&self) -> &Vec<AdvicesList> {
|
||||||
|
self.old_advices.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn endpoint_advices() -> url::Url {
|
||||||
|
as_url("https://www.dhl.de/int-aviseanzeigen/advices?width=400")
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::www::WebClient {
|
||||||
|
// FIXME: more error parsing
|
||||||
|
pub async fn advices(&self, dhli: &crate::login::DHLIdToken) -> crate::LibraryResult<AdvicesResponse> {
|
||||||
|
let cookie_headervalue = CookieHeaderValueBuilder::new()
|
||||||
|
.add_dhli(dhli)
|
||||||
|
.add_dhlcs(dhli)
|
||||||
|
.build_string();
|
||||||
|
|
||||||
|
let req = self.web_client
|
||||||
|
.get(endpoint_advices().clone())
|
||||||
|
.header("Cookie", cookie_headervalue)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let res = self.web_client.execute(req).await;
|
||||||
|
|
||||||
|
if let Err(err) = res {
|
||||||
|
return Err(err.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = res.unwrap();
|
||||||
|
let res_text = res.text().await.unwrap();
|
||||||
|
let res = serde_json::from_str::<AdvicesResponse>(&res_text);
|
||||||
|
|
||||||
|
let res = match res {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if res.access_token_url.is_some() {
|
||||||
|
if res.access_token_url.as_ref().unwrap().as_str() != crate::advices::endpoint_access_tokens() {
|
||||||
|
return Err(crate::LibraryError::APIChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
35
libpaket/src/common.rs
Normal file
35
libpaket/src/common.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
use crate::{LibraryError, LibraryResult};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum APIErrorType {
|
||||||
|
InvalidGrant,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub(crate) struct APIError {
|
||||||
|
pub error: APIErrorType,
|
||||||
|
pub error_description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// mostly used in the aggregated endpoint
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub(crate) enum APIResult<T> {
|
||||||
|
APIError(APIError),
|
||||||
|
Okay(T),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Into<LibraryResult<T>> for APIResult<T> {
|
||||||
|
fn into(self) -> LibraryResult<T> {
|
||||||
|
match self {
|
||||||
|
APIResult::APIError(err) => {
|
||||||
|
return Err(LibraryError::from(err));
|
||||||
|
}
|
||||||
|
APIResult::Okay(res) => {
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
libpaket/src/constants.rs
Normal file
21
libpaket/src/constants.rs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
|
||||||
|
pub fn app_version() -> &'static str {
|
||||||
|
"9.9.1.95 (a72fec7be)"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn linux_android_version() -> &'static str {
|
||||||
|
"Linux; Android 11"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn webview_user_agent() -> String {
|
||||||
|
format!("{} [DHL Paket Android App/{}]", web_user_agent(), app_version())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn device_string() -> &'static str {
|
||||||
|
"OnePlus 6T Build/RQ3A.211001.001"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn web_user_agent() -> String {
|
||||||
|
format!("Mozilla/5.0 ({}; {}; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/117.0.0.0 Mobile Safari/537.36",
|
||||||
|
linux_android_version(), device_string())
|
||||||
|
}
|
66
libpaket/src/lib.rs
Normal file
66
libpaket/src/lib.rs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
mod www;
|
||||||
|
pub use www::WebClient;
|
||||||
|
|
||||||
|
pub mod login;
|
||||||
|
pub use login::OpenIdClient;
|
||||||
|
|
||||||
|
pub mod stammdaten;
|
||||||
|
pub use stammdaten::StammdatenClient;
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
#[macro_use]
|
||||||
|
mod utils;
|
||||||
|
pub mod constants;
|
||||||
|
|
||||||
|
#[cfg(feature = "locker_base")]
|
||||||
|
pub mod locker;
|
||||||
|
|
||||||
|
#[cfg(feature = "advices")]
|
||||||
|
pub mod advices;
|
||||||
|
#[cfg(feature = "advices")]
|
||||||
|
pub use advices::AdviceClient;
|
||||||
|
|
||||||
|
/*#[cfg(test)]
|
||||||
|
pub(crate) mod private;*/
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug, PartialEq)]
|
||||||
|
pub enum LibraryError {
|
||||||
|
#[error("network error: unable to fetch resource")]
|
||||||
|
NetworkFetch,
|
||||||
|
#[error("invalid credentials, unauthorized")]
|
||||||
|
Unauthorized,
|
||||||
|
#[error("invalid argument: {0}")]
|
||||||
|
InvalidArgument(String),
|
||||||
|
#[error("internal error, unable to decode: {0}")]
|
||||||
|
DecodeError(String),
|
||||||
|
#[error("upstream api was changed. not continuing")]
|
||||||
|
APIChange,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type LibraryResult<T> = Result<T, LibraryError>;
|
||||||
|
|
||||||
|
impl From<reqwest::Error> for LibraryError {
|
||||||
|
fn from(item: reqwest::Error) -> Self {
|
||||||
|
if item.is_timeout() || item.is_request() || item.is_connect() || item.is_decode() {
|
||||||
|
Self::NetworkFetch
|
||||||
|
} else {
|
||||||
|
panic!("FIXME: unknown reqwest error kind: {:?}", item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<common::APIError> for LibraryError {
|
||||||
|
fn from(value: common::APIError) -> Self {
|
||||||
|
match value.error {
|
||||||
|
common::APIErrorType::InvalidGrant => Self::Unauthorized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for LibraryError {
|
||||||
|
fn from(value: serde_json::Error) -> Self {
|
||||||
|
Self::DecodeError(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
40
libpaket/src/locker/api.rs
Normal file
40
libpaket/src/locker/api.rs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
use reqwest::{header::HeaderMap, Request, RequestBuilder};
|
||||||
|
|
||||||
|
use crate::constants::{app_version, web_user_agent};
|
||||||
|
|
||||||
|
pub struct Client {
|
||||||
|
client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Client {
|
||||||
|
client: reqwest::ClientBuilder::new()
|
||||||
|
.default_headers(headers())
|
||||||
|
.user_agent(user_agent())
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_agent() -> String {
|
||||||
|
format!("LPS Consumer SDK/2.1.0 okhttp/4.9.1 {}", web_user_agent())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn headers() -> HeaderMap {
|
||||||
|
let aaa = vec![
|
||||||
|
/* ("accept", "application/json") */
|
||||||
|
("app-version", app_version()),
|
||||||
|
("device-os", "Android"),
|
||||||
|
("device-key", "") /* is the android id... */
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut map = HeaderMap::new();
|
||||||
|
for bbb in aaa {
|
||||||
|
map.append(bbb.0, bbb.1.parse().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
map
|
||||||
|
}
|
330
libpaket/src/locker/command.rs
Normal file
330
libpaket/src/locker/command.rs
Normal file
|
@ -0,0 +1,330 @@
|
||||||
|
use num_enum::TryFromPrimitive;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{LibraryError, LibraryResult};
|
||||||
|
|
||||||
|
use super::LockerServiceUUID;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, TryFromPrimitive, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum CommandType {
|
||||||
|
InitSessionRequest = 1,
|
||||||
|
InitSessionResponse = 2,
|
||||||
|
OpenRequest = 3,
|
||||||
|
OpenedRespone = 4,
|
||||||
|
AlreadyOpenResponse = 5,
|
||||||
|
OpenAutoignoreRequest = 6,
|
||||||
|
CloseCompartmentsResponse = 7,
|
||||||
|
PingRequest = 8,
|
||||||
|
CheckFirmwareVersionRequest = 9,
|
||||||
|
CheckFirmwareVersionResponse = 10,
|
||||||
|
UpdateFirmwareRequest = 11,
|
||||||
|
UpdateFirmwareResponse = 12,
|
||||||
|
GetLogCounterRequest = 13,
|
||||||
|
GetLogCounterResponse = 14,
|
||||||
|
GetLogFileRequest = 15,
|
||||||
|
GetLogFileResponse = 16,
|
||||||
|
CheckOpenedComparmentsRequest = 17,
|
||||||
|
CheckOpenedCompartmentsResponse = 18,
|
||||||
|
TransferFirmwareRequest = 19,
|
||||||
|
TransferFirmwareResponse = 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
const REQUESTS: [CommandType; 10] = [
|
||||||
|
CommandType::InitSessionRequest,
|
||||||
|
CommandType::OpenRequest,
|
||||||
|
CommandType::OpenAutoignoreRequest,
|
||||||
|
CommandType::PingRequest,
|
||||||
|
CommandType::CheckFirmwareVersionRequest,
|
||||||
|
CommandType::UpdateFirmwareRequest,
|
||||||
|
CommandType::GetLogCounterRequest,
|
||||||
|
CommandType::GetLogFileRequest,
|
||||||
|
CommandType::CheckOpenedComparmentsRequest,
|
||||||
|
CommandType::TransferFirmwareRequest,
|
||||||
|
];
|
||||||
|
|
||||||
|
struct ResponseInitSession {
|
||||||
|
raw_command: Command
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Response {
|
||||||
|
InitSession(ResponseInitSession),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Command> for Response {
|
||||||
|
type Error = LibraryError;
|
||||||
|
|
||||||
|
fn try_from(value: Command) -> Result<Self, Self::Error> {
|
||||||
|
if REQUESTS.binary_search(&value.r#type).is_ok() {
|
||||||
|
return Err(LibraryError::InvalidArgument("TryFrom<Command> for Response: CommandType is a Request (expected Response)".to_string()))
|
||||||
|
}
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message(
|
||||||
|
// command (byte)
|
||||||
|
// length in bytes (int)
|
||||||
|
// payload (arr)
|
||||||
|
// initVector (arr)
|
||||||
|
// metadata (arr)
|
||||||
|
// checksum (short)
|
||||||
|
//
|
||||||
|
|
||||||
|
// Checksum function
|
||||||
|
|
||||||
|
|
||||||
|
pub struct Command {
|
||||||
|
r#type: CommandType,
|
||||||
|
payload: Vec<u8>,
|
||||||
|
init_vector: Vec<u8>,
|
||||||
|
metadata: Vec<u8>
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PrimitiveBuilder {
|
||||||
|
bin: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrimitiveBuilder {
|
||||||
|
fn new() -> Self {
|
||||||
|
PrimitiveBuilder {
|
||||||
|
bin: Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_u8(self, number: u8) -> Self {
|
||||||
|
self.write_array(&[number])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_u16(self, number: u16) -> Self {
|
||||||
|
self.write_array(&number.to_be_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_u32(self, number: u32) -> Self {
|
||||||
|
self.write_array(&number.to_be_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_array(mut self, new: &[u8]) -> Self {
|
||||||
|
for b in new {
|
||||||
|
self.bin.push(*b);
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_array_with_len(self, bin: &[u8]) -> Self {
|
||||||
|
self
|
||||||
|
.write_u32(bin.len() as u32)
|
||||||
|
.write_array(bin)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(self) -> Vec<u8> {
|
||||||
|
self.bin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PrimitiveReader<'a> {
|
||||||
|
offset: usize,
|
||||||
|
vec: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yes, I know Cursor exists and eio exists. from_be_bytes should be a stdlib trait tho.
|
||||||
|
impl<'a> PrimitiveReader<'a> {
|
||||||
|
fn read_u8(&mut self) -> u8 {
|
||||||
|
let number = self.vec[self.offset];
|
||||||
|
self.offset += 1;
|
||||||
|
number
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u16(&mut self) -> u16 {
|
||||||
|
let arr: [u8; 2] = self.read_arr_const();
|
||||||
|
u16::from_be_bytes(arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u32(&mut self) -> u32 {
|
||||||
|
let arr: [u8; 4] = self.read_arr_const();
|
||||||
|
u32::from_be_bytes(arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u64(&mut self) -> u64 {
|
||||||
|
let arr: [u8; 8] = self.read_arr_const();
|
||||||
|
u64::from_be_bytes(arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_arr_const<const N: usize>(&mut self) -> [u8; N] {
|
||||||
|
let mut arr = [0u8; N];
|
||||||
|
for n in 0..N {
|
||||||
|
arr[n] = self.read_u8();
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_arr(&mut self, n: usize) -> Vec<u8> {
|
||||||
|
let mut arr: Vec<u8> = vec![];
|
||||||
|
for _ in 0..n {
|
||||||
|
arr.push(self.read_u8());
|
||||||
|
}
|
||||||
|
|
||||||
|
arr
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_arr_from_len(&mut self) -> Vec<u8> {
|
||||||
|
let size = self.read_u32() as usize;
|
||||||
|
self.read_arr(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn left_to_process(&self) -> usize {
|
||||||
|
self.vec.len() - self.offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
|
fn checksum(bin: &[u8]) -> u16 {
|
||||||
|
// CRC16 of some kind...
|
||||||
|
// i'm too eepy to use the crc crate for this
|
||||||
|
let mut result: u16 = 0;
|
||||||
|
|
||||||
|
for byte in bin {
|
||||||
|
result = result ^ ((*byte as u16) << 8);
|
||||||
|
let mut i = 0;
|
||||||
|
while i < 8 {
|
||||||
|
let mut a = result << 1;
|
||||||
|
if result & 0x8000 != 0 {
|
||||||
|
a ^= 0x1021;
|
||||||
|
}
|
||||||
|
result = a;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(bin: Vec<u8>) -> LibraryResult<Self> {
|
||||||
|
// command byte + message length + 3 empty message arguments (array with size 0) + 2 checksum bytes
|
||||||
|
let to_few_bytes = LibraryError::InvalidArgument("Command::parse: Invalid vec.len() (to few bytes)".to_string());
|
||||||
|
if bin.len() < 1 + 4 + (4 * 3) + 2 {
|
||||||
|
return Err(to_few_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let checksum = {
|
||||||
|
PrimitiveReader {
|
||||||
|
offset: bin.len() - 2,
|
||||||
|
vec: bin.as_slice(),
|
||||||
|
}.read_u16()
|
||||||
|
};
|
||||||
|
|
||||||
|
if checksum != Self::checksum(&bin[0..bin.len() - 2]) {
|
||||||
|
return Err(LibraryError::InvalidArgument("Command::parse: Invalid checksum".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut reader = PrimitiveReader {
|
||||||
|
offset: 0,
|
||||||
|
vec: bin.as_slice(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let r#type = reader.read_u8();
|
||||||
|
let Ok(r#type) = CommandType::try_from_primitive(r#type) else {
|
||||||
|
return Err(LibraryError::DecodeError("unable determine CommandType".to_string()));
|
||||||
|
};
|
||||||
|
|
||||||
|
let size_of_message = reader.read_u32() as usize;
|
||||||
|
if reader.left_to_process() < size_of_message {
|
||||||
|
return Err(to_few_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload: Vec<u8> = reader.read_arr_from_len();
|
||||||
|
let init_vector = reader.read_arr_from_len();
|
||||||
|
let metadata = reader.read_arr_from_len();
|
||||||
|
|
||||||
|
Ok(Command {
|
||||||
|
r#type,
|
||||||
|
payload: payload.clone(),
|
||||||
|
init_vector: init_vector.clone(),
|
||||||
|
metadata: metadata.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish(self) -> Vec<u8> {
|
||||||
|
let vec1 = PrimitiveBuilder::new()
|
||||||
|
.write_array_with_len(&self.payload)
|
||||||
|
.write_array_with_len(&self.init_vector)
|
||||||
|
.write_array_with_len(&self.metadata)
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
let vec2 = PrimitiveBuilder::new()
|
||||||
|
.write_u8(self.r#type as u8)
|
||||||
|
.write_u32(vec1.len() as u32 + 2)
|
||||||
|
.write_array(&vec1)
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
PrimitiveBuilder::new()
|
||||||
|
.write_array(&vec2)
|
||||||
|
.write_u16(Self::checksum(vec2.as_slice()))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_only_decimal(str: &String) {
|
||||||
|
for c in str.chars() {
|
||||||
|
assert!(c.is_digit(10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_session_request(uuid: LockerServiceUUID) -> LibraryResult<Self> {
|
||||||
|
let uuid: Uuid = uuid.into();
|
||||||
|
let fields = uuid.as_fields();
|
||||||
|
// For some reason the code make sure that the heximal string only contains digits from base10...
|
||||||
|
// Interesting findings: first 5 digits are PLZ, last three packstation id (end-user), then uid?
|
||||||
|
|
||||||
|
// de.dhl.paket does some kinky string conversion
|
||||||
|
|
||||||
|
let vec = Vec::<u8>::new().into_iter()
|
||||||
|
.chain(fields.0.to_be_bytes().into_iter())
|
||||||
|
.chain(fields.1.to_be_bytes().into_iter())
|
||||||
|
.chain(fields.2.to_be_bytes().into_iter())
|
||||||
|
.chain(fields.3.to_vec().into_iter())
|
||||||
|
.collect::<Vec<u8>>();
|
||||||
|
|
||||||
|
let new_vec = PrimitiveBuilder::new()
|
||||||
|
.write_array_with_len(&vec).finish();
|
||||||
|
|
||||||
|
Ok(Command {
|
||||||
|
r#type: CommandType::InitSessionRequest,
|
||||||
|
payload: Vec::new(),
|
||||||
|
init_vector: Vec::new(),
|
||||||
|
metadata: new_vec,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::private::sample_packstation::{INIT_SESSION_REQUEST, INIT_SESSION_RESPONSE, INIT_SESSION_SERVICE_UUID};
|
||||||
|
|
||||||
|
use super::{Command, CommandType};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_init_request() {
|
||||||
|
let mut corrected = Vec::<u8>::new();
|
||||||
|
for b in INIT_SESSION_REQUEST {
|
||||||
|
corrected.push(b as u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
let command = Command::init_session_request(uuid::uuid!(INIT_SESSION_SERVICE_UUID).try_into().unwrap()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(command.finish().as_slice(), corrected.as_slice())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_init_session_response() {
|
||||||
|
let mut corrected = Vec::<u8>::new();
|
||||||
|
for b in INIT_SESSION_RESPONSE {
|
||||||
|
corrected.push(b as u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
let command = Command::parse(corrected);
|
||||||
|
}
|
||||||
|
|
||||||
|
}*/
|
75
libpaket/src/locker/crypto.rs
Normal file
75
libpaket/src/locker/crypto.rs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
|
use rand::prelude::*;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use ed25519_dalek::SigningKey;
|
||||||
|
use ed25519_dalek::Signer;
|
||||||
|
|
||||||
|
|
||||||
|
pub struct CustomerKeySeed {
|
||||||
|
postnumber: String,
|
||||||
|
seed: Seed,
|
||||||
|
uuid: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Seed {
|
||||||
|
bytes: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Seed {
|
||||||
|
pub(self) fn from_bytes(bytes: Vec<u8>) -> Self {
|
||||||
|
assert_eq!(bytes.len(), 32);
|
||||||
|
Seed {
|
||||||
|
bytes: bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn random() -> Self {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
|
||||||
|
let mut bytes: Vec<u8> = vec![0; 32];
|
||||||
|
|
||||||
|
rng.fill_bytes(bytes.as_mut_slice());
|
||||||
|
|
||||||
|
Seed { bytes: bytes }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_bytes(&self) -> &[u8; 32] {
|
||||||
|
(&self.bytes[..]).try_into().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomerKeySeed {
|
||||||
|
pub fn new(postnumber: String) -> Self {
|
||||||
|
CustomerKeySeed {
|
||||||
|
postnumber,
|
||||||
|
seed: Seed::random(),
|
||||||
|
uuid: uuid::Uuid::new_v4(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from(postnumber: &String, seed: Vec<u8>, uuid: &Uuid) -> Self {
|
||||||
|
CustomerKeySeed {
|
||||||
|
postnumber: postnumber.clone(),
|
||||||
|
seed: Seed::from_bytes(seed),
|
||||||
|
uuid: uuid.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sign(&self, message: &[u8]) -> String {
|
||||||
|
let signing_key = SigningKey::from_bytes(self.seed.as_bytes());
|
||||||
|
|
||||||
|
let signature = signing_key.sign(message);
|
||||||
|
|
||||||
|
let sig_str = general_purpose::STANDARD.encode(signature.to_bytes());
|
||||||
|
|
||||||
|
format!("{}.{}", self.uuid.to_string(), sig_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_public_key(&self) -> String {
|
||||||
|
let signing_key = SigningKey::from_bytes(self.seed.as_bytes());
|
||||||
|
let public_bytes = signing_key.verifying_key().to_bytes();
|
||||||
|
let public_str = general_purpose::STANDARD.encode(public_bytes);
|
||||||
|
|
||||||
|
format!("{}.{}", self.uuid.to_string(), public_str)
|
||||||
|
}
|
||||||
|
}
|
23
libpaket/src/locker/mod.rs
Normal file
23
libpaket/src/locker/mod.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
#[cfg(feature = "locker_ble")]
|
||||||
|
mod command;
|
||||||
|
#[cfg(feature = "locker_ble")]
|
||||||
|
mod types;
|
||||||
|
#[cfg(feature = "locker_ble")]
|
||||||
|
pub use types::*;
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
pub mod crypto;
|
||||||
|
|
||||||
|
#[cfg(feature = "locker_register_base")]
|
||||||
|
mod register_base;
|
||||||
|
#[cfg(feature = "locker_register_regtoken")]
|
||||||
|
mod register_regtoken;
|
||||||
|
#[cfg(feature = "locker_register_regtoken")]
|
||||||
|
mod regtoken;
|
||||||
|
#[cfg(feature = "locker_register_base")]
|
||||||
|
pub mod register {
|
||||||
|
pub use super::register_base::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "locker_register_regtoken")]
|
||||||
|
pub use super::regtoken::*;
|
||||||
|
}
|
113
libpaket/src/locker/register_base.rs
Normal file
113
libpaket/src/locker/register_base.rs
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_repr::Deserialize_repr;
|
||||||
|
|
||||||
|
use crate::common::APIResult;
|
||||||
|
use crate::login::DHLIdToken;
|
||||||
|
use crate::LibraryResult;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct RegistrationPayload {
|
||||||
|
pub(crate) challenge: String,
|
||||||
|
#[serde(rename = "registrationOptions")]
|
||||||
|
pub registration_options: Vec<RegistrationOption>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, PartialEq, Eq)]
|
||||||
|
pub enum RegistrationOption {
|
||||||
|
#[serde(rename = "registerDirect")]
|
||||||
|
Direct,
|
||||||
|
#[serde(rename = "registerByRegToken")]
|
||||||
|
ByRegToken,
|
||||||
|
#[serde(rename = "registerByPeerDevice")]
|
||||||
|
ByPeerDevice,
|
||||||
|
#[serde(rename = "registerByVerifyToken")]
|
||||||
|
ByVerifyToken,
|
||||||
|
#[serde(rename = "registerByCardNumber")]
|
||||||
|
ByCardNumber,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct DeviceMetadata {
|
||||||
|
pub manufacturer_model: String,
|
||||||
|
pub manufacturer_name: String,
|
||||||
|
pub manufacturer_operating_system: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Device {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub metadata: DeviceMetadata,
|
||||||
|
|
||||||
|
pub id: String,
|
||||||
|
pub registered_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum DeviceRegistrationResponse {
|
||||||
|
Error {
|
||||||
|
id: APIRegisterError,
|
||||||
|
description: String,
|
||||||
|
},
|
||||||
|
Okay(Device),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize_repr, PartialEq, Debug)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum APIRegisterError {
|
||||||
|
MaxDevices = 7,
|
||||||
|
ScanInvalidCustomerCard = 14,
|
||||||
|
AlreadyRegisteredOneDevice = 16,
|
||||||
|
ScanInvalidRegToken = 17,
|
||||||
|
ScanExpiredAuthToken = 19,
|
||||||
|
ScanInvalidAuthToken = 20,
|
||||||
|
UserCanNotYetRegister = 21,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(super) struct RegistrationCommonDevice {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub(super) metadata: DeviceMetadata,
|
||||||
|
|
||||||
|
pub(super) public_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::StammdatenClient {
|
||||||
|
pub async fn begin_registration(
|
||||||
|
&self,
|
||||||
|
dhli: &DHLIdToken,
|
||||||
|
) -> LibraryResult<RegistrationPayload> {
|
||||||
|
let body = "{\"hasVerifyToken\": false}";
|
||||||
|
|
||||||
|
let req = self.base_request(
|
||||||
|
self.client
|
||||||
|
.put(endpoint_devices_begin_registration())
|
||||||
|
.body(body)
|
||||||
|
.header("content-length", body.len())
|
||||||
|
.header("content-type", "application/json; charset=UTF-8"),
|
||||||
|
dhli,
|
||||||
|
);
|
||||||
|
let res = self.client.execute(req).await;
|
||||||
|
if let Err(err) = res {
|
||||||
|
return Err(err.into());
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = res.unwrap().text().await.unwrap();
|
||||||
|
let res = serde_json::from_str::<APIResult<RegistrationPayload>>(&res);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(res) => res.into(),
|
||||||
|
Err(err) => Err(err.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn endpoint_devices_begin_registration() -> &'static str {
|
||||||
|
"https://www.dhl.de/int-stammdaten/public/devices/beginRegistration"
|
||||||
|
}
|
152
libpaket/src/locker/register_regtoken.rs
Normal file
152
libpaket/src/locker/register_regtoken.rs
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use hmac::{digest::CtOutput, Mac, SimpleHmac};
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
|
use crate::common::APIResult;
|
||||||
|
use crate::locker::register::{DeviceMetadata, DeviceRegistrationResponse, RegToken, RegistrationCommonDevice, RegistrationOption, RegistrationPayload};
|
||||||
|
use crate::locker::crypto::CustomerKeySeed;
|
||||||
|
use crate::login::DHLIdToken;
|
||||||
|
use crate::LibraryResult;
|
||||||
|
|
||||||
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) struct RegistrationRegToken {
|
||||||
|
pub(super) challenge: String,
|
||||||
|
pub(super) customer_password: String,
|
||||||
|
pub(super) device: RegistrationCommonDevice,
|
||||||
|
pub(super) verifier: String,
|
||||||
|
pub(super) verifier_signature: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegistrationRegToken {
|
||||||
|
pub fn new(
|
||||||
|
customer_key_seed: &CustomerKeySeed,
|
||||||
|
begin_registration: RegistrationPayload,
|
||||||
|
reg_token: &RegToken,
|
||||||
|
device_metadata: DeviceMetadata,
|
||||||
|
) -> Self {
|
||||||
|
let challange_bin = general_purpose::STANDARD.decode(begin_registration.challenge.as_str()).unwrap();
|
||||||
|
|
||||||
|
let mut mac = reg_token.hmac();
|
||||||
|
mac.update(challange_bin.as_slice());
|
||||||
|
|
||||||
|
let verifier_bin: CtOutput<SimpleHmac<Sha256>> = mac.finalize();
|
||||||
|
let verifier_bin = verifier_bin.into_bytes();
|
||||||
|
let verifier = general_purpose::STANDARD.encode(&verifier_bin);
|
||||||
|
let verifier_signature = customer_key_seed.sign(&verifier_bin);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
challenge: begin_registration.challenge,
|
||||||
|
customer_password: reg_token.customer_password(),
|
||||||
|
device: RegistrationCommonDevice {
|
||||||
|
metadata: device_metadata,
|
||||||
|
public_key: customer_key_seed.get_public_key(),
|
||||||
|
},
|
||||||
|
verifier: verifier,
|
||||||
|
verifier_signature: verifier_signature,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::StammdatenClient {
|
||||||
|
|
||||||
|
pub async fn register_by_regtoken(
|
||||||
|
&self,
|
||||||
|
dhli: &DHLIdToken,
|
||||||
|
customer_key_seed: &CustomerKeySeed,
|
||||||
|
registration_payload: RegistrationPayload,
|
||||||
|
device_metadata: DeviceMetadata,
|
||||||
|
reg_token: &crate::locker::register::RegToken,
|
||||||
|
) -> LibraryResult<DeviceRegistrationResponse> {
|
||||||
|
|
||||||
|
let mut valid = false;
|
||||||
|
for option in registration_payload.registration_options.iter() {
|
||||||
|
if *option == RegistrationOption::ByRegToken {
|
||||||
|
valid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(valid);
|
||||||
|
|
||||||
|
let body = RegistrationRegToken::new(
|
||||||
|
customer_key_seed,
|
||||||
|
registration_payload,
|
||||||
|
reg_token,
|
||||||
|
device_metadata
|
||||||
|
);
|
||||||
|
|
||||||
|
let body = serde_json::to_string(&body).unwrap();
|
||||||
|
|
||||||
|
let req = self.base_request(
|
||||||
|
self.client
|
||||||
|
.post(endpoint_devices_register_by_regtoken())
|
||||||
|
.header("content-length", body.len())
|
||||||
|
.body(body)
|
||||||
|
.header("content-type", "application/json; charset=UTF-8"),
|
||||||
|
dhli,
|
||||||
|
);
|
||||||
|
|
||||||
|
let res = self.client.execute(req).await;
|
||||||
|
if let Err(err) = res {
|
||||||
|
return Err(err.into());
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = res.unwrap().text().await.unwrap();
|
||||||
|
let res = serde_json::from_str::<APIResult<DeviceRegistrationResponse>>(&res);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(res) => res.into(),
|
||||||
|
Err(err) => Err(err.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn endpoint_devices_register_by_regtoken() -> &'static str {
|
||||||
|
"https://www.dhl.de/int-stammdaten/public/devices/registerByRegToken"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*#[cfg(test)]
|
||||||
|
mod registration_api {
|
||||||
|
use std::str::FromStr as _;
|
||||||
|
use base64::Engine as _;
|
||||||
|
|
||||||
|
use crate::private::init as private;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn regtoken_customer_password() {
|
||||||
|
let regtoken = super::RegToken::parse_from_qrcode_uri(&private::IN_REGTOKEN).unwrap();
|
||||||
|
assert!(regtoken.customer_password() == private::OUT_CUSTOMER_PASSWORD);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn register_via_regtoken(){
|
||||||
|
let regtoken = super::RegToken::parse_from_qrcode_uri(&private::IN_REGTOKEN).unwrap();
|
||||||
|
|
||||||
|
let customer_key_seed = crate::locker::crypto::CustomerKeySeed::from(&private::IN_POSTNUMBER.to_string(),
|
||||||
|
base64::engine::general_purpose::STANDARD.decode(&private::IN_SEED_BASE64).unwrap(),
|
||||||
|
&uuid::Uuid::from_str(&private::IN_KEY_ID).unwrap());
|
||||||
|
|
||||||
|
let registration_payload = super::RegistrationPayload {
|
||||||
|
challenge: private::IN_CHALLENGE.to_string(),
|
||||||
|
registration_options: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let reg_reg_token = super::RegistrationRegToken::new(
|
||||||
|
&customer_key_seed,
|
||||||
|
registration_payload,
|
||||||
|
®token,
|
||||||
|
super::DeviceMetadata { manufacturer_model: "".to_string(), manufacturer_name: "".to_string(), manufacturer_operating_system: "".to_string(), name: "".to_string() }
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(reg_reg_token.verifier == private::OUT_VERIFIER);
|
||||||
|
assert!(reg_reg_token.verifier_signature == private::OUT_VERIFIERSIGNATURE);
|
||||||
|
assert!(reg_reg_token.device.public_key == private::OUT_DEVICE_PUBKEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
48
libpaket/src/locker/regtoken.rs
Normal file
48
libpaket/src/locker/regtoken.rs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
|
use hmac::{Mac, SimpleHmac};
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
|
use crate::{LibraryResult, LibraryError};
|
||||||
|
|
||||||
|
pub struct RegToken {
|
||||||
|
token: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegToken {
|
||||||
|
pub fn parse_from_qrcode_uri(uri: &str) -> LibraryResult<RegToken> {
|
||||||
|
if uri.len() <= 23 {
|
||||||
|
return Err(LibraryError::DecodeError("RegTokenUri too short".to_string()));
|
||||||
|
}
|
||||||
|
if !uri.starts_with("urn:dhl.de:regtoken:v1:") {
|
||||||
|
return Err(LibraryError::DecodeError("RegTokenUri has invalid magic".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = &uri[23..];
|
||||||
|
let token = general_purpose::STANDARD.decode(token);
|
||||||
|
|
||||||
|
let Ok(mut token) = token else {
|
||||||
|
return Err(LibraryError::DecodeError("RegTokenUri not decodeable (base64)".to_string()));
|
||||||
|
};
|
||||||
|
|
||||||
|
if token.len() > 64 {
|
||||||
|
return Err(LibraryError::DecodeError("RegToken longer than expected".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if token.len() < 32 {
|
||||||
|
token.extend(vec![0; 32 - token.len()].iter())
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(RegToken { token })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn customer_password(&self) -> String {
|
||||||
|
general_purpose::STANDARD.encode(&self.token[32..])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hmac(&self) -> SimpleHmac<Sha256> {
|
||||||
|
let mac = SimpleHmac::<Sha256>::new_from_slice(&self.token[0..32])
|
||||||
|
.expect("HMAC can take key of any size");
|
||||||
|
|
||||||
|
mac
|
||||||
|
}
|
||||||
|
}
|
29
libpaket/src/locker/types.rs
Normal file
29
libpaket/src/locker/types.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
use crate::LibraryError;
|
||||||
|
|
||||||
|
// 601e7028-0565-
|
||||||
|
pub static LOCKER_SERVICE_UUID_PREFIX: (u32, u16) = (0x601e7028, 0x0565);
|
||||||
|
|
||||||
|
pub struct LockerServiceUUID {
|
||||||
|
service_uuid: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<uuid::Uuid> for LockerServiceUUID {
|
||||||
|
type Error = crate::LibraryError;
|
||||||
|
|
||||||
|
fn try_from(value: uuid::Uuid) -> Result<Self, Self::Error> {
|
||||||
|
let fields = value.as_fields();
|
||||||
|
if fields.0 != LOCKER_SERVICE_UUID_PREFIX.0 || fields.1 != LOCKER_SERVICE_UUID_PREFIX.1 {
|
||||||
|
return Err(LibraryError::InvalidArgument("TryFrom<Uuid> for LockerServiceUUID: prefix mismatch (expected 601e7028-0565-)".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(LockerServiceUUID {
|
||||||
|
service_uuid: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<uuid::Uuid> for LockerServiceUUID {
|
||||||
|
fn into(self) -> uuid::Uuid {
|
||||||
|
self.service_uuid
|
||||||
|
}
|
||||||
|
}
|
130
libpaket/src/login/constants.rs
Normal file
130
libpaket/src/login/constants.rs
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
use super::utils::CodeVerfier;
|
||||||
|
use super::dhl_claims::DHLClaimsOptional;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
pub fn client_id() -> &'static str {
|
||||||
|
"42ec7de4-e357-4c5d-aa63-f6aae5ca4d8f"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn redirect_uri() -> &'static str {
|
||||||
|
"dhllogin://de.dhl.paket/login"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod token {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn refresh_token_form(refresh_token: &str) -> Vec<(&str, &str)> {
|
||||||
|
vec![
|
||||||
|
("refresh_token", refresh_token),
|
||||||
|
("grant_type", "refresh_token"),
|
||||||
|
("client_id", client_id()),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn authorization_code_form(
|
||||||
|
authorization_code: String,
|
||||||
|
code_verfier: &CodeVerfier,
|
||||||
|
) -> Vec<(String, String)> {
|
||||||
|
vec![
|
||||||
|
("code".to_string(), authorization_code),
|
||||||
|
("grant_type".to_string(), "authorization_code".to_string()),
|
||||||
|
("redirect_uri".to_string(), redirect_uri().to_string()),
|
||||||
|
("code_verifier".to_string(), code_verfier.code_verfier()),
|
||||||
|
("client_id".to_string(), client_id().to_string()),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user_agent() -> &'static str {
|
||||||
|
"Dalvik/2.1.0 (Linux; U; Android 11; OnePlus 6T Build/RQ3A.211001.001)"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn endpoint() -> &'static str {
|
||||||
|
"https://login.dhl.de/af5f9bb6-27ad-4af4-9445-008e7a5cddb8/login/token"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn headers() -> reqwest::header::HeaderMap {
|
||||||
|
let aaa = vec![
|
||||||
|
("Content-Type", "application/x-www-form-urlencoded"),
|
||||||
|
("Accept", "application/json"),
|
||||||
|
("Content-Length", "150"),
|
||||||
|
("Host", "login.dhl.de"),
|
||||||
|
("Connection", "Keep-Alive"),
|
||||||
|
("Accept-Encoding", "gzip"),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut map = reqwest::header::HeaderMap::new();
|
||||||
|
for bbb in aaa {
|
||||||
|
map.append(bbb.0, bbb.1.parse().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
map
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod webbrowser_authorize {
|
||||||
|
use crate::constants::web_user_agent;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn user_agent() -> String {
|
||||||
|
web_user_agent()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_endpoint(nonce: &String, code_verfier: &CodeVerfier) -> String {
|
||||||
|
endpoint().to_string() + authorize_query_string(nonce, code_verfier).as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn endpoint() -> &'static str {
|
||||||
|
"https://login.dhl.de/af5f9bb6-27ad-4af4-9445-008e7a5cddb8/login/authorize"
|
||||||
|
}
|
||||||
|
|
||||||
|
// copying an app state
|
||||||
|
fn state_json() -> &'static str {
|
||||||
|
"ewogICJmaWQiOiAiYXBwLWxvZ2luLW1laHItZm9vdGVyIiwKICAiaGlkIjogImFwcC1sb2dpbi1tZWhyLWhlYWRlciIsCiAgIm1yIjogZmFsc2UsCiAgInJwIjogZmFsc2UsCiAgInJzIjogdHJ1ZSwKICAicnYiOiBmYWxzZQp9"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authorize_query_string(nonce: &String, code_verfier: &CodeVerfier) -> String {
|
||||||
|
let mut out = "?".to_string();
|
||||||
|
|
||||||
|
let mut iter = authorize_query(nonce, code_verfier).into_iter();
|
||||||
|
let mut not_first_run = false;
|
||||||
|
|
||||||
|
while let Some(val) = iter.next() {
|
||||||
|
if not_first_run {
|
||||||
|
out = out + "&";
|
||||||
|
} else {
|
||||||
|
not_first_run = true;
|
||||||
|
}
|
||||||
|
out = out
|
||||||
|
+ urlencoding::encode(&val.0).into_owned().as_str()
|
||||||
|
+ "="
|
||||||
|
+ urlencoding::encode(&val.1).into_owned().as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", out);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Default)]
|
||||||
|
struct Claims {
|
||||||
|
pub id_token: DHLClaimsOptional,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn authorize_query(nonce: &String, code_verfier: &CodeVerfier) -> Vec<(String, String)> {
|
||||||
|
vec![
|
||||||
|
("redirect_uri".to_string(), redirect_uri().to_string()),
|
||||||
|
("client_id".to_string(), client_id().to_string()),
|
||||||
|
("response_type".to_string(), "code".to_string()),
|
||||||
|
("prompt".to_string(), "login".to_string()),
|
||||||
|
("state".to_string(), state_json().to_string()),
|
||||||
|
("nonce".to_string(), nonce.clone()),
|
||||||
|
("scope".to_string(), "openid offline_access".to_string()),
|
||||||
|
("code_challenge".to_string(), code_verfier.code_challenge()),
|
||||||
|
("code_challenge_method".to_string(), "S256".to_string()),
|
||||||
|
(
|
||||||
|
"claims".to_string(),
|
||||||
|
serde_json::to_string(&Claims::default()).unwrap(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
58
libpaket/src/login/dhl_claims.rs
Normal file
58
libpaket/src/login/dhl_claims.rs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
use super::openid_token::OpenIDToken;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_newtype::newtype;
|
||||||
|
|
||||||
|
// used to generate scopes, copy of DHLClaims
|
||||||
|
#[derive(Serialize, Default, Debug)]
|
||||||
|
pub(super) struct DHLClaimsOptional {
|
||||||
|
customer_type: Option<u64>,
|
||||||
|
data_confirmation_required: Option<String>,
|
||||||
|
deactivate_account: Option<String>,
|
||||||
|
display_name: Option<String>,
|
||||||
|
email: Option<String>,
|
||||||
|
last_login: Option<String>,
|
||||||
|
post_number: Option<String>,
|
||||||
|
service_mask: Option<String>,
|
||||||
|
twofa: Option<u64>, // probably an enum
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: made everything optional again, as those weren't initally there
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct DHLClaims {
|
||||||
|
customer_type: u64,
|
||||||
|
data_confirmation_required: Option<String>,
|
||||||
|
deactivate_account: Option<String>,
|
||||||
|
display_name: Option<String>,
|
||||||
|
email: Option<String>,
|
||||||
|
last_login: Option<String>,
|
||||||
|
post_number: PostNumber,
|
||||||
|
service_mask: Option<String>,
|
||||||
|
twofa: Option<u64>, // FIXME: probably an enum
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Sanity checks
|
||||||
|
newtype! {
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub PostNumber: String[|_k| true,
|
||||||
|
"",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
newtype! {
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub DHLCs: String[|_k| true,
|
||||||
|
"",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenIDToken<DHLClaims> {
|
||||||
|
pub fn get_post_number(&self) -> &PostNumber {
|
||||||
|
&self.claims.custom.post_number
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_dhlcs(&self) -> DHLCs {
|
||||||
|
DHLCs::new(self.claims.sub.clone()).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type DHLIdToken = OpenIDToken<DHLClaims>;
|
100
libpaket/src/login/mod.rs
Normal file
100
libpaket/src/login/mod.rs
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
pub mod constants;
|
||||||
|
mod dhl_claims;
|
||||||
|
mod openid_response;
|
||||||
|
pub mod openid_token;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
pub use self::dhl_claims::{DHLIdToken, DHLCs};
|
||||||
|
pub use self::openid_response::{RefreshToken, TokenResponse};
|
||||||
|
pub use self::utils::{CodeVerfier, create_nonce};
|
||||||
|
|
||||||
|
use super::common::APIResult;
|
||||||
|
use crate::{LibraryError, LibraryResult};
|
||||||
|
|
||||||
|
|
||||||
|
pub struct OpenIdClient {
|
||||||
|
client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenIdClient {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
OpenIdClient {
|
||||||
|
client: reqwest::ClientBuilder::new()
|
||||||
|
.default_headers(constants::token::headers())
|
||||||
|
.user_agent(constants::token::user_agent())
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn token_refresh(
|
||||||
|
&self,
|
||||||
|
refresh_token: &RefreshToken,
|
||||||
|
) -> LibraryResult<TokenResponse> {
|
||||||
|
let req = self
|
||||||
|
.client
|
||||||
|
.post(constants::token::endpoint())
|
||||||
|
.form(constants::token::refresh_token_form(refresh_token.as_str()).as_slice())
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let res = self.client.execute(req).await;
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(res) => {
|
||||||
|
let res = res.text().await.unwrap();
|
||||||
|
let res = serde_json::from_str::<APIResult<TokenResponse>>(&res);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(res) => return res.into(),
|
||||||
|
Err(err) => Err(LibraryError::from(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => Err(LibraryError::from(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn token_authorization(
|
||||||
|
&self,
|
||||||
|
authorization_code: String,
|
||||||
|
code_verfier: &CodeVerfier,
|
||||||
|
) -> LibraryResult<TokenResponse> {
|
||||||
|
let mut req = self
|
||||||
|
.client
|
||||||
|
.post(constants::token::endpoint())
|
||||||
|
.form(
|
||||||
|
constants::token::authorization_code_form(authorization_code, code_verfier)
|
||||||
|
.as_slice(),
|
||||||
|
)
|
||||||
|
.header("Host", "login.dhl.de")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Accept-Encoding", "gzip")
|
||||||
|
.header("User-Agent", constants::token::user_agent())
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
let len = req.body().unwrap().as_bytes().unwrap().len();
|
||||||
|
let headermap = req.headers_mut();
|
||||||
|
headermap.append("Content-Length", len.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = self.client.execute(req).await;
|
||||||
|
|
||||||
|
println!("auth_code: {:?}", res);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(res) => {
|
||||||
|
let res = res.text().await.unwrap();
|
||||||
|
println!("auth_code reply: {:?}", res);
|
||||||
|
let res = serde_json::from_str::<APIResult<TokenResponse>>(&res);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(res) => res.into(),
|
||||||
|
Err(err) => Err(LibraryError::from(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => Err(LibraryError::from(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
libpaket/src/login/openid_response.rs
Normal file
23
libpaket/src/login/openid_response.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_newtype::newtype;
|
||||||
|
|
||||||
|
use super::DHLIdToken;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct TokenResponse {
|
||||||
|
access_token: String,
|
||||||
|
expires_in: u64,
|
||||||
|
pub id_token: DHLIdToken,
|
||||||
|
pub refresh_token: RefreshToken,
|
||||||
|
scope: String,
|
||||||
|
token_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: sanity checks
|
||||||
|
newtype! {
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub RefreshToken: String[|_k| true,
|
||||||
|
"",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
impl secrecy::SerializableSecret for RefreshToken {}
|
121
libpaket/src/login/openid_token.rs
Normal file
121
libpaket/src/login/openid_token.rs
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
|
use serde::{
|
||||||
|
de::{DeserializeOwned, Visitor},
|
||||||
|
Deserialize, Serialize,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct Claims<CustomClaims> {
|
||||||
|
// only on first time login
|
||||||
|
pub nonce: Option<String>,
|
||||||
|
auth_time: u64,
|
||||||
|
pub iat: u64,
|
||||||
|
pub exp: u64,
|
||||||
|
pub aud: Vec<String>,
|
||||||
|
pub sub: String,
|
||||||
|
pub iss: String,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub custom: CustomClaims,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct OpenIDToken<CustomClaims> {
|
||||||
|
token: String,
|
||||||
|
|
||||||
|
pub claims: Claims<CustomClaims>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> secrecy::SerializableSecret for OpenIDToken<T> {}
|
||||||
|
|
||||||
|
impl<T> OpenIDToken<T> {
|
||||||
|
pub fn has_nonce(&self) -> bool {
|
||||||
|
self.claims.nonce.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_nonce(&self, nonce: String) -> bool {
|
||||||
|
if let Some(claims_nonce) = &self.claims.nonce {
|
||||||
|
return claims_nonce.as_str() == nonce.as_str();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_string(&self) -> String {
|
||||||
|
self.token.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
self.token.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
// requires valid system time
|
||||||
|
pub fn is_expired(&self) -> bool {
|
||||||
|
let duration: Result<std::time::Duration, std::time::SystemTimeError> = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH);
|
||||||
|
|
||||||
|
let Ok(duration) = duration else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// we artificaly shorten the expiary period, as we don't want it to happen during an api call
|
||||||
|
duration.as_secs() > self.claims.exp - 30
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expire_time(&self) -> u64 {
|
||||||
|
self.claims.exp - 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Serialize for OpenIDToken<T> {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(self.token.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de, T: DeserializeOwned> Deserialize<'de> for OpenIDToken<T> {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
deserializer.deserialize_string(OpenIDTokenVisitor {
|
||||||
|
phantom_data: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OpenIDTokenVisitor<T> {
|
||||||
|
phantom_data: PhantomData<T>,
|
||||||
|
}
|
||||||
|
impl<'de, T> Visitor<'de> for OpenIDTokenVisitor<T>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned,
|
||||||
|
{
|
||||||
|
type Value = OpenIDToken<T>;
|
||||||
|
|
||||||
|
fn expecting(&self, _: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<'a, E>(self, s: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
|
||||||
|
let splits = s.split_terminator(".").collect::<Vec<&str>>();
|
||||||
|
let claims = serde_json::from_slice::<Claims<T>>(
|
||||||
|
general_purpose::URL_SAFE_NO_PAD
|
||||||
|
.decode(splits[1])
|
||||||
|
.unwrap()
|
||||||
|
.as_slice(),
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
Ok(OpenIDToken {
|
||||||
|
token: s.to_string(),
|
||||||
|
claims: claims,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
41
libpaket/src/login/utils.rs
Normal file
41
libpaket/src/login/utils.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
pub fn create_nonce() -> String {
|
||||||
|
let charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-";
|
||||||
|
|
||||||
|
random_string::generate(22, charset)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Clone)]
|
||||||
|
pub struct CodeVerfier {
|
||||||
|
client_secret: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CodeVerfier {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-";
|
||||||
|
|
||||||
|
CodeVerfier {
|
||||||
|
client_secret: random_string::generate(86, charset),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sha256(&self) -> Vec<u8> {
|
||||||
|
let hash = {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(self.client_secret.as_bytes());
|
||||||
|
hasher.finalize()
|
||||||
|
};
|
||||||
|
|
||||||
|
hash.to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn code_challenge(&self) -> String {
|
||||||
|
general_purpose::URL_SAFE_NO_PAD.encode(self.sha256())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn code_verfier(&self) -> String {
|
||||||
|
self.client_secret.clone()
|
||||||
|
}
|
||||||
|
}
|
152
libpaket/src/stammdaten.rs
Normal file
152
libpaket/src/stammdaten.rs
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
use reqwest::{header::HeaderMap, Request, RequestBuilder};
|
||||||
|
|
||||||
|
use crate::www::authorized_credentials;
|
||||||
|
use crate::constants::{app_version, linux_android_version};
|
||||||
|
use crate::common::APIResult;
|
||||||
|
use crate::{login::DHLIdToken, LibraryResult};
|
||||||
|
|
||||||
|
pub struct StammdatenClient {
|
||||||
|
pub(crate) client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Parse more status codes and return LibraryErrors
|
||||||
|
impl StammdatenClient {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
StammdatenClient {
|
||||||
|
client: reqwest::ClientBuilder::new()
|
||||||
|
.default_headers(headers())
|
||||||
|
.user_agent(user_agent())
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn base_request(&self, request_builder: RequestBuilder, dhli: &DHLIdToken) -> Request {
|
||||||
|
request_builder
|
||||||
|
.basic_auth(
|
||||||
|
authorized_credentials().0,
|
||||||
|
Some(authorized_credentials().1),
|
||||||
|
)
|
||||||
|
.headers(headers())
|
||||||
|
.header("cookie", format!("dhli={}", dhli.as_str()))
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn customer_data(&self, dhli: &DHLIdToken) -> LibraryResult<CustomerData> {
|
||||||
|
let req = self.base_request(self.client.get(endpoint_customer_data()), dhli);
|
||||||
|
|
||||||
|
let res = self.client.execute(req).await;
|
||||||
|
|
||||||
|
if let Err(err) = res {
|
||||||
|
return Err(err.into());
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = res.unwrap().text().await.unwrap();
|
||||||
|
let res = serde_json::from_str::<APIResult<CustomerData>>(&res);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(res) => res.into(),
|
||||||
|
Err(err) => Err(err.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn customer_data_full(&self, dhli: &DHLIdToken) -> LibraryResult<CustomerDataFull> {
|
||||||
|
let req = self.base_request(self.client.get(endpoint_customer_master_data()), dhli);
|
||||||
|
|
||||||
|
let res = self.client.execute(req).await;
|
||||||
|
|
||||||
|
if let Err(err) = res {
|
||||||
|
return Err(err.into());
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = res.unwrap().text().await.unwrap();
|
||||||
|
let res = serde_json::from_str::<APIResult<CustomerDataFull>>(&res);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(res) => res.into(),
|
||||||
|
Err(err) => Err(err.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_agent() -> String {
|
||||||
|
format!(
|
||||||
|
"okhttp/4.11.0 Post & DHL/{} ({})",
|
||||||
|
app_version(),
|
||||||
|
linux_android_version()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn headers() -> HeaderMap {
|
||||||
|
let aaa = vec![
|
||||||
|
("x-api-key", "zAuoC3%7*qbRVmiXdNGyYz9iJ7N@Ph3Cw4zV"),
|
||||||
|
/* authorization: Basic base dhl.de http credentials */
|
||||||
|
/* cookie: dhli= */
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut map = HeaderMap::new();
|
||||||
|
for bbb in aaa {
|
||||||
|
map.append(bbb.0, bbb.1.parse().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
// needs authorized
|
||||||
|
fn endpoint_customer_data() -> &'static str {
|
||||||
|
"https://www.dhl.de/int-stammdaten/public/customerData"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn endpoint_customer_master_data() -> &'static str {
|
||||||
|
"https://www.dhl.de/int-stammdaten/public/customerMasterData"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug)]
|
||||||
|
pub struct CustomerData {
|
||||||
|
//dataConfirmationRequired
|
||||||
|
#[serde(rename = "displayName")]
|
||||||
|
pub display_name: String,
|
||||||
|
#[serde(rename = "emailAddress")]
|
||||||
|
pub email_address: String,
|
||||||
|
#[serde(rename = "postNumber")]
|
||||||
|
pub post_number: String,
|
||||||
|
pub services: Vec<CustomerDataService>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug)]
|
||||||
|
pub struct CustomerAddress {
|
||||||
|
pub city: String,
|
||||||
|
pub country: Option<String>,
|
||||||
|
#[serde(rename = "houseNumber")]
|
||||||
|
pub house_number: String,
|
||||||
|
#[serde(rename = "postalCode")]
|
||||||
|
pub postal_code: String,
|
||||||
|
pub street: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug)]
|
||||||
|
pub enum CustomerDataService {
|
||||||
|
#[serde(rename = "PACKSTATION")]
|
||||||
|
Packstation,
|
||||||
|
#[serde(rename = "PAKETANKUENDIGUNG")]
|
||||||
|
Paketankuendigung,
|
||||||
|
#[serde(rename = "POSTFILIALE_DIREKT")]
|
||||||
|
PostfilialeDirekt,
|
||||||
|
#[serde(rename = "DIGIBEN")]
|
||||||
|
Digiben,
|
||||||
|
#[serde(rename = "GERAET_AKTIVIERT")]
|
||||||
|
GeraetAktiviert,
|
||||||
|
#[serde(rename = "BRIEFANKUENDIGUNG")]
|
||||||
|
Briefankuendigung,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug)]
|
||||||
|
pub struct CustomerDataFull {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub common: CustomerData,
|
||||||
|
|
||||||
|
pub address: CustomerAddress,
|
||||||
|
}
|
75
libpaket/src/utils.rs
Normal file
75
libpaket/src/utils.rs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
use crate::login::DHLIdToken;
|
||||||
|
|
||||||
|
pub(crate) fn as_url(input: &str) -> url::Url {
|
||||||
|
url::Url::parse(input).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct CookieHeaderValueBuilder {
|
||||||
|
list: Vec<(String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CookieHeaderValueBuilder {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
CookieHeaderValueBuilder { list: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_dhli(mut self, dhli: &DHLIdToken) -> Self {
|
||||||
|
self.list.push(("dhli".to_string(), dhli.to_string()));
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_dhlcs(mut self, dhli: &DHLIdToken) -> Self {
|
||||||
|
self.list.push(("dhlcs".to_string(), dhli.get_dhlcs().to_string()));
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_string(self) -> String {
|
||||||
|
let name_value = self
|
||||||
|
.list
|
||||||
|
.iter()
|
||||||
|
.map(|(name, value)| name.clone().to_owned() + "=" + value);
|
||||||
|
name_value.collect::<Vec<String>>().join("; ")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_headermap(self) -> reqwest::header::HeaderMap {
|
||||||
|
let mut map = reqwest::header::HeaderMap::new();
|
||||||
|
|
||||||
|
self.list.iter().for_each(|(name, value)| {
|
||||||
|
map.append(
|
||||||
|
"cookie",
|
||||||
|
reqwest::header::HeaderValue::from_str(format!("{}={}", name, value).as_str())
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
map
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: use crate `tracing`
|
||||||
|
macro_rules! mini_assert_api_eq {
|
||||||
|
($a:expr, $b:expr) => {
|
||||||
|
if $a != $b {
|
||||||
|
return Err(crate::LibraryError::APIChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! mini_assert_api {
|
||||||
|
($a: expr) => {
|
||||||
|
if !$a {
|
||||||
|
return Err(crate::LibraryError::APIChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
macro_rules! mini_assert_inval {
|
||||||
|
($a: expr) => {
|
||||||
|
if !$a {
|
||||||
|
return Err(crate::LibraryError::InvalidArgument(format!("MiniAssert failed: $a")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
50
libpaket/src/www.rs
Normal file
50
libpaket/src/www.rs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
use reqwest::header::HeaderMap;
|
||||||
|
|
||||||
|
pub struct WebClient {
|
||||||
|
pub(crate) web_client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebClient {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
WebClient {
|
||||||
|
web_client: reqwest::ClientBuilder::new()
|
||||||
|
.default_headers(web_headers())
|
||||||
|
.user_agent(crate::constants::webview_user_agent())
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn web_headers() -> HeaderMap {
|
||||||
|
let aaa = vec![
|
||||||
|
("accept", "application/json"),
|
||||||
|
("pragma", "no-cache"),
|
||||||
|
("cache-control", "no-cache,no-store,must-revalidate"),
|
||||||
|
("accept-language", "de"),
|
||||||
|
("expires", "0"),
|
||||||
|
("x-requested-with", "de.dhl.paket"),
|
||||||
|
("sec-fetch-site", "same-origin"),
|
||||||
|
("sec-fetch-mode", "cors"),
|
||||||
|
(
|
||||||
|
"referer",
|
||||||
|
"https://www.dhl.de/int-static/pdapp/spa/prod/ver5-SPA-VERFOLGEN.html",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut map = HeaderMap::new();
|
||||||
|
for bbb in aaa {
|
||||||
|
map.append(bbb.0, bbb.1.parse().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn authorized_credentials() -> (&'static str, &'static str) {
|
||||||
|
("erkennen", "8XRUfutM8PTvUz3A")
|
||||||
|
}
|
||||||
|
|
||||||
|
// "/int-push/", "X-APIKey", "5{@8*nB=?\\.@t,XwWK>Y[=yY^*Y8&myzDE7_"
|
||||||
|
// /int-stammdaten/", null, "zAuoC3%7*qbRVmiXdNGyYz9iJ7N@Ph3Cw4zV"
|
||||||
|
// "/int-verfolgen/data/packstation/v2/", null, "a0d5b9049ba8918871e6e20bd5c49974",
|
||||||
|
// "/", null, "a0d5b9049ba8918871e6e20bd5c49974",
|
18
paket/Cargo.toml
Normal file
18
paket/Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "paket"
|
||||||
|
authors.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
relm4 = { version = "0.9" , features = [ "libadwaita", "macros" ], path = "/home/jane400/sources/alpine/Relm4/relm4" }
|
||||||
|
relm4-components = { version = "0.9", path = "/home/jane400/sources/alpine/Relm4/relm4-components" }
|
||||||
|
relm4-macros = { version = "0.9", path = "/home/jane400/sources/alpine/Relm4/relm4-macros" }
|
||||||
|
tracker = "0.2"
|
||||||
|
adw = {package = "libadwaita", version = "0.7", features = [ "v1_5" ]}
|
||||||
|
webkit = { package = "webkit6", version = "0.4" }
|
||||||
|
reqwest = "0.12"
|
||||||
|
libpaket = { path = "../libpaket" }
|
||||||
|
glycin = { version = "2.0.0-beta", features = ["gdk4"] }
|
||||||
|
oo7 = { version = "0.3" }
|
106
paket/src/advices.rs
Normal file
106
paket/src/advices.rs
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
use libpaket::advices::UatToken;
|
||||||
|
use libpaket::LibraryError;
|
||||||
|
use relm4::gtk;
|
||||||
|
use gtk::gdk;
|
||||||
|
use adw::{gio, glib};
|
||||||
|
|
||||||
|
use relm4::prelude::*;
|
||||||
|
use adw::prelude::*;
|
||||||
|
use glib::prelude::*;
|
||||||
|
use gio::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AppAdviceMetadata {
|
||||||
|
pub date: String,
|
||||||
|
pub advice: libpaket::advices::Advice,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracker::track]
|
||||||
|
pub struct AppAdvice {
|
||||||
|
#[do_not_track]
|
||||||
|
metadata: AppAdviceMetadata,
|
||||||
|
texture: Option<gdk::Texture>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AppAdviceCmds {
|
||||||
|
GotTexture(gdk::Texture),
|
||||||
|
Error(LibraryError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[relm4::factory(pub)]
|
||||||
|
impl FactoryComponent for AppAdvice {
|
||||||
|
type Init = (AppAdviceMetadata, UatToken);
|
||||||
|
type Input = ();
|
||||||
|
type Output = ();
|
||||||
|
type CommandOutput = AppAdviceCmds;
|
||||||
|
type ParentWidget = adw::Carousel;
|
||||||
|
|
||||||
|
view! {
|
||||||
|
#[root]
|
||||||
|
gtk::Overlay {
|
||||||
|
add_overlay = >k::Spinner {
|
||||||
|
start: (),
|
||||||
|
set_align: gtk::Align::Center,
|
||||||
|
|
||||||
|
#[track(self.changed_texture())]
|
||||||
|
set_visible: self.texture.is_none(),
|
||||||
|
},
|
||||||
|
|
||||||
|
add_overlay = >k::Label {
|
||||||
|
set_halign: gtk::Align::Center,
|
||||||
|
set_valign: gtk::Align::Center,
|
||||||
|
|
||||||
|
set_label: self.metadata.date.as_str(),
|
||||||
|
},
|
||||||
|
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = >k::Picture {
|
||||||
|
#[track(self.changed_texture())]
|
||||||
|
set_paintable: self.texture.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_model(value: Self::Init, _index: &DynamicIndex, sender: FactorySender<Self>) -> Self {
|
||||||
|
let _self = Self {
|
||||||
|
metadata: value.0,
|
||||||
|
texture: None,
|
||||||
|
tracker: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let advice = _self.metadata.advice.clone();
|
||||||
|
let uat = value.1;
|
||||||
|
|
||||||
|
sender.oneshot_command(async move {
|
||||||
|
let res = libpaket::advices::AdviceClient::new().fetch_advice_image(&advice, &uat).await;
|
||||||
|
|
||||||
|
let res = match res {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(err) => return AppAdviceCmds::Error(err),
|
||||||
|
};
|
||||||
|
|
||||||
|
let file = {
|
||||||
|
let (file, io_stream) = gio::File::new_tmp(None::<&std::path::Path>).unwrap();
|
||||||
|
let output_stream = io_stream.output_stream();
|
||||||
|
output_stream.write(res.as_slice(), None::<&gio::Cancellable>).unwrap();
|
||||||
|
file
|
||||||
|
};
|
||||||
|
|
||||||
|
let image = glycin::Loader::new(file).load().await.expect("Image decoding failed");
|
||||||
|
let frame = image.next_frame().await.expect("Image frame decoding failed");
|
||||||
|
|
||||||
|
AppAdviceCmds::GotTexture(frame.texture())
|
||||||
|
});
|
||||||
|
|
||||||
|
_self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_cmd(&mut self, message: Self::CommandOutput, sender: FactorySender<Self>) {
|
||||||
|
match message {
|
||||||
|
AppAdviceCmds::GotTexture(texture) => self.set_texture(Some(texture)),
|
||||||
|
AppAdviceCmds::Error(err) => todo!()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
1
paket/src/constants.rs
Normal file
1
paket/src/constants.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub const APP_ID: &str = "de.j4ne.Paket";
|
384
paket/src/login.rs
Normal file
384
paket/src/login.rs
Normal file
|
@ -0,0 +1,384 @@
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
collections::HashMap,
|
||||||
|
sync::{Arc, OnceLock},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use adw::prelude::*;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use libpaket::{
|
||||||
|
login::{create_nonce, CodeVerfier, DHLIdToken, RefreshToken, TokenResponse},
|
||||||
|
LibraryError, LibraryResult, OpenIdClient,
|
||||||
|
};
|
||||||
|
use relm4::{
|
||||||
|
adw, gtk,
|
||||||
|
prelude::*,
|
||||||
|
tokio::{sync::Mutex, time::sleep},
|
||||||
|
AsyncComponentSender, SharedState,
|
||||||
|
};
|
||||||
|
use webkit::{prelude::WebViewExt, URIRequest, WebContext, WebView};
|
||||||
|
|
||||||
|
static KEYRING: OnceLock<oo7::Keyring> = OnceLock::new();
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum LoginInput {
|
||||||
|
NeedsLogin,
|
||||||
|
NeedsRefresh,
|
||||||
|
ReceivedAuthCode(String),
|
||||||
|
BreakWorld,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum LoginState {
|
||||||
|
InFlow,
|
||||||
|
Offline,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LoginFlowModel {
|
||||||
|
code_verifier: CodeVerfier,
|
||||||
|
nonce: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type LoginSharedState = Arc<Mutex<Arc<SharedState<Option<DHLIdToken>>>>>;
|
||||||
|
|
||||||
|
pub async fn get_id_token(value: &LoginSharedState) -> Option<DHLIdToken> {
|
||||||
|
let mutex_guard = value.lock().await;
|
||||||
|
let shared_state_guard = mutex_guard.read();
|
||||||
|
shared_state_guard.as_ref().cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracker::track]
|
||||||
|
pub struct Login {
|
||||||
|
#[do_not_track]
|
||||||
|
shared_id_token: LoginSharedState,
|
||||||
|
#[do_not_track]
|
||||||
|
refresh_token: Option<RefreshToken>,
|
||||||
|
#[do_not_track]
|
||||||
|
flow_model: RefCell<Option<LoginFlowModel>>,
|
||||||
|
|
||||||
|
state: LoginState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum LoginOutput {
|
||||||
|
RequiresLogin,
|
||||||
|
RequiresLoading,
|
||||||
|
NetworkFail,
|
||||||
|
Error(libpaket::LibraryError),
|
||||||
|
KeyringError(oo7::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum LoginCommand {
|
||||||
|
Token(LibraryResult<TokenResponse>),
|
||||||
|
NeedsRefresh,
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEYRING_ATTRIBUTES: [(&str, &str); 2] =
|
||||||
|
[("app", crate::constants::APP_ID), ("type", "refresh_token")];
|
||||||
|
|
||||||
|
#[relm4::component(async, pub)]
|
||||||
|
impl AsyncComponent for Login {
|
||||||
|
type Init = LoginSharedState;
|
||||||
|
type Input = LoginInput;
|
||||||
|
type Output = LoginOutput;
|
||||||
|
type CommandOutput = LoginCommand;
|
||||||
|
|
||||||
|
view! {
|
||||||
|
#[root]
|
||||||
|
adw::Bin {
|
||||||
|
#[wrap(Some)]
|
||||||
|
#[name = "webview"]
|
||||||
|
set_child = &WebView::builder().web_context(&webcontext).build() {
|
||||||
|
#[track(model.changed(Self::state()))]
|
||||||
|
set_visible: model.state == LoginState::InFlow,
|
||||||
|
|
||||||
|
|
||||||
|
#[track(model.changed_state() && model.state == LoginState::InFlow)]
|
||||||
|
load_request?: &model.construct_request_uri(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init(
|
||||||
|
init: Self::Init,
|
||||||
|
root: Self::Root,
|
||||||
|
sender: AsyncComponentSender<Self>,
|
||||||
|
) -> AsyncComponentParts<Self> {
|
||||||
|
// TODO: Make keyring creation failure less fatal :(
|
||||||
|
let mut model: Login = Login {
|
||||||
|
shared_id_token: init,
|
||||||
|
flow_model: RefCell::new(None),
|
||||||
|
refresh_token: None,
|
||||||
|
state: LoginState::Offline,
|
||||||
|
tracker: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let result = oo7::Keyring::new().await;
|
||||||
|
match result {
|
||||||
|
Ok(keyring) => {
|
||||||
|
KEYRING.set(keyring).unwrap();
|
||||||
|
{
|
||||||
|
let keyring = KEYRING.get().unwrap();
|
||||||
|
if let Err(err) = keyring.unlock().await {
|
||||||
|
sender
|
||||||
|
.output(LoginOutput::KeyringError(err))
|
||||||
|
.expect("sender not worky");
|
||||||
|
} else {
|
||||||
|
match keyring
|
||||||
|
.search_items(&HashMap::from(KEYRING_ATTRIBUTES))
|
||||||
|
.await {
|
||||||
|
Ok(res) => {
|
||||||
|
if res.len() > 0 {
|
||||||
|
let item = &res[0];
|
||||||
|
let refresh_token = item.secret().await.unwrap();
|
||||||
|
let refresh_token = std::str::from_utf8(refresh_token.as_slice()).unwrap();
|
||||||
|
model.refresh_token = Some(RefreshToken::new(refresh_token.to_string()).unwrap());
|
||||||
|
sender.input(LoginInput::NeedsRefresh);
|
||||||
|
} else {
|
||||||
|
sender.input(LoginInput::NeedsLogin);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
sender
|
||||||
|
.output(LoginOutput::KeyringError(err))
|
||||||
|
.expect("sender not worky");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
sender
|
||||||
|
.output(LoginOutput::KeyringError(err))
|
||||||
|
.expect("sender not worky");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let webcontext = WebContext::builder().build();
|
||||||
|
{
|
||||||
|
let sender = sender.clone();
|
||||||
|
webcontext.register_uri_scheme("dhllogin", move |req| {
|
||||||
|
let uri = req.uri().unwrap();
|
||||||
|
let uri = reqwest::Url::parse(uri.as_str()).unwrap();
|
||||||
|
for (name, value) in uri.query_pairs() {
|
||||||
|
if name == "code" {
|
||||||
|
sender.input(LoginInput::ReceivedAuthCode(value.to_string()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sender.input(LoginInput::BreakWorld);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let widgets = view_output!();
|
||||||
|
|
||||||
|
let settings = WebViewExt::settings(&widgets.webview).unwrap();
|
||||||
|
settings.set_enable_developer_extras(true);
|
||||||
|
settings.set_user_agent(Some(
|
||||||
|
libpaket::login::constants::webbrowser_authorize::user_agent().as_str(),
|
||||||
|
));
|
||||||
|
|
||||||
|
AsyncComponentParts { model, widgets }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(
|
||||||
|
&mut self,
|
||||||
|
message: Self::Input,
|
||||||
|
sender: AsyncComponentSender<Self>,
|
||||||
|
_: &Self::Root,
|
||||||
|
) {
|
||||||
|
self.reset();
|
||||||
|
|
||||||
|
match message {
|
||||||
|
LoginInput::NeedsRefresh => {
|
||||||
|
let refresh_token = self.refresh_token.as_ref().unwrap().clone();
|
||||||
|
sender.oneshot_command(async { use_refresh_token(refresh_token).await })
|
||||||
|
}
|
||||||
|
LoginInput::ReceivedAuthCode(auth_code) => {
|
||||||
|
self.set_state(LoginState::Offline);
|
||||||
|
sender.output(LoginOutput::RequiresLoading).unwrap();
|
||||||
|
let model = self.flow_model.borrow();
|
||||||
|
let code_verifier = model.as_ref().unwrap().code_verifier.clone();
|
||||||
|
sender
|
||||||
|
.oneshot_command(async { received_auth_code(auth_code, code_verifier).await });
|
||||||
|
}
|
||||||
|
LoginInput::BreakWorld => {
|
||||||
|
self.set_state(LoginState::Offline);
|
||||||
|
sender.output(LoginOutput::Error(libpaket::LibraryError::APIChange)).unwrap();
|
||||||
|
{
|
||||||
|
let shared_id_token = self.shared_id_token.lock().await;
|
||||||
|
let mut shared_id_token = shared_id_token.write();
|
||||||
|
*shared_id_token = None;
|
||||||
|
}
|
||||||
|
self.flow_model.replace(None);
|
||||||
|
}
|
||||||
|
LoginInput::NeedsLogin => {
|
||||||
|
self.set_state(LoginState::InFlow);
|
||||||
|
sender.output(LoginOutput::RequiresLogin).unwrap();
|
||||||
|
self.flow_model.replace(Some(LoginFlowModel {
|
||||||
|
code_verifier: CodeVerfier::new(),
|
||||||
|
nonce: create_nonce(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_cmd(
|
||||||
|
&mut self,
|
||||||
|
message: Self::CommandOutput,
|
||||||
|
sender: AsyncComponentSender<Self>,
|
||||||
|
_: &Self::Root,
|
||||||
|
) {
|
||||||
|
match message {
|
||||||
|
LoginCommand::Token(res) => {
|
||||||
|
self.send_library_response(res, sender).await;
|
||||||
|
}
|
||||||
|
LoginCommand::NeedsRefresh => {
|
||||||
|
sender.input(LoginInput::NeedsRefresh);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
enum ResponseType {
|
||||||
|
Retry,
|
||||||
|
Okay,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Login {
|
||||||
|
fn construct_request_uri(&self) -> Option<URIRequest> {
|
||||||
|
if self.state != LoginState::InFlow {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let borrow = self.flow_model.borrow();
|
||||||
|
let model = borrow.as_ref().unwrap();
|
||||||
|
let uri = libpaket::login::constants::webbrowser_authorize::build_endpoint(
|
||||||
|
&model.nonce,
|
||||||
|
&model.code_verifier,
|
||||||
|
);
|
||||||
|
Some(URIRequest::new(uri.as_str()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_library_response(
|
||||||
|
&mut self,
|
||||||
|
res: LibraryResult<TokenResponse>,
|
||||||
|
sender: AsyncComponentSender<Login>,
|
||||||
|
) {
|
||||||
|
match res {
|
||||||
|
Ok(res) => {
|
||||||
|
{
|
||||||
|
let id_token = res.id_token.clone();
|
||||||
|
sender.command(|out, shutdown| {
|
||||||
|
shutdown
|
||||||
|
.register(async move {
|
||||||
|
let unix_target_time = id_token.expire_time();
|
||||||
|
|
||||||
|
let duration = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap();
|
||||||
|
let duration = unix_target_time - duration.as_secs();
|
||||||
|
if duration > 0 {
|
||||||
|
let duration = Duration::from_secs(duration);
|
||||||
|
sleep(duration).await;
|
||||||
|
}
|
||||||
|
out.emit(LoginCommand::NeedsRefresh)
|
||||||
|
})
|
||||||
|
.drop_on_shutdown()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let future = async {
|
||||||
|
self.refresh_token = Some(res.refresh_token);
|
||||||
|
let keyring = KEYRING.get().unwrap();
|
||||||
|
keyring.create_item("Refresh Token", &HashMap::from(KEYRING_ATTRIBUTES), self.refresh_token.as_ref().unwrap().to_string(), true).await.unwrap();
|
||||||
|
};
|
||||||
|
|
||||||
|
if !res.id_token.is_expired() {
|
||||||
|
let credentials_model = self.shared_id_token.lock().await;
|
||||||
|
let mut credentials_model = credentials_model.write();
|
||||||
|
|
||||||
|
*credentials_model = Some(res.id_token);
|
||||||
|
}
|
||||||
|
future.await;
|
||||||
|
}
|
||||||
|
Err(res) => {
|
||||||
|
// We disarm the webkit flow/aka breaking the application. We want to reduce invalid requests
|
||||||
|
match res {
|
||||||
|
libpaket::LibraryError::APIChange => {
|
||||||
|
sender.input(LoginInput::BreakWorld);
|
||||||
|
}
|
||||||
|
libpaket::LibraryError::InvalidArgument(_) => {
|
||||||
|
panic!("{}", res);
|
||||||
|
}
|
||||||
|
libpaket::LibraryError::NetworkFetch => {
|
||||||
|
sender.output(LoginOutput::NetworkFail).unwrap();
|
||||||
|
}
|
||||||
|
libpaket::LibraryError::DecodeError(_) => {
|
||||||
|
sender.output(LoginOutput::Error(res)).unwrap();
|
||||||
|
}
|
||||||
|
libpaket::LibraryError::Unauthorized => {
|
||||||
|
sender.input(LoginInput::NeedsLogin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_library_error_to_response_type(err: &LibraryError) -> ResponseType {
|
||||||
|
match err {
|
||||||
|
LibraryError::NetworkFetch => ResponseType::Retry,
|
||||||
|
LibraryError::Unauthorized => ResponseType::Okay,
|
||||||
|
LibraryError::InvalidArgument(_) => ResponseType::Okay,
|
||||||
|
LibraryError::DecodeError(_) => ResponseType::Okay,
|
||||||
|
LibraryError::APIChange => ResponseType::Okay,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn received_auth_code(auth_code: String, code_verifier: CodeVerfier) -> LoginCommand {
|
||||||
|
let client = OpenIdClient::new();
|
||||||
|
let mut err = LibraryError::NetworkFetch;
|
||||||
|
for _ in 0..6 {
|
||||||
|
let result: Result<TokenResponse, LibraryError> = client
|
||||||
|
.token_authorization(auth_code.clone(), &code_verifier)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Ok(result) = result {
|
||||||
|
return LoginCommand::Token(Ok(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = result.unwrap_err();
|
||||||
|
let response_type = convert_library_error_to_response_type(&err);
|
||||||
|
if response_type == ResponseType::Retry {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
return LoginCommand::Token(Err(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LoginCommand::Token(Err(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn use_refresh_token(refresh_token: RefreshToken) -> LoginCommand {
|
||||||
|
let client = OpenIdClient::new();
|
||||||
|
let mut err = LibraryError::NetworkFetch;
|
||||||
|
for _ in 0..6 {
|
||||||
|
let result: Result<TokenResponse, LibraryError> =
|
||||||
|
client.token_refresh(&refresh_token).await;
|
||||||
|
|
||||||
|
err = match result {
|
||||||
|
Ok(result) => return LoginCommand::Token(Ok(result)),
|
||||||
|
Err(err) => err,
|
||||||
|
};
|
||||||
|
let response_type = convert_library_error_to_response_type(&err);
|
||||||
|
if response_type == ResponseType::Retry {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
return LoginCommand::Token(Err(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LoginCommand::Token(Err(err))
|
||||||
|
}
|
292
paket/src/main.rs
Normal file
292
paket/src/main.rs
Normal file
|
@ -0,0 +1,292 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use login::{Login, LoginOutput, LoginSharedState};
|
||||||
|
use ready::{Ready, ReadyOutput};
|
||||||
|
use relm4::{
|
||||||
|
adw, gtk, main_adw_application, prelude::*, tokio::sync::Mutex,
|
||||||
|
AsyncComponentSender, SharedState,
|
||||||
|
};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use adw::{glib, prelude::*};
|
||||||
|
|
||||||
|
mod advices;
|
||||||
|
mod constants;
|
||||||
|
mod login;
|
||||||
|
mod ready;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
enum AppState {
|
||||||
|
Loading,
|
||||||
|
RequiresLogIn,
|
||||||
|
FatalError,
|
||||||
|
Ready,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct AppError {
|
||||||
|
short: String,
|
||||||
|
long: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum AppInput {
|
||||||
|
ErrorOccoured(AppError),
|
||||||
|
FatalErrorOccoured(AppError),
|
||||||
|
SwitchToLogin,
|
||||||
|
SwitchToLoading,
|
||||||
|
SwitchToReady,
|
||||||
|
NetworkFail,
|
||||||
|
Notification(String, u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracker::track]
|
||||||
|
struct App {
|
||||||
|
state: AppState,
|
||||||
|
_network_fail: bool,
|
||||||
|
|
||||||
|
#[do_not_track]
|
||||||
|
login: AsyncController<Login>,
|
||||||
|
#[do_not_track]
|
||||||
|
ready: Controller<Ready>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[relm4::component(async)]
|
||||||
|
impl AsyncComponent for App {
|
||||||
|
type Input = AppInput;
|
||||||
|
type Output = ();
|
||||||
|
type Init = ();
|
||||||
|
type CommandOutput = ();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
#[root]
|
||||||
|
main_window = adw::ApplicationWindow::new(&main_adw_application()) {
|
||||||
|
add_breakpoint = adw::Breakpoint::new(
|
||||||
|
adw::BreakpointCondition::new_length(adw::BreakpointConditionLengthType::MaxWidth, 550.0, adw::LengthUnit::Sp)
|
||||||
|
) {
|
||||||
|
add_setter: (&ready_headerbar, "show-title", Some(&glib::Value::from(false))),
|
||||||
|
add_setter: (&ready_switcherbar, "reveal", Some(&glib::Value::from(true)))
|
||||||
|
},
|
||||||
|
|
||||||
|
set_default_height: 600,
|
||||||
|
set_default_width: 800,
|
||||||
|
set_width_request: 300,
|
||||||
|
set_height_request: 300,
|
||||||
|
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_content = &adw::ViewStack {
|
||||||
|
#[name = "page_prepare"]
|
||||||
|
add = &adw::ToolbarView {
|
||||||
|
add_top_bar = &adw::HeaderBar {},
|
||||||
|
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_content = prepare_toast_overlay = &adw::ToastOverlay {
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = &adw::ViewStack {
|
||||||
|
#[name = "page_loading"]
|
||||||
|
add = &adw::Bin {
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = >k::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
|
||||||
|
gtk::Spinner {
|
||||||
|
start: (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Will be filled in init with a webkit */
|
||||||
|
#[local_ref]
|
||||||
|
add = page_login -> adw::Bin {},
|
||||||
|
|
||||||
|
#[name = "page_fatal"]
|
||||||
|
add = &adw::Bin {
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = fatal_status_page = &adw::StatusPage {
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
#[track(model.changed(App::state()) && model.state != AppState::Ready)]
|
||||||
|
set_visible_child: {
|
||||||
|
let page: &adw::Bin = match model.state {
|
||||||
|
AppState::Loading => page_loading.as_ref(),
|
||||||
|
AppState::RequiresLogIn => page_login.as_ref(),
|
||||||
|
AppState::FatalError => page_fatal.as_ref(),
|
||||||
|
AppState::Ready => panic!(),
|
||||||
|
};
|
||||||
|
page
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
#[name = "page_ready"]
|
||||||
|
add = &adw::Bin {
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = &adw::NavigationView {
|
||||||
|
add = &adw::NavigationPage {
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = &adw::ToolbarView {
|
||||||
|
add_top_bar = ready_headerbar = &adw::HeaderBar {
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_title_widget = ready_switchertop = &adw::ViewSwitcher{
|
||||||
|
set_stack: Some(ready_view_stack),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_content = ready_toast_overlay = &adw::ToastOverlay {
|
||||||
|
set_child: Some(ready_view_stack),
|
||||||
|
},
|
||||||
|
|
||||||
|
add_bottom_bar = ready_switcherbar = &adw::ViewSwitcherBar {
|
||||||
|
set_stack: Some(ready_view_stack),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
#[track(model.changed(App::state()) && model.state == AppState::Ready)]
|
||||||
|
set_visible_child: {
|
||||||
|
let page: &adw::Bin = page_ready.as_ref();
|
||||||
|
page
|
||||||
|
},
|
||||||
|
#[track(model.changed(App::state()) && model.state != AppState::Ready)]
|
||||||
|
set_visible_child: {
|
||||||
|
let page: &adw::ToolbarView = page_prepare.as_ref();
|
||||||
|
page
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init(
|
||||||
|
_: Self::Init,
|
||||||
|
root: Self::Root,
|
||||||
|
sender: AsyncComponentSender<Self>,
|
||||||
|
) -> AsyncComponentParts<Self> {
|
||||||
|
let login_shared_state = Arc::new(Mutex::new(Arc::new(SharedState::new())));
|
||||||
|
|
||||||
|
let ready = Ready::builder()
|
||||||
|
.launch(login_shared_state.clone())
|
||||||
|
.forward(sender.input_sender(), convert_ready_response);
|
||||||
|
|
||||||
|
let login = Login::builder()
|
||||||
|
.launch(login_shared_state.clone())
|
||||||
|
.forward(sender.input_sender(), convert_login_response);
|
||||||
|
|
||||||
|
let model = App {
|
||||||
|
_network_fail: false,
|
||||||
|
login,
|
||||||
|
ready,
|
||||||
|
state: AppState::Loading,
|
||||||
|
tracker: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let ready_view_stack = model.ready.widget();
|
||||||
|
let page_login = model.login.widget();
|
||||||
|
|
||||||
|
let widgets = view_output!();
|
||||||
|
|
||||||
|
AsyncComponentParts { model, widgets }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_with_view(
|
||||||
|
&mut self,
|
||||||
|
widgets: &mut Self::Widgets,
|
||||||
|
message: Self::Input,
|
||||||
|
sender: AsyncComponentSender<Self>,
|
||||||
|
root: &Self::Root,
|
||||||
|
) -> Self::Output {
|
||||||
|
self.reset();
|
||||||
|
match message {
|
||||||
|
AppInput::ErrorOccoured(error) => {
|
||||||
|
let dialog: adw::AlertDialog = adw::AlertDialog::builder()
|
||||||
|
.title(error.short)
|
||||||
|
.body(error.long)
|
||||||
|
.build();
|
||||||
|
dialog.present(Some(root));
|
||||||
|
}
|
||||||
|
AppInput::SwitchToLoading => {
|
||||||
|
self.set_state(AppState::Loading);
|
||||||
|
}
|
||||||
|
AppInput::SwitchToLogin => {
|
||||||
|
self.set_state(AppState::RequiresLogIn);
|
||||||
|
}
|
||||||
|
AppInput::NetworkFail => {
|
||||||
|
self.set__network_fail(true);
|
||||||
|
if self.changed__network_fail() {
|
||||||
|
sender.input(AppInput::Notification(
|
||||||
|
"The internet connection is unstable.".to_string(),
|
||||||
|
10,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppInput::Notification(notification, timeout) => {
|
||||||
|
let toast_overlay = match self.state {
|
||||||
|
AppState::Loading => &widgets.prepare_toast_overlay,
|
||||||
|
AppState::RequiresLogIn => &widgets.prepare_toast_overlay,
|
||||||
|
AppState::Ready => &widgets.ready_toast_overlay,
|
||||||
|
AppState::FatalError => &widgets.prepare_toast_overlay,
|
||||||
|
};
|
||||||
|
|
||||||
|
toast_overlay.add_toast(
|
||||||
|
adw::Toast::builder()
|
||||||
|
.title(notification.as_str())
|
||||||
|
.timeout(timeout)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
AppInput::FatalErrorOccoured(error) => {
|
||||||
|
widgets.fatal_status_page.set_title(&error.short);
|
||||||
|
widgets.fatal_status_page.set_description(Some(format!("{}\nThis error is fatal, the app can't continue.", &error.long).as_str()));
|
||||||
|
self.set_state(AppState::FatalError);
|
||||||
|
}
|
||||||
|
AppInput::SwitchToReady => {
|
||||||
|
self.set_state(AppState::Ready);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.update_view(widgets, sender);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_login_response(response: LoginOutput) -> AppInput {
|
||||||
|
match response {
|
||||||
|
LoginOutput::RequiresLogin => AppInput::SwitchToLogin,
|
||||||
|
LoginOutput::RequiresLoading => AppInput::SwitchToLoading,
|
||||||
|
LoginOutput::Error(err) => AppInput::ErrorOccoured(AppError {
|
||||||
|
short: "An authorization error occured :(".to_string(),
|
||||||
|
long: err.to_string(),
|
||||||
|
}),
|
||||||
|
LoginOutput::NetworkFail => AppInput::NetworkFail,
|
||||||
|
LoginOutput::KeyringError(err) => AppInput::FatalErrorOccoured(AppError {
|
||||||
|
short: "Unable to operate on the keyring :(".to_string(),
|
||||||
|
long: err.to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_ready_response(response: ReadyOutput) -> AppInput {
|
||||||
|
match response {
|
||||||
|
ReadyOutput::FatalError(err) => AppInput::FatalErrorOccoured(AppError {
|
||||||
|
short: "Unexpted error occured.".to_string(),
|
||||||
|
long: err.to_string(),
|
||||||
|
}),
|
||||||
|
ReadyOutput::NoServicesEnabled => AppInput::FatalErrorOccoured(AppError {
|
||||||
|
short: "You can't use this app".to_string(),
|
||||||
|
long: "There is no feature on your account which is supported by this app. You need the offical app and register for one or more of:\n\"Briefasnkündigung\"".to_string(),
|
||||||
|
}),
|
||||||
|
ReadyOutput::Error(err) => AppInput::ErrorOccoured(AppError { short: "meow".to_string(), long: err.to_string() }),
|
||||||
|
ReadyOutput::Ready => AppInput::SwitchToReady,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let app = RelmApp::new(constants::APP_ID);
|
||||||
|
app.run_async::<App>(());
|
||||||
|
}
|
286
paket/src/ready.rs
Normal file
286
paket/src/ready.rs
Normal file
|
@ -0,0 +1,286 @@
|
||||||
|
// managed the various pages...
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use adw::prelude::*;
|
||||||
|
use libpaket::{
|
||||||
|
self,
|
||||||
|
advices::{AdvicesList, UatToken},
|
||||||
|
LibraryError, LibraryResult,
|
||||||
|
};
|
||||||
|
use relm4::{adw, factory::FactoryVecDeque, prelude::*};
|
||||||
|
|
||||||
|
use crate::advices::AppAdviceMetadata;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum ReadyAdvicesState {
|
||||||
|
Loading,
|
||||||
|
HaveNone,
|
||||||
|
HaveSome,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracker::track]
|
||||||
|
pub struct Ready {
|
||||||
|
#[do_not_track]
|
||||||
|
login: crate::LoginSharedState,
|
||||||
|
activate: bool,
|
||||||
|
have_service_advices: bool,
|
||||||
|
#[do_not_track]
|
||||||
|
advices_factory: FactoryVecDeque<crate::advices::AppAdvice>,
|
||||||
|
advices_state: ReadyAdvicesState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ReadyOutput {
|
||||||
|
Ready,
|
||||||
|
Error(LibraryError),
|
||||||
|
FatalError(LibraryError),
|
||||||
|
NoServicesEnabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ReadyCmds {
|
||||||
|
LoggedIn,
|
||||||
|
LoggedOut,
|
||||||
|
GotCustomerDataFull(LibraryResult<libpaket::stammdaten::CustomerDataFull>),
|
||||||
|
RetryAdvices,
|
||||||
|
GotAdvices((LibraryResult<Vec<AppAdviceMetadata>>, Option<UatToken>)),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ReadyInput {
|
||||||
|
Activate,
|
||||||
|
Deactivate,
|
||||||
|
HaveAdvicesService,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[relm4::component(pub)]
|
||||||
|
impl Component for Ready {
|
||||||
|
type Input = ReadyInput;
|
||||||
|
type Output = ReadyOutput;
|
||||||
|
type Init = crate::LoginSharedState;
|
||||||
|
type CommandOutput = ReadyCmds;
|
||||||
|
|
||||||
|
view! {
|
||||||
|
#[root]
|
||||||
|
adw::ViewStack {
|
||||||
|
add = &adw::Bin {
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = &adw::ViewStack {
|
||||||
|
#[name = "advices_page_loading"]
|
||||||
|
add = &adw::StatusPage {
|
||||||
|
set_title: "Loading mail notifications...",
|
||||||
|
|
||||||
|
},
|
||||||
|
#[name = "advices_page_no_available"]
|
||||||
|
add = &adw::StatusPage {
|
||||||
|
set_title: "No mail notifications available."
|
||||||
|
},
|
||||||
|
#[name = "advices_page_have_some"]
|
||||||
|
add = >k::Box {
|
||||||
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
|
|
||||||
|
#[local_ref]
|
||||||
|
advices_carousel -> adw::Carousel {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
adw::CarouselIndicatorDots {
|
||||||
|
set_carousel: Some(advices_carousel),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
#[track(model.changed_advices_state())]
|
||||||
|
set_visible_child: {
|
||||||
|
let page: >k::Widget = match model.advices_state {
|
||||||
|
ReadyAdvicesState::Loading => advices_page_loading.upcast_ref::<gtk::Widget>(),
|
||||||
|
ReadyAdvicesState::HaveNone => advices_page_no_available.upcast_ref::<gtk::Widget>(),
|
||||||
|
ReadyAdvicesState::HaveSome => advices_page_have_some.upcast_ref::<gtk::Widget>(),
|
||||||
|
};
|
||||||
|
page
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} -> page_advices: adw::ViewStackPage {
|
||||||
|
set_title: Some("Mail notification"),
|
||||||
|
#[track(model.changed_have_service_advices())]
|
||||||
|
set_visible: model.have_service_advices,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(
|
||||||
|
init: Self::Init,
|
||||||
|
root: Self::Root,
|
||||||
|
sender: ComponentSender<Self>,
|
||||||
|
) -> ComponentParts<Self> {
|
||||||
|
let advices_factory = FactoryVecDeque::builder()
|
||||||
|
.launch(adw::Carousel::new())
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
let model = Ready {
|
||||||
|
have_service_advices: false,
|
||||||
|
advices_factory,
|
||||||
|
advices_state: ReadyAdvicesState::Loading,
|
||||||
|
login: init.clone(),
|
||||||
|
activate: false,
|
||||||
|
tracker: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let advices_carousel = model.advices_factory.widget();
|
||||||
|
|
||||||
|
let widgets = view_output!();
|
||||||
|
{
|
||||||
|
let login = model.login.clone();
|
||||||
|
sender.command(move |out, shutdown| {
|
||||||
|
shutdown
|
||||||
|
.register(async move {
|
||||||
|
let login = { login.clone().as_ref().lock().await.clone() };
|
||||||
|
let (sender, receiver) = relm4::channel::<ReadyCmds>();
|
||||||
|
login.subscribe(&sender, |model| match model {
|
||||||
|
Some(_) => ReadyCmds::LoggedIn,
|
||||||
|
None => ReadyCmds::LoggedOut,
|
||||||
|
});
|
||||||
|
loop {
|
||||||
|
out.send(receiver.recv().await.unwrap()).unwrap();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.drop_on_shutdown()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ComponentParts { model, widgets }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>, _: &Self::Root) {
|
||||||
|
self.reset();
|
||||||
|
|
||||||
|
let token = self.login.clone();
|
||||||
|
match message {
|
||||||
|
ReadyInput::Activate => {
|
||||||
|
self.set_activate(true);
|
||||||
|
if self.changed_activate() {
|
||||||
|
sender.oneshot_command(async move {
|
||||||
|
let token = crate::login::get_id_token(&token).await.unwrap();
|
||||||
|
let client = libpaket::StammdatenClient::new();
|
||||||
|
ReadyCmds::GotCustomerDataFull(client.customer_data_full(&token).await)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ReadyInput::Deactivate => {
|
||||||
|
self.set_activate(false);
|
||||||
|
}
|
||||||
|
ReadyInput::HaveAdvicesService => {
|
||||||
|
sender.oneshot_command(async move {
|
||||||
|
// fetching advices
|
||||||
|
let dhli_token = crate::login::get_id_token(&token).await.unwrap();
|
||||||
|
let advices = libpaket::WebClient::new().advices(&dhli_token).await;
|
||||||
|
let advices = match advices {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(err) => return ReadyCmds::GotAdvices((Err(err), None)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut advices_vec = Vec::new();
|
||||||
|
|
||||||
|
if let Some(current) = advices.get_current_advice() {
|
||||||
|
push_advice_from_libpaket_advice(&mut advices_vec, current);
|
||||||
|
}
|
||||||
|
|
||||||
|
for old_n in advices.get_old_advices() {
|
||||||
|
push_advice_from_libpaket_advice(&mut advices_vec, old_n);
|
||||||
|
}
|
||||||
|
|
||||||
|
if advices_vec.len() == 0 {
|
||||||
|
return ReadyCmds::GotAdvices((Ok(advices_vec), None));
|
||||||
|
}
|
||||||
|
|
||||||
|
match libpaket::advices::AdviceClient::new()
|
||||||
|
.access_token(&advices)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(uat_token) => ReadyCmds::GotAdvices((Ok(advices_vec), Some(uat_token))),
|
||||||
|
Err(err) => ReadyCmds::GotAdvices((Err(err), None)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_cmd(
|
||||||
|
&mut self,
|
||||||
|
message: Self::CommandOutput,
|
||||||
|
sender: ComponentSender<Self>,
|
||||||
|
_: &Self::Root,
|
||||||
|
) {
|
||||||
|
match message {
|
||||||
|
ReadyCmds::LoggedIn => sender.input(ReadyInput::Activate),
|
||||||
|
ReadyCmds::LoggedOut => sender.input(ReadyInput::Deactivate),
|
||||||
|
ReadyCmds::GotCustomerDataFull(res) => match res {
|
||||||
|
Ok(res) => {
|
||||||
|
let mut a_service_was_activated = false;
|
||||||
|
for service in &res.common.services {
|
||||||
|
match service {
|
||||||
|
libpaket::stammdaten::CustomerDataService::Packstation => (),
|
||||||
|
libpaket::stammdaten::CustomerDataService::Paketankuendigung => (),
|
||||||
|
libpaket::stammdaten::CustomerDataService::PostfilialeDirekt => (),
|
||||||
|
libpaket::stammdaten::CustomerDataService::Digiben => (),
|
||||||
|
libpaket::stammdaten::CustomerDataService::GeraetAktiviert => (),
|
||||||
|
libpaket::stammdaten::CustomerDataService::Briefankuendigung => {
|
||||||
|
a_service_was_activated = true;
|
||||||
|
sender.input(ReadyInput::HaveAdvicesService);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a_service_was_activated {
|
||||||
|
sender.output(ReadyOutput::Ready).unwrap()
|
||||||
|
} else {
|
||||||
|
sender.output(ReadyOutput::NoServicesEnabled).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => sender.output(ReadyOutput::FatalError(err)).unwrap(),
|
||||||
|
},
|
||||||
|
ReadyCmds::GotAdvices(res) => match res.0 {
|
||||||
|
Ok(advices_vec) => {
|
||||||
|
{
|
||||||
|
let mut guard = self.advices_factory.guard();
|
||||||
|
guard.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if advices_vec.len() == 0 {
|
||||||
|
self.set_advices_state(ReadyAdvicesState::HaveNone);
|
||||||
|
} else {
|
||||||
|
self.set_advices_state(ReadyAdvicesState::HaveSome);
|
||||||
|
let uat_token = res.1.unwrap();
|
||||||
|
let mut guard = self.advices_factory.guard();
|
||||||
|
guard.clear();
|
||||||
|
for i in advices_vec {
|
||||||
|
guard.push_back((i, uat_token.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
sender.output(ReadyOutput::Error(err)).unwrap();
|
||||||
|
sender.oneshot_command(async {
|
||||||
|
relm4::tokio::time::sleep(Duration::from_secs(30)).await;
|
||||||
|
ReadyCmds::RetryAdvices
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ReadyCmds::RetryAdvices => {
|
||||||
|
sender.input(ReadyInput::HaveAdvicesService);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_advice_from_libpaket_advice(
|
||||||
|
vec: &mut Vec<AppAdviceMetadata>,
|
||||||
|
libpaket_advice: &AdvicesList,
|
||||||
|
) {
|
||||||
|
for i in &libpaket_advice.list {
|
||||||
|
vec.push(AppAdviceMetadata {
|
||||||
|
date: libpaket_advice.date.clone(),
|
||||||
|
advice: i.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue