From ce5503da892e09df60d87d75ff722214f7f50bde Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Sat, 2 Jul 2022 18:32:41 +0200 Subject: [PATCH] Add code to authenticate to NextCloud server --- .cspell/custom-dictionary-workspace.txt | 6 + .vscode/settings.json | 12 + Cargo.lock | 373 ++++++++++++++++++++++++ Cargo.toml | 23 +- src/config/credentials.rs | 118 ++++++++ src/config/mod.rs | 40 +++ src/main.rs | 45 ++- 7 files changed, 614 insertions(+), 3 deletions(-) create mode 100644 .cspell/custom-dictionary-workspace.txt create mode 100644 .vscode/settings.json create mode 100644 src/config/credentials.rs create mode 100644 src/config/mod.rs diff --git a/.cspell/custom-dictionary-workspace.txt b/.cspell/custom-dictionary-workspace.txt new file mode 100644 index 0000000..b7b2a19 --- /dev/null +++ b/.cspell/custom-dictionary-workspace.txt @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2022 Matteo Settenvini +# SPDX-License-Identifier: CC0-1.0 +# Custom Dictionary Words +montecristosoftware +reqwest +webbrowser diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..165fc97 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: CC0-1.0 +{ + "cSpell.customDictionaries": { + "custom-dictionary-workspace": { + "name": "custom-dictionary-workspace", + "path": "${workspaceFolder:cooking-schedule}/.cspell/custom-dictionary-workspace.txt", + "addWords": true, + "scope": "workspace" + } + } +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 77bfdad..197a209 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,23 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anyhow" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -38,6 +55,12 @@ version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.0" @@ -53,14 +76,55 @@ dependencies = [ "envmnt", ] +[[package]] +name = "clap" +version = "3.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83" +dependencies = [ + "atty", + "bitflags", + "clap_lex", + "indexmap", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "combine" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a604e93b79d1808327a6fca85a6f2d69de66461e7620f5a4cbf5fb4d1d7c948" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "cooking-schedule" version = "0.0.0" dependencies = [ + "anyhow", + "clap", + "directories", "reqwest", "rusty-hook", + "serde", "serde_json", "tokio", + "toml", + "webbrowser", ] [[package]] @@ -79,6 +143,61 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "directories" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "encoding_rs" version = "0.8.31" @@ -159,6 +278,12 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +[[package]] +name = "futures-io" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" + [[package]] name = "futures-sink" version = "0.3.21" @@ -178,9 +303,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ "futures-core", + "futures-io", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -192,6 +320,17 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "h2" version = "0.3.13" @@ -297,6 +436,12 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.3" @@ -339,6 +484,26 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "js-sys" version = "0.3.58" @@ -417,6 +582,62 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" +dependencies = [ + "bitflags", + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-glue" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d0c4a7b83860226e6b4183edac21851f05d5a51756e97a1144b7f5a6b63e65f" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" +dependencies = [ + "darling", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ndk-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" +dependencies = [ + "jni-sys", +] + [[package]] name = "nias" version = "0.5.0" @@ -433,6 +654,27 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "once_cell" version = "1.12.0" @@ -484,6 +726,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "os_str_bytes" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" + [[package]] name = "percent-encoding" version = "2.1.0" @@ -508,6 +756,16 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +[[package]] +name = "proc-macro-crate" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" +dependencies = [ + "thiserror", + "toml", +] + [[package]] name = "proc-macro2" version = "1.0.40" @@ -535,6 +793,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -599,6 +868,15 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.20" @@ -637,6 +915,20 @@ name = "serde" version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "serde_json" @@ -677,6 +969,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.98" @@ -702,6 +1000,41 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -856,6 +1189,17 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + [[package]] name = "want" version = "0.3.0" @@ -948,6 +1292,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webbrowser" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a3cffdb686fbb24d9fb8f03a213803277ed2300f11026a3afe1f108dc021b" +dependencies = [ + "jni", + "ndk-glue", + "url", + "web-sys", + "widestring", + "winapi", +] + +[[package]] +name = "widestring" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" + [[package]] name = "winapi" version = "0.3.9" @@ -964,6 +1328,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 7cba4f2..6dc6fbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ [package] name = "cooking-schedule" +description = "Build a schedule of dishes to cook with the help of NextCloud Cookbook" version = "0.0.0" edition = "2021" authors = ["Matteo Settenvini "] @@ -15,13 +16,33 @@ path = "src/main.rs" [dev-dependencies.rusty-hook] version = "0.11" +[dependencies.anyhow] +version = "1.0" + +[dependencies.clap] +version = "3.2" +features = ["cargo"] + +[dependencies.directories] +version = "4.0" + [dependencies.reqwest] version = "0.11" -features = ["json"] +features = ["json", "blocking"] + +[dependencies.serde] +version = "1.0" +features = ["derive"] [dependencies.serde_json] version = "1.0" +[dependencies.toml] +version = "0.5" + [dependencies.tokio] version = "1" features = ["rt-multi-thread", "net", "macros"] + +[dependencies.webbrowser] +version = "0.7" diff --git a/src/config/credentials.rs b/src/config/credentials.rs new file mode 100644 index 0000000..8673c2c --- /dev/null +++ b/src/config/credentials.rs @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: AGPL-3.0-or-later + +use { + anyhow::{anyhow, Result}, + reqwest::{StatusCode, Url}, + serde::{Deserialize, Serialize}, + std::collections::HashMap, + std::io::ErrorKind, + std::path::{Path, PathBuf}, + std::time::Duration, +}; + +#[derive(Deserialize, Serialize, Default)] +pub struct Credentials { + #[serde(skip)] + config_file: PathBuf, + + servers: HashMap, +} + +#[derive(Deserialize, Serialize)] +pub struct Server { + url: String, + login_name: String, + password: String, +} + +impl Credentials { + pub fn from_directory(config_dir: &Path) -> Result { + let credentials_file = config_dir.join("credentials.toml"); + match std::fs::read_to_string(&credentials_file) { + Ok(content) => { + let mut credentials: Credentials = toml::from_str(&content)?; + credentials.config_file = credentials_file; + Ok(credentials) + } + Err(err) if err.kind() == ErrorKind::NotFound => { + let mut credentials = Credentials::default(); + credentials.config_file = credentials_file; + credentials.write_back()?; + Ok(credentials) + } + Err(err) => Err(anyhow!(err)), + } + } + + pub fn add(&mut self, server: Url) -> Result<()> { + let http_client = reqwest::blocking::Client::new(); + + #[derive(Deserialize)] + struct LoginFlow { + poll: LoginPollEndpoint, + login: String, + } + #[derive(Deserialize)] + struct LoginPollEndpoint { + token: String, + endpoint: String, + } + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct LoginResult { + server: String, + login_name: String, + app_password: String, + } + + let login_url = server.join("index.php/login/v2")?; + + let login_flow = http_client + .post(login_url.clone()) + .send()? + .json::()?; + + // User auth flow happens in browser + webbrowser::open(&login_flow.login)?; + + // We start polling for the end of the flow + loop { + let response = http_client + .post(&login_flow.poll.endpoint) + .form(&[("token", &login_flow.poll.token)]) + .send()?; + + match response.status() { + StatusCode::OK => { + let login_result = response.json::()?; + let new_server = Server { + url: login_result.server, + login_name: login_result.login_name, + password: login_result.app_password, + }; + + let host_name = server + .host_str() + .ok_or(anyhow!("No hostname for provided URL, is it valid?"))?; + self.servers.insert(host_name.to_owned(), new_server); + break; + } + StatusCode::NOT_FOUND => { + std::thread::sleep(Duration::from_secs(1)); + // ...then keep polling + } + _ => { + response.error_for_status()?; + } + } + } + + self.write_back() + } + + fn write_back(&self) -> Result<()> { + std::fs::write(&self.config_file, toml::to_string(self)?)?; + Ok(()) + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..86d770c --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: AGPL-3.0-or-later + +pub mod credentials; + +use { + self::credentials::Credentials, + anyhow::Result, + directories::ProjectDirs, + serde::Deserialize, + std::io::{Error, ErrorKind}, + std::path::PathBuf, +}; + +#[derive(Deserialize)] +pub struct Config { + #[serde(skip_deserializing)] + pub credentials: Credentials, +} + +impl Config { + pub fn new() -> Self { + let config_dir = Self::ensure_config_dir().unwrap(); + Config { + credentials: Credentials::from_directory(&config_dir).unwrap(), + } + } + + fn ensure_config_dir() -> Result { + let config_dir = ProjectDirs::from("eu", "montecristosoftware", "cooking-schedule") + .ok_or(Error::new( + ErrorKind::Other, + "Unable to determine application configuration folder", + ))? + .config_dir() + .to_path_buf(); + std::fs::create_dir_all(&config_dir)?; + Ok(config_dir) + } +} diff --git a/src/main.rs b/src/main.rs index 12ea41d..93b842e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,47 @@ // SPDX-FileCopyrightText: 2022 Matteo Settenvini // SPDX-License-Identifier: AGPL-3.0-or-later -fn main() { - println!("Hello, world!"); +mod config; + +use { + self::config::Config, + clap::{arg, command, ArgMatches, Command}, + reqwest::Url, +}; + +fn parse_args() -> ArgMatches { + command!() + .propagate_version(true) + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand( + Command::new("init") + .about("Authenticate against the provided NextCloud server") + .arg(arg!( "NextCloud server to connect to")), + ) + .get_matches() +} + +#[tokio::main(flavor = "multi_thread")] +async fn main() -> anyhow::Result<()> { + let args = parse_args(); + let mut configuration = Config::new(); + + match args.subcommand() { + Some(("init", sub_matches)) => { + let server = sub_matches + .get_one::("server") + .expect("Mandatory parameter "); + tokio::task::block_in_place(move || -> anyhow::Result<()> { + configuration + .credentials + .add(Url::parse(server)?) + .expect("Unable to authenticate to NextCloud instance"); + Ok(()) + })?; + } + _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"), + }; + + Ok(()) }