From 38d638294f8bb1b490125157607a7b3a8b275eff Mon Sep 17 00:00:00 2001 From: "bestia.dev" Date: Wed, 1 May 2024 01:58:57 +0000 Subject: [PATCH] new_cli --- .vscode/settings.json | 4 + Cargo.toml | 6 +- README.md | 10 +- RELEASES.md | 2 + automation_tasks_rs/src/main.rs | 7 - src/bin/cargo-auto/main.rs | 10 +- src/lib.rs | 10 +- src/template_new_auto_mod.rs | 422 +++- src/template_new_cli_mod.rs | 2224 +---------------- src/template_new_pwa_wasm_mod.rs | 380 ++- src/template_new_wasm_mod.rs | 378 ++- template_new_cli/.gitattributes | 16 - .../.github/workflows/clear_all_cache.yml | 51 - .../.github/workflows/docs_pages.yml | 43 - .../workflows/rust_fmt_auto_build_test.yml | 52 - template_new_cli/.gitignore | 11 - template_new_cli/.vscode/settings.json | 48 - template_new_cli/Cargo.toml | 39 - template_new_cli/DEVELOPMENT.md | 82 - template_new_cli/LICENSE | 21 - template_new_cli/README.md | 67 - template_new_cli/RELEASES.md | 15 - .../automation_tasks_rs/.gitignore | 9 - .../automation_tasks_rs/.vscode/settings.json | 51 - .../automation_tasks_rs/Cargo.toml | 31 - .../automation_tasks_rs/rustfmt.toml | 1 - .../automation_tasks_rs/src/main.rs | 463 ---- .../src/secrets_always_local_mod.rs | 763 ------ template_new_cli/examples/example_1.rs | 19 - template_new_cli/rustfmt.toml | 1 - .../bin/cargo_auto_template_new_cli/main.rs | 80 - template_new_cli/src/hello_mod.rs | 50 - template_new_cli/src/lib.rs | 42 - template_new_cli/tests/integration_test.rs | 14 - .../pwa_short_name/service_worker.js | 2 +- 35 files changed, 894 insertions(+), 4530 deletions(-) delete mode 100644 template_new_cli/.gitattributes delete mode 100644 template_new_cli/.github/workflows/clear_all_cache.yml delete mode 100644 template_new_cli/.github/workflows/docs_pages.yml delete mode 100644 template_new_cli/.github/workflows/rust_fmt_auto_build_test.yml delete mode 100644 template_new_cli/.gitignore delete mode 100644 template_new_cli/.vscode/settings.json delete mode 100644 template_new_cli/Cargo.toml delete mode 100644 template_new_cli/DEVELOPMENT.md delete mode 100644 template_new_cli/LICENSE delete mode 100644 template_new_cli/README.md delete mode 100644 template_new_cli/RELEASES.md delete mode 100644 template_new_cli/automation_tasks_rs/.gitignore delete mode 100644 template_new_cli/automation_tasks_rs/.vscode/settings.json delete mode 100644 template_new_cli/automation_tasks_rs/Cargo.toml delete mode 100644 template_new_cli/automation_tasks_rs/rustfmt.toml delete mode 100644 template_new_cli/automation_tasks_rs/src/main.rs delete mode 100644 template_new_cli/automation_tasks_rs/src/secrets_always_local_mod.rs delete mode 100644 template_new_cli/examples/example_1.rs delete mode 100644 template_new_cli/rustfmt.toml delete mode 100644 template_new_cli/src/bin/cargo_auto_template_new_cli/main.rs delete mode 100644 template_new_cli/src/hello_mod.rs delete mode 100644 template_new_cli/src/lib.rs delete mode 100644 template_new_cli/tests/integration_test.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index d42a191c..33cb48bc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -38,6 +38,7 @@ "filedate", "filehash", "filetime", + "flate", "HEXLOWER", "imageops", "Lanczos", @@ -66,9 +67,12 @@ "subcommand", "subsecond", "substack", + "tempdir", + "tempfile", "termion", "thiserror", "unoptimized", + "walkdir", "webassembly", "wooow", "xtask", diff --git a/Cargo.toml b/Cargo.toml index b5084d64..157428cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-auto" -version = "2024.422.214" +version = "2024.501.55" authors = ["bestia.dev"] homepage = "https://bestia.dev" edition = "2021" @@ -32,6 +32,10 @@ data-encoding = "2.5.0" anyhow="1.0.79" serde_derive = "1.0.196" serde_json = "1.0.113" +reqwest = { version = "0.12.3", features = ["blocking"] } +flate2 = "1.0.30" +tar = "0.4.40" +walkdir = "2.5.0" [lib] name = "cargo_auto_local_lib" diff --git a/README.md b/README.md index 616bc362..5c4d7bc3 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [//]: # (auto_cargo_toml_to_md start) **Automation tasks coded in Rust language for the workflow of Rust projects** -***version: 2024.422.214 date: 2024-04-22 author: [bestia.dev](https://bestia.dev) repository: [GitHub](https://github.com/automation-tasks-rs/cargo-auto)*** +***version: 2024.501.55 date: 2024-05-01 author: [bestia.dev](https://bestia.dev) repository: [GitHub](https://github.com/automation-tasks-rs/cargo-auto)*** ![maintained](https://img.shields.io/badge/maintained-green) ![ready-for-use](https://img.shields.io/badge/ready_for_use-green) @@ -28,11 +28,11 @@ ![cargo-auto](https://bestia.dev/webpage_hit_counter/get_svg_image/959103982.svg) [//]: # (auto_lines_of_code start) -[![Lines in Rust code](https://img.shields.io/badge/Lines_in_Rust-3145-green.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -[![Lines in Doc comments](https://img.shields.io/badge/Lines_in_Doc_comments-1203-blue.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -[![Lines in Comments](https://img.shields.io/badge/Lines_in_comments-704-purple.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +[![Lines in Rust code](https://img.shields.io/badge/Lines_in_Rust-2565-green.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +[![Lines in Doc comments](https://img.shields.io/badge/Lines_in_Doc_comments-1159-blue.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +[![Lines in Comments](https://img.shields.io/badge/Lines_in_comments-615-purple.svg)](https://github.com/automation-tasks-rs/cargo-auto/) [![Lines in examples](https://img.shields.io/badge/Lines_in_examples-0-yellow.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -[![Lines in tests](https://img.shields.io/badge/Lines_in_tests-10949-orange.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +[![Lines in tests](https://img.shields.io/badge/Lines_in_tests-10243-orange.svg)](https://github.com/automation-tasks-rs/cargo-auto/) [//]: # (auto_lines_of_code end) diff --git a/RELEASES.md b/RELEASES.md index e7678fdf..10245c19 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -10,6 +10,8 @@ The TODO section is part of the [README.md](https://github.com/automation-tasks- ## Unreleased +- new_cli + ## Version 2024.422.214 (2024-04-22) - allow dead code diff --git a/automation_tasks_rs/src/main.rs b/automation_tasks_rs/src/main.rs index cd49a933..92f35442 100644 --- a/automation_tasks_rs/src/main.rs +++ b/automation_tasks_rs/src/main.rs @@ -214,13 +214,6 @@ fn copy_files_into_module(){ &exclude_big_folders, ); - cl::copy_folder_files_into_module( - std::path::Path::new("template_new_cli"), - std::path::Path::new("src/template_new_cli_mod.rs"), - &ext_for_binary_files, - &exclude_big_folders, - ); - cl::copy_folder_files_into_module( std::path::Path::new("template_new_wasm"), std::path::Path::new("src/template_new_wasm_mod.rs"), diff --git a/src/bin/cargo-auto/main.rs b/src/bin/cargo-auto/main.rs index 2fe9b291..a33b1636 100644 --- a/src/bin/cargo-auto/main.rs +++ b/src/bin/cargo-auto/main.rs @@ -6,7 +6,7 @@ //! # cargo-auto //! //! **Automation tasks coded in Rust language for the workflow of Rust projects** -//! ***version: 2024.422.214 date: 2024-04-22 author: [bestia.dev](https://bestia.dev) repository: [GitHub](https://github.com/automation-tasks-rs/cargo-auto)*** +//! ***version: 2024.501.55 date: 2024-05-01 author: [bestia.dev](https://bestia.dev) repository: [GitHub](https://github.com/automation-tasks-rs/cargo-auto)*** //! //! ![maintained](https://img.shields.io/badge/maintained-green) //! ![ready-for-use](https://img.shields.io/badge/ready_for_use-green) @@ -26,11 +26,11 @@ //! [![Newest docs](https://img.shields.io/badge/newest_docs-blue.svg)](https://automation-tasks-rs.github.io/cargo-auto/cargo_auto/index.html) //! ![cargo-auto](https://bestia.dev/webpage_hit_counter/get_svg_image/959103982.svg) //! -//! [![Lines in Rust code](https://img.shields.io/badge/Lines_in_Rust-3145-green.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -//! [![Lines in Doc comments](https://img.shields.io/badge/Lines_in_Doc_comments-1203-blue.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -//! [![Lines in Comments](https://img.shields.io/badge/Lines_in_comments-704-purple.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +//! [![Lines in Rust code](https://img.shields.io/badge/Lines_in_Rust-2565-green.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +//! [![Lines in Doc comments](https://img.shields.io/badge/Lines_in_Doc_comments-1159-blue.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +//! [![Lines in Comments](https://img.shields.io/badge/Lines_in_comments-615-purple.svg)](https://github.com/automation-tasks-rs/cargo-auto/) //! [![Lines in examples](https://img.shields.io/badge/Lines_in_examples-0-yellow.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -//! [![Lines in tests](https://img.shields.io/badge/Lines_in_tests-10949-orange.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +//! [![Lines in tests](https://img.shields.io/badge/Lines_in_tests-10243-orange.svg)](https://github.com/automation-tasks-rs/cargo-auto/) //! //! Hashtags: #maintained #ready-for-use #rustlang #automation #workflow //! My projects on GitHub are more like a tutorial than a finished product: [bestia-dev tutorials](https://github.com/bestia-dev/tutorials_rust_wasm). diff --git a/src/lib.rs b/src/lib.rs index f82b1cc0..58c34436 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ //! # cargo-auto //! //! **Automation tasks coded in Rust language for the workflow of Rust projects** -//! ***version: 2024.422.214 date: 2024-04-22 author: [bestia.dev](https://bestia.dev) repository: [GitHub](https://github.com/automation-tasks-rs/cargo-auto)*** +//! ***version: 2024.501.55 date: 2024-05-01 author: [bestia.dev](https://bestia.dev) repository: [GitHub](https://github.com/automation-tasks-rs/cargo-auto)*** //! //! ![maintained](https://img.shields.io/badge/maintained-green) //! ![ready-for-use](https://img.shields.io/badge/ready_for_use-green) @@ -26,11 +26,11 @@ //! [![Newest docs](https://img.shields.io/badge/newest_docs-blue.svg)](https://automation-tasks-rs.github.io/cargo-auto/cargo_auto/index.html) //! ![cargo-auto](https://bestia.dev/webpage_hit_counter/get_svg_image/959103982.svg) //! -//! [![Lines in Rust code](https://img.shields.io/badge/Lines_in_Rust-3145-green.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -//! [![Lines in Doc comments](https://img.shields.io/badge/Lines_in_Doc_comments-1203-blue.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -//! [![Lines in Comments](https://img.shields.io/badge/Lines_in_comments-704-purple.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +//! [![Lines in Rust code](https://img.shields.io/badge/Lines_in_Rust-2565-green.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +//! [![Lines in Doc comments](https://img.shields.io/badge/Lines_in_Doc_comments-1159-blue.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +//! [![Lines in Comments](https://img.shields.io/badge/Lines_in_comments-615-purple.svg)](https://github.com/automation-tasks-rs/cargo-auto/) //! [![Lines in examples](https://img.shields.io/badge/Lines_in_examples-0-yellow.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -//! [![Lines in tests](https://img.shields.io/badge/Lines_in_tests-10949-orange.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +//! [![Lines in tests](https://img.shields.io/badge/Lines_in_tests-10243-orange.svg)](https://github.com/automation-tasks-rs/cargo-auto/) //! //! Hashtags: #maintained #ready-for-use #rustlang #automation #workflow //! My projects on GitHub are more like a tutorial than a finished product: [bestia-dev tutorials](https://github.com/bestia-dev/tutorials_rust_wasm). diff --git a/src/template_new_auto_mod.rs b/src/template_new_auto_mod.rs index 9aec95b8..d023cbd6 100644 --- a/src/template_new_auto_mod.rs +++ b/src/template_new_auto_mod.rs @@ -83,9 +83,9 @@ Cargo.lock /.old_metadata.json "###, }); - vec_file.push(crate::FileItem{ - file_name :"src/main.rs", - file_content : r###"// automation_tasks_rs for cargo_auto_template_new_cli + vec_file.push(crate::FileItem { + file_name: "src/main.rs", + file_content: r###"// automation_tasks_rs for cargo_auto_template_new_cli // region: library and modules with basic automation tasks @@ -237,18 +237,18 @@ fn print_help() { {YELLOW}It is preferred to use SSH for git push to GitHub.{RESET} {YELLOW}{YELLOW} {YELLOW}On the very first commit, this task will initialize a new local git repository and create a remote GitHub repo.{RESET} - {YELLOW}For the GitHub API the task needs the Personal Access Token Classic from {RESET} - {YELLOW}You can choose to type the token every time or to store it in a file encrypted with an SSH key.{RESET} + {YELLOW}For the GitHub API the task needs the Personal Access secret_token Classic from {RESET} + {YELLOW}You can choose to type the secret_token every time or to store it in a file encrypted with an SSH key.{RESET} {YELLOW}Then you can type the passphrase of the private key every time. This is pretty secure.{RESET} {YELLOW}Somewhat less secure (but more comfortable) way is to store the private key in ssh-agent.{RESET} {GREEN}cargo auto publish_to_crates_io{RESET} - {YELLOW}publish to crates.io, git tag{RESET} - {YELLOW}You need the API secret_token for publishing. Get the token on .{RESET} - {YELLOW}You can choose to type the token every time or to store it in a file encrypted with an SSH key.{RESET} + {YELLOW}You need the API secret_token for publishing. Get the secret_token on .{RESET} + {YELLOW}You can choose to type the secret_token every time or to store it in a file encrypted with an SSH key.{RESET} {YELLOW}Then you can type the passphrase of the private key every time. This is pretty secure.{RESET} {YELLOW}Somewhat less secure (but more comfortable) way is to store the private key in ssh-agent.{RESET} {GREEN}cargo auto github_new_release{RESET} - {YELLOW}creates new release on GitHub{RESET} - {YELLOW}For the GitHub API the task needs the Personal Access Token Classic from {RESET} - {YELLOW}You can choose to type the token every time or to store it in a file encrypted with an SSH key.{RESET} + {YELLOW}For the GitHub API the task needs the Personal Access secret_token Classic from {RESET} + {YELLOW}You can choose to type the secret_token every time or to store it in a file encrypted with an SSH key.{RESET} {YELLOW}Then you can type the passphrase of the private key every time. This is pretty secure.{RESET} {YELLOW}Somewhat less secure (but more comfortable) way is to store the private key in ssh-agent.{RESET} @@ -359,9 +359,12 @@ fn task_doc() { cl::run_shell_command_static("rsync -a --info=progress2 --delete-after target/doc/ docs/").unwrap_or_else(|e| panic!("{e}")); // Create simple index.html file in docs directory - cl::ShellCommandLimitedDoubleQuotesSanitizer::new(r#"printf "\n" > docs/index.html"#).unwrap_or_else(|e| panic!("{e}")) - .arg("{url_sanitized_for_double_quote}", &cargo_toml.package_name().replace("-", "_")).unwrap_or_else(|e| panic!("{e}")) - .run().unwrap_or_else(|e| panic!("{e}")); + cl::ShellCommandLimitedDoubleQuotesSanitizer::new(r#"printf "\n" > docs/index.html"#) + .unwrap_or_else(|e| panic!("{e}")) + .arg("{url_sanitized_for_double_quote}", &cargo_toml.package_name().replace("-", "_")) + .unwrap_or_else(|e| panic!("{e}")) + .run() + .unwrap_or_else(|e| panic!("{e}")); // pretty html cl::auto_doc_tidy_html().unwrap_or_else(|e| panic!("{e}")); @@ -406,11 +409,11 @@ fn task_commit_and_push(arg_2: Option) { // If needed, ask to create a GitHub remote repository if !cgl::git_has_remote() || !cgl::git_has_upstream() { - let github_client = github_mod::GitHubClient::new_with_stored_token(); + let github_client = github_mod::GitHubClient::new_with_stored_secret_token(); cgl::new_remote_github_repository(&github_client).unwrap(); cgl::description_and_topics_to_github(&github_client); } else { - let github_client = github_mod::GitHubClient::new_with_stored_token(); + let github_client = github_mod::GitHubClient::new_with_stored_secret_token(); // if description or topics/keywords/tags have changed cgl::description_and_topics_to_github(&github_client); @@ -421,9 +424,12 @@ fn task_commit_and_push(arg_2: Option) { cl::add_message_to_unreleased(&message); // the real commit of code - cl::ShellCommandLimitedDoubleQuotesSanitizer::new(r#"git add -A && git diff --staged --quiet || git commit -m "{message_sanitized_for_double_quote}" "#).unwrap_or_else(|e| panic!("{e}")) - .arg("{message_sanitized_for_double_quote}", &message).unwrap_or_else(|e| panic!("{e}")) - .run().unwrap_or_else(|e| panic!("{e}")); + cl::ShellCommandLimitedDoubleQuotesSanitizer::new(r#"git add -A && git diff --staged --quiet || git commit -m "{message_sanitized_for_double_quote}" "#) + .unwrap_or_else(|e| panic!("{e}")) + .arg("{message_sanitized_for_double_quote}", &message) + .unwrap_or_else(|e| panic!("{e}")) + .run() + .unwrap_or_else(|e| panic!("{e}")); cl::run_shell_command_static("git push").unwrap_or_else(|e| panic!("{e}")); } @@ -444,8 +450,8 @@ fn task_publish_to_crates_io() { // take care of tags let tag_name_version = cl::git_tag_sync_check_create_push(&version); - // cargo publish with encrypted secret token - let crates_io_client = crates_io_mod::CratesIoClient::new_with_stored_token(); + // cargo publish with encrypted secret secret_token + let crates_io_client = crates_io_mod::CratesIoClient::new_with_stored_secret_token(); crates_io_client.publish_to_crates_io(); println!( @@ -479,7 +485,7 @@ fn task_github_new_release() { // Then the automation task will copy the content to GitHub release let body_md_text = cl::body_text_from_releases_md().unwrap(); - let github_client = github_mod::GitHubClient::new_with_stored_token(); + let github_client = github_mod::GitHubClient::new_with_stored_secret_token(); let json_value = github_client.send_to_github_api(cgl::github_api_create_new_release(&owner, &repo_name, &tag_name_version, &release_name, branch, &body_md_text)); // early exit on error if let Some(error_message) = json_value.get("message") { @@ -515,7 +521,8 @@ fn task_github_new_release() { // compress files tar.gz let tar_name = format!("{repo_name}-{tag_name_version}-x86_64-unknown-linux-gnu.tar.gz"); - cl::ShellCommandLimitedDoubleQuotesSanitizer::new(r#"tar -zcvf "{tar_name_sanitized_for_double_quote}" "target/release/{repo_name_sanitized_for_double_quote}" "#).unwrap_or_else(|e| panic!("{e}")) + cl::ShellCommandLimitedDoubleQuotesSanitizer::new( + r#"tar -zcvf "{tar_name_sanitized_for_double_quote}" "target/release/{repo_name_sanitized_for_double_quote}" "#).unwrap_or_else(|e| panic!("{e}")) .arg("{tar_name_sanitized_for_double_quote}", &tar_name).unwrap_or_else(|e| panic!("{e}")) .arg("{repo_name_sanitized_for_double_quote}", &repo_name).unwrap_or_else(|e| panic!("{e}")) .run().unwrap_or_else(|e| panic!("{e}")); @@ -523,7 +530,8 @@ fn task_github_new_release() { // upload asset cgl::github_api_upload_asset_to_release(&github_client, &owner, &repo_name, &release_id, &tar_name); - cl::ShellCommandLimitedDoubleQuotesSanitizer::new(r#"rm "{tar_name_sanitized_for_double_quote}" "#).unwrap_or_else(|e| panic!("{e}")) + cl::ShellCommandLimitedDoubleQuotesSanitizer::new( + r#"rm "{tar_name_sanitized_for_double_quote}" "#).unwrap_or_else(|e| panic!("{e}")) .arg("{tar_name_sanitized_for_double_quote}", &tar_name).unwrap_or_else(|e| panic!("{e}")) .run().unwrap_or_else(|e| panic!("{e}")); @@ -543,12 +551,12 @@ fn task_github_new_release() { } // endregion: tasks "###, -}); + }); vec_file.push(crate::FileItem { file_name: "src/secrets_always_local_mod.rs", file_content: r###"// secrets_always_local_mod.rs -/// Secrets like GitHub API secret_token, crates.io secret_token, SSH private key passphrase and similar +/// Secrets like GitHub API secret_token, crates.io secret token, docker hub secret_token, SSH private key passphrase and similar /// must never go out of this crate. Never pass any secret to an external crate library as much as possible. /// The user has the source code under his fingers in this crate. So he knows nobody will mess with this code /// once he inspected and reviewed it. @@ -556,11 +564,27 @@ fn task_github_new_release() { /// The simple program flow of functions that need secrets is butchered to avoid secrets leaving this crate. /// Now it looks like a mess, but the goal is achieved. The secrets never leave this crate. +// region: Public API constants +// ANSI colors for Linux terminal +// https://github.com/shiena/ansicolor/blob/master/README.md +/// ANSI color +pub const RED: &str = "\x1b[31m"; +/// ANSI color +pub const GREEN: &str = "\x1b[32m"; +/// ANSI color +pub const YELLOW: &str = "\x1b[33m"; +/// ANSI color +pub const BLUE: &str = "\x1b[34m"; +/// ANSI color +pub const RESET: &str = "\x1b[0m"; +// endregion: Public API constants + +pub use cargo_auto_encrypt_secret_lib::EncryptedString; +pub use secrecy::ExposeSecret; + pub(crate) mod decrypt_mod { - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - use secrecy::ExposeSecret; + use crate::secrets_always_local_mod::*; /// The secrets must not leave this crate. /// They are never going into an external library crate. @@ -606,12 +630,7 @@ pub(crate) mod decrypt_mod { } pub(crate) mod encrypt_mod { - - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - - // bring trait to scope - use secrecy::ExposeSecret; + use crate::secrets_always_local_mod::*; /// The secrets must not leave this crate. /// They are never going into an external library crate. @@ -657,7 +676,7 @@ pub(crate) mod secrecy_mod { //! But I want to encrypt the content, so I will make a wrapper. //! The secrets must always be moved to secrecy types as soon as possible. - use cargo_auto_encrypt_secret_lib::EncryptedString; + use crate::secrets_always_local_mod::*; pub struct SecretEncryptedString { encrypted_string: EncryptedString, @@ -686,18 +705,8 @@ pub(crate) mod secrecy_mod { pub(crate) mod ssh_mod { - #[allow(unused_imports)] - use cargo_auto_lib::BLUE; - use cargo_auto_lib::GREEN; - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - use cargo_auto_lib::YELLOW; - use crate::secrets_always_local_mod::*; - // bring trait into scope - use secrecy::ExposeSecret; - pub struct SshContext { signed_passcode_is_a_secret: secrecy::SecretVec, decrypted_string: secrecy::SecretString, @@ -723,7 +732,7 @@ pub(crate) mod ssh_mod { self.decrypted_string = decryptor.return_secret_string().clone(); } - /// get token and encrypt + /// get secret_token and encrypt fn get_secret_token_and_encrypt(&self) -> cargo_auto_encrypt_secret_lib::EncryptedString { /// Internal function used only for test configuration /// @@ -738,7 +747,7 @@ pub(crate) mod ssh_mod { #[cfg(not(test))] fn get_secret_token() -> secrecy::SecretString { eprintln!(" "); - eprintln!(" {BLUE}Enter the API secret_token to encrypt:{RESET}"); + eprintln!(" {BLUE}Enter the secret_token to encrypt:{RESET}"); secrecy::SecretString::new( inquire::Password::new("") .without_confirmation() @@ -747,9 +756,9 @@ pub(crate) mod ssh_mod { .unwrap(), ) } - let token_is_a_secret = get_secret_token(); + let secret_token = get_secret_token(); // use this signed as password for symmetric encryption - let encryptor = encrypt_mod::Encryptor::new_for_encrypt(token_is_a_secret, &self.signed_passcode_is_a_secret); + let encryptor = encrypt_mod::Encryptor::new_for_encrypt(secret_token, &self.signed_passcode_is_a_secret); let encrypted_token = encryptor.encrypt_symmetric().unwrap(); // return @@ -798,7 +807,7 @@ pub(crate) mod ssh_mod { } None => { // ask user to think about adding with ssh-add - eprintln!(" {YELLOW}SSH key for encrypted token is not found in the ssh-agent.{RESET}"); + eprintln!(" {YELLOW}SSH key for encrypted secret_token is not found in the ssh-agent.{RESET}"); eprintln!(" {YELLOW}Without ssh-agent, you will have to type the private key passphrase every time. This is more secure, but inconvenient.{RESET}"); eprintln!(" {YELLOW}You can manually add the SSH identity to ssh-agent for 1 hour:{RESET}"); eprintln!(" {YELLOW}WARNING: using ssh-agent is less secure, because there is no need for user interaction.{RESET}"); @@ -819,6 +828,7 @@ pub(crate) mod ssh_mod { } } } + /// Expand path and check if identity file exists /// /// Inform the user how to generate identity file. @@ -828,9 +838,11 @@ pub(crate) mod ssh_mod { eprintln!("{RED}Identity file {identity_private_file_path_expanded} that contains the SSH private key does not exist! {RESET}"); eprintln!(" {YELLOW}Create the SSH key manually in bash with this command:{RESET}"); if identity_private_file_path_expanded.as_str().contains("github_api") { - eprintln!(r#"{GREEN}ssh-keygen -t ed25519 -f "{identity_private_file_path_expanded}" -C "github API secret_token"{RESET}"#); + eprintln!(r#"{GREEN}ssh-keygen -t ed25519 -f "{identity_private_file_path_expanded}" -C "github api secret_token"{RESET}"#); } else if identity_private_file_path_expanded.as_str().contains("crates_io") { eprintln!(r#"{GREEN}ssh-keygen -t ed25519 -f "{identity_private_file_path_expanded}" -C "crates io secret_token"{RESET}"#); + } else if identity_private_file_path_expanded.as_str().contains("docker_hub") { + eprintln!(r#"{GREEN}ssh-keygen -t ed25519 -f "{identity_private_file_path_expanded}" -C "docker hub secret_token"{RESET}"#); } eprintln!(" "); panic!("{RED}Error: File {identity_private_file_path_expanded} does not exist! {RESET}"); @@ -844,23 +856,17 @@ pub(crate) mod github_mod { //! Every API call needs the GitHub API secret_token. This is a secret important just like a password. //! I don't want to pass this secret to an "obscure" library crate that is difficult to review. //! This secret will stay here in this codebase that every developer can easily inspect. - //! Instead of the token, I will pass the struct GitHubClient with the trait SendToGitHubApi. - //! This way, the secret token will be encapsulated. + //! Instead of the secret_token, I will pass the struct GitHubClient with the trait SendToGitHubApi. + //! This way, the secret_token will be encapsulated. + use crate::secrets_always_local_mod::*; use cargo_auto_github_lib as cgl; - - use cargo_auto_lib::BLUE; - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - use reqwest::Client; - // bring trait into scope - use secrecy::ExposeSecret; /// Struct GitHubClient contains only private fields /// This fields are accessible only to methods in implementation of traits. pub struct GitHubClient { - /// Passcode for encrypt the token_is_a_secret to encrypted_token in memory. + /// Passcode for encrypt the secret_token to encrypted_token in memory. /// So that the secret is in memory as little as possible as plain text. /// For every session (program start) a new random passcode is created. session_passcode: secrecy::SecretVec, @@ -872,9 +878,9 @@ pub(crate) mod github_mod { impl GitHubClient { /// Create new GitHub client /// - /// Interactively ask the user to input the GitHub token. - pub fn new_interactive_input_token() -> Self { - let mut github_client = Self::new_wo_token(); + /// Interactively ask the user to input the GitHub secret_token. + pub fn new_interactive_input_secret_token() -> Self { + let mut github_client = Self::new_wo_secret_token(); println!("{BLUE}Enter the GitHub API secret_token:{RESET}"); github_client.encrypted_token = @@ -884,8 +890,8 @@ pub(crate) mod github_mod { github_client } - /// Create new GitHub client without token - fn new_wo_token() -> Self { + /// Create new GitHub client without secret_token + fn new_wo_secret_token() -> Self { /// Internal function Generate a random password fn random_byte_passcode() -> [u8; 32] { let mut password = [0_u8; 32]; @@ -902,25 +908,25 @@ pub(crate) mod github_mod { /// Use the stored API secret_token /// - /// If the token not exists ask user to interactively input the token. - /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the token. + /// If the secret_token not exists ask user to interactively input the secret_token. + /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the secret_token. /// it is then possible also to have the ssh key in ssh-agent and write the passphrase only once. - /// But this great user experience comes with security concerns. The token is accessible if the attacker is very dedicated. - pub fn new_with_stored_token() -> Self { + /// But this great user experience comes with security concerns. The secret_token is accessible if the attacker is very dedicated. + pub fn new_with_stored_secret_token() -> Self { /// Internal function for DRY Don't Repeat Yourself - fn read_token_and_decrypt_return_github_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> GitHubClient { - // read the token and decrypt + fn read_secret_token_and_decrypt_return_github_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> GitHubClient { + // read the secret_token and decrypt cargo_auto_encrypt_secret_lib::decrypt_with_ssh_interactive_from_file(&mut ssh_context, encrypted_string_file_path); - let token_is_a_secret = ssh_context.get_decrypted_string(); - let mut github_client = GitHubClient::new_wo_token(); - github_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(token_is_a_secret, &github_client.session_passcode); + let secret_token = ssh_context.get_decrypted_string(); + let mut github_client = GitHubClient::new_wo_secret_token(); + github_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(secret_token, &github_client.session_passcode); github_client } - let encrypted_string_file_path = camino::Utf8Path::new("~/.ssh/github_api_token_encrypted.txt"); + let encrypted_string_file_path = camino::Utf8Path::new("~/.ssh/github_api_secret_token_encrypted.txt"); let encrypted_string_file_path_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(encrypted_string_file_path); - let identity_private_file_path = camino::Utf8Path::new("~/.ssh/github_api_token_ssh_1"); + let identity_private_file_path = camino::Utf8Path::new("~/.ssh/github_api_secret_token_ssh_1"); let _identity_private_file_path_expanded = crate::secrets_always_local_mod::ssh_mod::expand_path_check_private_key_exists(identity_private_file_path); if !encrypted_string_file_path_expanded.exists() { @@ -928,27 +934,27 @@ pub(crate) mod github_mod { println!(" {BLUE}Do you want to store the GitHub API secret_token encrypted with an SSH key? (y/n){RESET}"); let answer = inquire::Text::new("").prompt().unwrap(); if answer.to_lowercase() != "y" { - // enter the token manually, not storing - return Self::new_interactive_input_token(); + // enter the secret_token manually, not storing + return Self::new_interactive_input_secret_token(); } else { - // get the passphrase and token interactively + // get the passphrase and secret_token interactively let mut ssh_context = super::ssh_mod::SshContext::new(); - // encrypt and save the encrypted token + // encrypt and save the encrypted secret_token cargo_auto_encrypt_secret_lib::encrypt_with_ssh_interactive_save_file(&mut ssh_context, identity_private_file_path, encrypted_string_file_path); - // read the token and decrypt, return GitHubClient - read_token_and_decrypt_return_github_client(ssh_context, encrypted_string_file_path) + // read the secret_token and decrypt, return GitHubClient + read_secret_token_and_decrypt_return_github_client(ssh_context, encrypted_string_file_path) } } else { // file exists let ssh_context = super::ssh_mod::SshContext::new(); - // read the token and decrypt, return GitHubClient - read_token_and_decrypt_return_github_client(ssh_context, encrypted_string_file_path) + // read the secret_token and decrypt, return GitHubClient + read_secret_token_and_decrypt_return_github_client(ssh_context, encrypted_string_file_path) } } - /// decrypts the secret token in memory + /// decrypts the secret_token in memory #[allow(dead_code)] - pub fn decrypt_token_in_memory(&self) -> secrecy::SecretString { + pub fn decrypt_secret_token_in_memory(&self) -> secrecy::SecretString { self.encrypted_token.expose_decrypted_secret(&self.session_passcode) } } @@ -959,10 +965,10 @@ pub(crate) mod github_mod { /// /// This function encapsulates the secret API secret_token. /// The RequestBuilder is created somewhere in the library crate. - /// The client can be passed to the library. It will not reveal the secret token. + /// The client can be passed to the library. It will not reveal the secret_token. fn send_to_github_api(&self, req: reqwest::blocking::RequestBuilder) -> serde_json::Value { // I must build the request to be able then to inspect it. - let req = req.bearer_auth(self.decrypt_token_in_memory().expose_secret()).build().unwrap(); + let req = req.bearer_auth(self.decrypt_secret_token_in_memory().expose_secret()).build().unwrap(); // region: Assert the correct url and https // It is important that the request coming from a external crate/library @@ -995,11 +1001,11 @@ pub(crate) mod github_mod { /// /// This function encapsulates the secret API secret_token. /// The RequestBuilder is created somewhere in the library crate. - /// The client can be passed to the library. It will not reveal the secret token. + /// The client can be passed to the library. It will not reveal the secret_token. /// This is basically an async fn, but use of `async fn` in public traits is discouraged... async fn upload_to_github(&self, req: reqwest::RequestBuilder) -> serde_json::Value { // I must build the request to be able then to inspect it. - let req = req.bearer_auth(self.decrypt_token_in_memory().expose_secret()).build().unwrap(); + let req = req.bearer_auth(self.decrypt_secret_token_in_memory().expose_secret()).build().unwrap(); // region: Assert the correct url and https // It is important that the request coming from a external crate/library @@ -1035,21 +1041,17 @@ pub(crate) mod crates_io_mod { //! Publish to crates.io needs the crates.io secret_token. This is a secret important just like a password. //! I don't want to pass this secret to an "obscure" library crate that is difficult to review. //! This secret will stay here in this codebase that every developer can easily inspect. - //! Instead of the token, I will pass the struct CratesIoClient with the trait SendToCratesIo. - //! This way, the secret token will be encapsulated. - - use cargo_auto_lib::BLUE; - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - use cargo_auto_lib::YELLOW; + //! Instead of the secret_token, I will pass the struct CratesIoClient with the trait SendToCratesIo. + //! This way, the secret_token will be encapsulated. - // bring trait into scope - use secrecy::ExposeSecret; + use crate::secrets_always_local_mod::*; + use cargo_auto_lib::ShellCommandLimitedDoubleQuotesSanitizer; + use cargo_auto_lib::ShellCommandLimitedDoubleQuotesSanitizerTrait; /// Struct CratesIoClient contains only private fields /// This fields are accessible only to methods in implementation of traits. pub struct CratesIoClient { - /// Passcode for encrypt the token_is_a_secret to encrypted_token in memory. + /// Passcode for encrypt the secret_token to encrypted_token in memory. /// So that the secret is in memory as little as possible as plain text. /// For every session (program start) a new random passcode is created. session_passcode: secrecy::SecretVec, @@ -1063,8 +1065,8 @@ pub(crate) mod crates_io_mod { /// /// Interactively ask the user to input the crates.io secret_token. #[allow(dead_code)] - pub fn new_interactive_input_token() -> Self { - let mut crates_io_client = Self::new_wo_token(); + pub fn new_interactive_input_secret_token() -> Self { + let mut crates_io_client = Self::new_wo_secret_token(); println!("{BLUE}Enter the crates.io secret_token:{RESET}"); crates_io_client.encrypted_token = @@ -1074,9 +1076,9 @@ pub(crate) mod crates_io_mod { crates_io_client } - /// Create new CratesIo client without token + /// Create new CratesIo client without secret_token #[allow(dead_code)] - fn new_wo_token() -> Self { + fn new_wo_secret_token() -> Self { /// Internal function Generate a random password fn random_byte_passcode() -> [u8; 32] { let mut password = [0_u8; 32]; @@ -1093,26 +1095,35 @@ pub(crate) mod crates_io_mod { /// Use the stored crates.io secret_token /// - /// If the token not exists ask user to interactively input the token. - /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the token. + /// If the secret_token not exists ask user to interactively input the secret_token. + /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the secret_token. /// It is then possible also to have the ssh key in ssh-agent and write the passphrase only once. - /// But this great user experience comes with security concerns. The token is accessible if the attacker is very dedicated. + /// But this great user experience comes with security concerns. The secret_token is accessible if the attacker is very dedicated. #[allow(dead_code)] - pub fn new_with_stored_token() -> Self { + pub fn new_with_stored_secret_token() -> Self { /// Internal function for DRY Don't Repeat Yourself - fn read_token_and_decrypt_return_crates_io_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> CratesIoClient { + fn read_secret_token_and_decrypt_return_crates_io_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> CratesIoClient { cargo_auto_encrypt_secret_lib::decrypt_with_ssh_interactive_from_file(&mut ssh_context, encrypted_string_file_path); - let token_is_a_secret = ssh_context.get_decrypted_string(); - let mut crates_io_client = CratesIoClient::new_wo_token(); - crates_io_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(token_is_a_secret, &crates_io_client.session_passcode); + let secret_token = ssh_context.get_decrypted_string(); + let mut crates_io_client = CratesIoClient::new_wo_secret_token(); + crates_io_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(secret_token, &crates_io_client.session_passcode); crates_io_client } - let encrypted_string_file_path = camino::Utf8Path::new("~/.ssh/crates_io_token_encrypted.txt"); + // check if the plain-text file from `podman login` exists and warn the user because it is a security vulnerability. + let file_auth = "~/.cargo/credentials.toml"; + let file_auth = camino::Utf8Path::new(file_auth); + // TODO: check for env variable also? + let file_auth_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(file_auth); + let file_auth_expanded = camino::Utf8Path::new(&file_auth_expanded); + if file_auth_expanded.exists() { + eprintln!("{RED}Security vulnerability: Found the cargo file with plain-text secret_token: {file_auth_expanded}. It would be better to inspect and remove it. {RESET}") + } + + let encrypted_string_file_path = camino::Utf8Path::new("~/.ssh/crates_io_secret_token_encrypted.txt"); let encrypted_string_file_path_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(encrypted_string_file_path); - let identity_private_file_path = camino::Utf8Path::new("~/.ssh/crates_io_token_ssh_1"); - + let identity_private_file_path = camino::Utf8Path::new("~/.ssh/crates_io_secret_token_ssh_1"); let _identity_private_file_path_expanded = crate::secrets_always_local_mod::ssh_mod::expand_path_check_private_key_exists(identity_private_file_path); if !encrypted_string_file_path_expanded.exists() { @@ -1120,44 +1131,189 @@ pub(crate) mod crates_io_mod { println!(" {BLUE}Do you want to store the crates.io secret_token encrypted with an SSH key? (y/n){RESET}"); let answer = inquire::Text::new("").prompt().unwrap(); if answer.to_lowercase() != "y" { - // enter the token manually, not storing - return Self::new_interactive_input_token(); + // enter the secret_token manually, not storing + return Self::new_interactive_input_secret_token(); } else { - // get the passphrase and token interactively + // get the passphrase and secret_token interactively let mut ssh_context = super::ssh_mod::SshContext::new(); - // encrypt and save the encrypted token + // encrypt and save the encrypted secret_token cargo_auto_encrypt_secret_lib::encrypt_with_ssh_interactive_save_file(&mut ssh_context, identity_private_file_path, encrypted_string_file_path); - // read the token and decrypt, return CratesIoClient - read_token_and_decrypt_return_crates_io_client(ssh_context, encrypted_string_file_path) + // read the secret_token and decrypt, return CratesIoClient + read_secret_token_and_decrypt_return_crates_io_client(ssh_context, encrypted_string_file_path) } } else { // file exists let ssh_context = super::ssh_mod::SshContext::new(); - // read the token and decrypt, return CratesIoClient - read_token_and_decrypt_return_crates_io_client(ssh_context, encrypted_string_file_path) + // read the secret_token and decrypt, return CratesIoClient + read_secret_token_and_decrypt_return_crates_io_client(ssh_context, encrypted_string_file_path) } } - /// decrypts the secret token in memory + /// decrypts the secret_token in memory #[allow(dead_code)] - pub fn decrypt_token_in_memory(&self) -> secrecy::SecretString { + pub fn decrypt_secret_token_in_memory(&self) -> secrecy::SecretString { self.encrypted_token.expose_decrypted_secret(&self.session_passcode) } /// Publish to crates.io /// /// This function encapsulates the secret crates.io secret_token. - /// The client can be passed to the library. It will not reveal the secret token. + /// The client can be passed to the library. It will not reveal the secret_token. #[allow(dead_code)] pub fn publish_to_crates_io(&self) { - // print command without the token - println!("{YELLOW}cargo publish --token [REDACTED]{RESET}"); - let shell_command = format!("cargo publish --token {}", self.decrypt_token_in_memory().expose_secret()); - let status = std::process::Command::new("sh").arg("-c").arg(shell_command).spawn().unwrap().wait().unwrap(); - let exit_code = status.code().expect(&format!("{RED}Error: publish to crates.io error. {RESET}")); - if exit_code != 0 { - panic!("{RED}Error: publish to crates.io error {exit_code}. {RESET}"); + // the secret_token is redacted when print on screen + ShellCommandLimitedDoubleQuotesSanitizer::new(r#"cargo publish --token "{secret_token}" "#) + .unwrap_or_else(|e| panic!("{e}")) + .arg_secret("{secret_token}", &self.decrypt_secret_token_in_memory()) + .unwrap_or_else(|e| panic!("{e}")) + .run() + .unwrap_or_else(|e| panic!("{e}")); + } + } +} + +pub(crate) mod docker_hub_mod { + + //! Push to docker-hub needs the docker hub secret_token. This is a secret important just like a password. + //! I don't want to pass this secret to an "obscure" library crate that is difficult to review. + //! This secret will stay here in this codebase that every developer can easily inspect. + //! Instead of the secret_token, I will pass the struct DockerHubClient with the trait SendToDockerHub. + //! This way, the secret_token will be encapsulated. + + use crate::secrets_always_local_mod::*; + use cargo_auto_lib::ShellCommandLimitedDoubleQuotesSanitizer; + use cargo_auto_lib::ShellCommandLimitedDoubleQuotesSanitizerTrait; + + /// Struct DockerHubClient contains only private fields + /// This fields are accessible only to methods in implementation of traits. + pub struct DockerHubClient { + /// Passcode for encrypt the secret_token to encrypted_token in memory. + /// So that the secret is in memory as little as possible as plain text. + /// For every session (program start) a new random passcode is created. + session_passcode: secrecy::SecretVec, + + /// private field is set only once in the new() constructor + encrypted_token: super::secrecy_mod::SecretEncryptedString, + } + + impl DockerHubClient { + /// Create new DockerHub client + /// + /// Interactively ask the user to input the docker hub secret_token. + #[allow(dead_code)] + pub fn new_interactive_input_secret_token() -> Self { + let mut docker_hub_client = Self::new_wo_secret_token(); + + println!("{BLUE}Enter the docker hub secret_token:{RESET}"); + docker_hub_client.encrypted_token = + super::secrecy_mod::SecretEncryptedString::new_with_string(inquire::Password::new("").without_confirmation().prompt().unwrap(), &docker_hub_client.session_passcode); + + // return + docker_hub_client + } + + /// Create new DockerHub client without secret_token + #[allow(dead_code)] + fn new_wo_secret_token() -> Self { + /// Internal function Generate a random password + fn random_byte_passcode() -> [u8; 32] { + let mut password = [0_u8; 32]; + use aes_gcm::aead::rand_core::RngCore; + aes_gcm::aead::OsRng.fill_bytes(&mut password); + password + } + + let session_passcode = secrecy::SecretVec::new(random_byte_passcode().to_vec()); + let encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_string("".to_string(), &session_passcode); + + DockerHubClient { session_passcode, encrypted_token } + } + + /// Use the stored docker hub secret_token + /// + /// If the secret_token not exists ask user to interactively input the secret_token. + /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the secret_token. + /// It is then possible also to have the ssh key in ssh-agent and write the passphrase only once. + /// But this great user experience comes with security concerns. The secret_token is accessible if the attacker is very dedicated. + #[allow(dead_code)] + pub fn new_with_stored_secret_token(user_name: &str, registry: &str) -> Self { + /// Internal function for DRY Don't Repeat Yourself + fn read_secret_token_and_decrypt_return_docker_hub_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> DockerHubClient { + cargo_auto_encrypt_secret_lib::decrypt_with_ssh_interactive_from_file(&mut ssh_context, encrypted_string_file_path); + let secret_token = ssh_context.get_decrypted_string(); + let mut docker_hub_client = DockerHubClient::new_wo_secret_token(); + docker_hub_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(secret_token, &docker_hub_client.session_passcode); + docker_hub_client + } + + // check if the plain-text file from `podman login` exists and warn the user because it is a security vulnerability. + let file_auth = "${XDG_RUNTIME_DIR}/containers/auth.json"; + // TODO: check for env variable also? + if let Some(xdg_runtime_dir) = std::env::var_os("XDG_RUNTIME_DIR"){ + let xdg_runtime_dir=xdg_runtime_dir.to_string_lossy().to_string(); + let file_auth_expanded = file_auth.replace("${XDG_RUNTIME_DIR}", &xdg_runtime_dir); + let file_auth_expanded = camino::Utf8Path::new(&file_auth_expanded); + if file_auth_expanded.exists() { + eprintln!("{RED}Security vulnerability: Found the docker hub file with plain-text secret_token: {file_auth_expanded}. It would be better to inspect and remove it. {RESET}") + } } + + // registry: docker.io -> replace dot into "--"" + // username: bestiadev + let registry_escaped = registry.replace(".", "--"); + let encrypted_string_file_path = format!("~/.ssh/docker_hub_{registry_escaped}_{user_name}.txt"); + let encrypted_string_file_path = camino::Utf8Path::new(&encrypted_string_file_path); + let encrypted_string_file_path_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(encrypted_string_file_path); + + let identity_private_file_path = camino::Utf8Path::new("~/.ssh/docker_hub_secret_token_ssh_1"); + let _identity_private_file_path_expanded = crate::secrets_always_local_mod::ssh_mod::expand_path_check_private_key_exists(identity_private_file_path); + + if !encrypted_string_file_path_expanded.exists() { + // ask interactive + println!(" {BLUE}Do you want to store the docker hub secret_token encrypted with an SSH key? (y/n){RESET}"); + let answer = inquire::Text::new("").prompt().unwrap(); + if answer.to_lowercase() != "y" { + // enter the secret_token manually, not storing + return Self::new_interactive_input_secret_token(); + } else { + // get the passphrase and secret_token interactively + let mut ssh_context = super::ssh_mod::SshContext::new(); + // encrypt and save the encrypted secret_token + cargo_auto_encrypt_secret_lib::encrypt_with_ssh_interactive_save_file(&mut ssh_context, identity_private_file_path, encrypted_string_file_path); + // read the secret_token and decrypt, return DockerHubClient + read_secret_token_and_decrypt_return_docker_hub_client(ssh_context, encrypted_string_file_path) + } + } else { + // file exists + let ssh_context = super::ssh_mod::SshContext::new(); + // read the secret_token and decrypt, return DockerHubClient + read_secret_token_and_decrypt_return_docker_hub_client(ssh_context, encrypted_string_file_path) + } + } + + /// decrypts the secret_token in memory + #[allow(dead_code)] + pub fn decrypt_secret_token_in_memory(&self) -> secrecy::SecretString { + self.encrypted_token.expose_decrypted_secret(&self.session_passcode) + } + + /// Push to docker hub + /// + /// This function encapsulates the secret docker hub secret_token. + /// The client can be passed to the library. It will not reveal the secret_token. + #[allow(dead_code)] + pub fn push_to_docker_hub(&self, image_url: &str, user_name: &str) { + // the secret_token can be used in place of the password in --cred + ShellCommandLimitedDoubleQuotesSanitizer::new(r#"podman push --creds "{user_name}:{secret_token}" "{image_url}" "#) + .unwrap_or_else(|e| panic!("{e}")) + .arg("{user_name}", user_name) + .unwrap_or_else(|e| panic!("{e}")) + .arg_secret("{secret_token}", &self.decrypt_secret_token_in_memory()) + .unwrap_or_else(|e| panic!("{e}")) + .arg("{image_url}", image_url) + .unwrap_or_else(|e| panic!("{e}")) + .run() + .unwrap_or_else(|e| panic!("{e}")); } } } diff --git a/src/template_new_cli_mod.rs b/src/template_new_cli_mod.rs index be08817f..f78069fd 100644 --- a/src/template_new_cli_mod.rs +++ b/src/template_new_cli_mod.rs @@ -2,24 +2,25 @@ //! template for new_cli //! -//! An automation task copy the content of the template_new_wasm folder into this strings. -//! When installing a crate from crates.io, only the code is transferred. No additional files. +//! The template is downloaded from github +//! use crate::{GREEN, RED, RESET, YELLOW}; pub fn new_cli(arg_2: Option, arg_3: Option) { if arg_2.is_none() { - println!("{RED}Error: Project name argument is missing: `cargo auto new_cli project_name github_owner`{RESET}"); + println!("{RED}Error: Project name argument is missing: `cargo auto new_cli project_name github_owner_or_organization`{RESET}"); return; } if arg_3.is_none() { - println!("{RED}Error: Github owner argument is missing: `cargo auto new_cli project_name github_owner`{RESET}"); + println!("{RED}Error: Github owner argument is missing: `cargo auto new_cli project_name github_owner_or_organization`{RESET}"); return; } let project_name = arg_2.unwrap(); - let github_owner = arg_3.unwrap(); + let github_owner_or_organization = arg_3.unwrap(); + + copy_to_files(&project_name, &github_owner_or_organization); - copy_to_files(&project_name, &github_owner); println!(""); println!(" {YELLOW}The command `cargo auto new_cli` generated the directory `{project_name}`.{RESET}"); println!(" {YELLOW}You can open this new Rust project in VSCode:{RESET}",); @@ -29,2177 +30,54 @@ pub fn new_cli(arg_2: Option, arg_3: Option) { println!(" {YELLOW}and follow the detailed instructions.{RESET}"); } -pub fn copy_to_files(project_name: &str, github_owner: &str) { +pub fn copy_to_files(project_name: &str, github_owner_or_organization: &str) { let folder_path = std::path::Path::new(project_name); - std::fs::create_dir_all(folder_path).unwrap(); - for file_item in get_vec_file() { - // rename/replace the project_name - let file_name = file_item.file_name.replace("cargo_auto_template_new_cli", project_name); - let file_content = file_item.file_content.replace("cargo_auto_template_new_cli", project_name); - let file_content = file_content.replace("/github_owner/", &format!("/{github_owner}/")); - - // create directory if needed - std::fs::create_dir_all(folder_path.join(&file_name).parent().unwrap()).unwrap(); - std::fs::write(folder_path.join(&file_name), file_content.as_bytes()).unwrap(); - } -} - -pub fn get_vec_file() -> Vec { - let mut vec_file = vec![]; - - // region: files copied into strings by automation tasks - vec_file.push(crate::FileItem { - file_name: "rustfmt.toml", - file_content: r###"max_width = 200 -"###, - }); - vec_file.push(crate::FileItem { - file_name: ".gitattributes", - file_content: r###"# Specific git config for the project - -# Declare files that will always have LF line endings on checkout. -*.rs text eol=lf -*.toml text eol=lf -*.md text eol=lf -*.json text eol=lf -*.json5 text eol=lf -*.lock text eol=lf -*.yml text eol=lf -*.html text eol=lf -*.js text eol=lf -*.css text eol=lf -LICENSE text eol=lf -.gitignore text eol=lf -.gitattributes text eol=lf"###, - }); - vec_file.push(crate::FileItem { - file_name: ".gitignore", - file_content: r###"/target - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -# Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk - -# not needed in commits, but also not a problem if they are committed -/.automation_tasks_rs_file_hashes.json"###, - }); - vec_file.push(crate::FileItem { - file_name: "automation_tasks_rs/rustfmt.toml", - file_content: r###"max_width = 200"###, - }); - vec_file.push(crate::FileItem { - file_name: "automation_tasks_rs/.gitignore", - file_content: r###"/target -/logs - -# so the GitHub action gets the fresh libraries -Cargo.lock - -# not needed in commits, but also not a problem if they are committed -/.file_hashes.json -/.old_metadata.json -"###, - }); - vec_file.push(crate::FileItem{ - file_name :"automation_tasks_rs/src/main.rs", - file_content : r###"// automation_tasks_rs for cargo_auto_template_new_cli - -// region: library and modules with basic automation tasks - -// for projects that don't use GitHub, delete all the mentions of GitHub -mod secrets_always_local_mod; -use crate::secrets_always_local_mod::crates_io_mod; -use crate::secrets_always_local_mod::github_mod; - -use cargo_auto_github_lib as cgl; -use cargo_auto_lib as cl; - -use cl::GREEN; -use cl::RED; -use cl::RESET; -use cl::YELLOW; - -// traits must be in scope (Rust strangeness) -use cgl::SendToGitHubApi; -use cl::CargoTomlPublicApiMethods; -use cl::ShellCommandLimitedDoubleQuotesSanitizerTrait; - -// region: library with basic automation tasks - -fn main() { - std::panic::set_hook(Box::new(|panic_info| panic_set_hook(panic_info))); - tracing_init(); - cl::exit_if_not_run_in_rust_project_root_directory(); - - // get CLI arguments - let mut args = std::env::args(); - // the zero argument is the name of the program - let _arg_0 = args.next(); - match_arguments_and_call_tasks(args); -} - -// region: general functions - -/// Initialize tracing to file logs/automation_tasks_rs.log -/// -/// The folder logs/ is in .gitignore and will not be committed. -pub fn tracing_init() { - // uncomment this line to enable tracing to file - // let file_appender = tracing_appender::rolling::daily("logs", "automation_tasks_rs.log"); - - let offset = time::UtcOffset::current_local_offset().expect("should get local offset!"); - let timer = tracing_subscriber::fmt::time::OffsetTime::new(offset, time::macros::format_description!("[hour]:[minute]:[second].[subsecond digits:6]")); - - // Filter out logs from: hyper_util, reqwest - // A filter consists of one or more comma-separated directives - // target[span{field=value}]=level - // examples: tokio::net=info - // directives can be added with the RUST_LOG environment variable: - // export RUST_LOG=automation_tasks_rs=trace - // Unset the environment variable RUST_LOG - // unset RUST_LOG - let filter = tracing_subscriber::EnvFilter::from_default_env() - .add_directive("hyper_util=error".parse().unwrap_or_else(|e| panic!("{e}"))) - .add_directive("reqwest=error".parse().unwrap_or_else(|e| panic!("{e}"))); - - tracing_subscriber::fmt() - .with_file(true) - .with_max_level(tracing::Level::DEBUG) - .with_timer(timer) - .with_line_number(true) - .with_ansi(false) - //.with_writer(file_appender) - .with_env_filter(filter) - .init(); -} - -/// The original Rust report of the panic is ugly for the end user -/// -/// I use panics extensively to stop the execution. I am lazy to implement a super complicated error handling. -/// I just need to stop the execution on every little bit of error. This utility is for developers. They will understand me. -/// For errors I print the location. If the message contains "Exiting..." than it is a "not-error exit" and the location is not important. -fn panic_set_hook(panic_info: &std::panic::PanicInfo) { - let mut string_message = "".to_string(); - if let Some(message) = panic_info.payload().downcast_ref::() { - string_message = message.to_owned(); - } - if let Some(message) = panic_info.payload().downcast_ref::<&str>() { - string_message.push_str(message); - } - - tracing::debug!("{string_message}"); - eprintln!("{string_message}"); - - if !string_message.contains("Exiting...") { - let file = panic_info.location().unwrap().file(); - let line = panic_info.location().unwrap().line(); - let column = panic_info.location().unwrap().column(); - tracing::debug!("Location: {file}:{line}:{column}"); - eprintln!("Location: {file}:{line}:{column}"); - } -} - -// endregion: general functions - -// region: match, help and completion - -/// match arguments and call tasks functions -fn match_arguments_and_call_tasks(mut args: std::env::Args) { - // the first argument is the user defined task: (no argument for help), build, release,... - let arg_1 = args.next(); - match arg_1 { - None => print_help(), - Some(task) => { - if &task == "completion" { - completion(); - } else { - println!("{YELLOW}Running automation task: {task}{RESET}"); - if &task == "build" { - task_build(); - } else if &task == "release" { - task_release(); - } else if &task == "doc" { - task_doc(); - } else if &task == "test" { - task_test(); - } else if &task == "commit_and_push" { - let arg_2 = args.next(); - task_commit_and_push(arg_2); - } else if &task == "publish_to_crates_io" { - task_publish_to_crates_io(); - } else if &task == "github_new_release" { - task_github_new_release(); - } else { - eprintln!("{RED}Error: Task {task} is unknown.{RESET}"); - print_help(); - } - } - } - } -} - -/// write a comprehensible help for user defined tasks -fn print_help() { - println!( - r#" - {YELLOW}Welcome to cargo-auto !{RESET} - {YELLOW}This program automates your custom tasks when developing a Rust project.{RESET} - - {YELLOW}User defined tasks in automation_tasks_rs:{RESET} -{GREEN}cargo auto build{RESET} - {YELLOW}builds the crate in debug mode, fmt, increment version{RESET} -{GREEN}cargo auto release{RESET} - {YELLOW}builds the crate in release mode, fmt, increment version{RESET} -{GREEN}cargo auto doc{RESET} - {YELLOW}builds the docs, copy to docs directory{RESET} -{GREEN}cargo auto test{RESET} - {YELLOW}runs all the tests{RESET} -{GREEN}cargo auto commit_and_push "message"{RESET} - {YELLOW}commits with message and push with mandatory message{RESET} - {YELLOW}It is preferred to use SSH for git push to GitHub.{RESET} - {YELLOW}{YELLOW} - {YELLOW}On the very first commit, this task will initialize a new local git repository and create a remote GitHub repo.{RESET} - {YELLOW}For the GitHub API the task needs the Personal Access Token Classic from {RESET} - {YELLOW}You can choose to type the token every time or to store it in a file encrypted with an SSH key.{RESET} - {YELLOW}Then you can type the passphrase of the private key every time. This is pretty secure.{RESET} - {YELLOW}Somewhat less secure (but more comfortable) way is to store the private key in ssh-agent.{RESET} -{GREEN}cargo auto publish_to_crates_io{RESET} - {YELLOW}publish to crates.io, git tag{RESET} - {YELLOW}You need the API secret_token for publishing. Get the token on .{RESET} - {YELLOW}You can choose to type the token every time or to store it in a file encrypted with an SSH key.{RESET} - {YELLOW}Then you can type the passphrase of the private key every time. This is pretty secure.{RESET} - {YELLOW}Somewhat less secure (but more comfortable) way is to store the private key in ssh-agent.{RESET} -{GREEN}cargo auto github_new_release{RESET} - {YELLOW}creates new release on GitHub{RESET} - {YELLOW}For the GitHub API the task needs the Personal Access Token Classic from {RESET} - {YELLOW}You can choose to type the token every time or to store it in a file encrypted with an SSH key.{RESET} - {YELLOW}Then you can type the passphrase of the private key every time. This is pretty secure.{RESET} - {YELLOW}Somewhat less secure (but more comfortable) way is to store the private key in ssh-agent.{RESET} - - {YELLOW}© 2024 bestia.dev MIT License github.com/automation-tasks-rs/cargo-auto{RESET} -"# - ); - print_examples_cmd(); -} - -/// all example commands in one place -fn print_examples_cmd() { -/* - println!( - r#" - {YELLOW}run examples:{RESET} -{GREEN}cargo run --example plantuml1{RESET} -"# - ); -*/ -} - -/// sub-command for bash auto-completion of `cargo auto` using the crate `dev_bestia_cargo_completion` -fn completion() { - let args: Vec = std::env::args().collect(); - let word_being_completed = args[2].as_str(); - let last_word = args[3].as_str(); - - if last_word == "cargo-auto" || last_word == "auto" { - let sub_commands = vec!["build", "release", "doc", "test", "commit_and_push", "publish_to_crates_io", "github_new_release"]; - cl::completion_return_one_or_more_sub_commands(sub_commands, word_being_completed); - } - /* - // the second level if needed - else if last_word == "new" { - let sub_commands = vec!["x"]; - cl::completion_return_one_or_more_sub_commands(sub_commands, word_being_completed); - } - */ -} - -// endregion: match, help and completion - -// region: tasks - -/// cargo build -fn task_build() { - let cargo_toml = cl::CargoToml::read(); - cl::auto_version_increment_semver_or_date(); - cl::run_shell_command_static("cargo fmt").unwrap_or_else(|e| panic!("{e}")); - cl::run_shell_command_static("cargo build").unwrap_or_else(|e| panic!("{e}")); - println!( - r#" - {YELLOW}After `cargo auto build`, run the compiled binary, examples and/or tests{RESET} -{GREEN}./target/debug/{package_name} print world{RESET} - {YELLOW}If ok then{RESET} -{GREEN}./target/debug/{package_name} upper world{RESET} - {YELLOW}If ok then{RESET} -{GREEN}./target/debug/{package_name} upper WORLD{RESET} - {YELLOW}if ok then{RESET} -{GREEN}cargo auto release{RESET} -"#, - package_name = cargo_toml.package_name(), - ); - print_examples_cmd(); -} - -/// cargo build --release -fn task_release() { - let cargo_toml = cl::CargoToml::read(); - cl::auto_version_increment_semver_or_date(); - cl::auto_cargo_toml_to_md(); - cl::auto_lines_of_code(""); - - cl::run_shell_command_static("cargo fmt").unwrap_or_else(|e| panic!("{e}")); - cl::run_shell_command_static("cargo build --release").unwrap_or_else(|e| panic!("{e}")); - - cl::ShellCommandLimitedDoubleQuotesSanitizer::new(r#"strip "target/release/{package_name}" "#).unwrap_or_else(|e| panic!("{e}")) - .arg("{package_name}", &cargo_toml.package_name()).unwrap_or_else(|e| panic!("{e}")) - .run().unwrap_or_else(|e| panic!("{e}")); - - println!( - r#" - {YELLOW}After `cargo auto release`, run the compiled binary, examples and/or tests{RESET} -{GREEN}./target/release/{package_name} print world{RESET} - {YELLOW}If ok then{RESET} -{GREEN}./target/release/{package_name} upper world{RESET} - {YELLOW}If ok then{RESET} -{GREEN}./target/release/{package_name} upper WORLD{RESET} - {YELLOW}if ok then{RESET} -{GREEN}cargo auto doc{RESET} -"#, - package_name = cargo_toml.package_name(), - ); - print_examples_cmd(); -} - -/// cargo doc, then copies to /docs/ folder, because this is a GitHub standard folder -fn task_doc() { - let cargo_toml = cl::CargoToml::read(); - cl::auto_cargo_toml_to_md(); - cl::auto_lines_of_code(""); - cl::auto_plantuml(&cargo_toml.package_repository().unwrap()); - cl::auto_playground_run_code(); - cl::auto_md_to_doc_comments(); - - cl::run_shell_command_static("cargo doc --no-deps --document-private-items").unwrap_or_else(|e| panic!("{e}")); - // copy target/doc into docs/ because it is GitHub standard - cl::run_shell_command_static("rsync -a --info=progress2 --delete-after target/doc/ docs/").unwrap_or_else(|e| panic!("{e}")); - - // Create simple index.html file in docs directory - cl::ShellCommandLimitedDoubleQuotesSanitizer::new(r#"printf "\n" > docs/index.html"#).unwrap_or_else(|e| panic!("{e}")) - .arg("{url_sanitized_for_double_quote}", &cargo_toml.package_name().replace("-", "_")).unwrap_or_else(|e| panic!("{e}")) - .run().unwrap_or_else(|e| panic!("{e}")); - - // pretty html - cl::auto_doc_tidy_html().unwrap_or_else(|e| panic!("{e}")); - cl::run_shell_command_static("cargo fmt").unwrap_or_else(|e| panic!("{e}")); - // message to help user with next move - println!( - r#" - {YELLOW}After `cargo auto doc`, ctrl-click on `docs/index.html`. - It will show the index.html in VSCode Explorer, then right-click and choose "Show Preview". - This works inside the CRUSTDE container, because of the extension "Live Preview" - - If ok then run the tests in code and the documentation code examples.{RESET} -{GREEN}cargo auto test{RESET} -"# - ); -} - -/// cargo test -fn task_test() { - cl::run_shell_command_static("cargo test").unwrap_or_else(|e| panic!("{e}")); - println!( - r#" - {YELLOW}After `cargo auto test`. If ok then {RESET} - {YELLOW}(commit message is mandatory){RESET} -{GREEN}cargo auto commit_and_push "message"{RESET} -"# - ); -} - -/// commit and push -fn task_commit_and_push(arg_2: Option) { - let Some(message) = arg_2 else { - eprintln!("{RED}Error: Message for commit is mandatory.{RESET}"); - // early exit - return; - }; - - // If needed, ask to create new local git repository - if !cl::git_is_local_repository() { - cl::new_local_repository(&message).unwrap(); + if folder_path.exists() { + panic!("{RED}Error: Folder {project_name} already exists! {RESET}"); } + std::fs::create_dir_all(folder_path).unwrap(); - // If needed, ask to create a GitHub remote repository - if !cgl::git_has_remote() || !cgl::git_has_upstream() { - let github_client = github_mod::GitHubClient::new_with_stored_token(); - cgl::new_remote_github_repository(&github_client).unwrap(); - cgl::description_and_topics_to_github(&github_client); + // download latest template.tar.gz + println!("{YELLOW}Downloading template.tar.gz...{RESET}"); + let file_name = "template.tar.gz"; + let path = "./template.tar.gz"; + let url = "https://github.com/automation-tasks-rs/cargo_auto_template_new_cli/releases/download/v0.0.3/template.tar.gz"; + let reqwest_client = reqwest::blocking::Client::new(); + let http_response = reqwest_client.get(url).send(); + if http_response.is_err() { + panic!("Error while retrieving data: {:#?}", http_response.err()); } else { - let github_client = github_mod::GitHubClient::new_with_stored_token(); - // if description or topics/keywords/tags have changed - cgl::description_and_topics_to_github(&github_client); - - // separate commit for docs if they changed, to not make a lot of noise in the real commit - if std::path::Path::new("docs").exists() { - cl::run_shell_command_static(r#"git add docs && git diff --staged --quiet || git commit -m "update docs" "#).unwrap_or_else(|e| panic!("{e}")); - } - - cl::add_message_to_unreleased(&message); - // the real commit of code - cl::ShellCommandLimitedDoubleQuotesSanitizer::new(r#"git add -A && git diff --staged --quiet || git commit -m "{message_sanitized_for_double_quote}" "#).unwrap_or_else(|e| panic!("{e}")) - .arg("{message_sanitized_for_double_quote}", &message).unwrap_or_else(|e| panic!("{e}")) - .run().unwrap_or_else(|e| panic!("{e}")); - - cl::run_shell_command_static("git push").unwrap_or_else(|e| panic!("{e}")); - } - - println!( - r#" - {YELLOW}After `cargo auto commit_and_push "message"`{RESET} -{GREEN}cargo auto publish_to_crates_io{RESET} -"# - ); -} - -/// publish to crates.io and git tag -fn task_publish_to_crates_io() { - let cargo_toml = cl::CargoToml::read(); - let package_name = cargo_toml.package_name(); - let version = cargo_toml.package_version(); - // take care of tags - let tag_name_version = cl::git_tag_sync_check_create_push(&version); - - // cargo publish with encrypted secret token - let crates_io_client = crates_io_mod::CratesIoClient::new_with_stored_token(); - crates_io_client.publish_to_crates_io(); - - println!( - r#" - {YELLOW}After `cargo auto publish_to_crates_io`, check in browser{RESET} -{GREEN}https://crates.io/crates/{package_name}{RESET} - {YELLOW}Add the dependency to your Rust project and check how it works.{RESET} -{GREEN}{package_name} = "{version}"{RESET} - - {YELLOW}First write the content of the release in the RELEASES.md in the `## Unreleased` section, then{RESET} - {YELLOW}Then create the GitHub-Release for {tag_name_version}.{RESET} -{GREEN}cargo auto github_new_release{RESET} -"# - ); -} - -/// create a new release on github -fn task_github_new_release() { - let cargo_toml = cl::CargoToml::read(); - let version = cargo_toml.package_version(); - // take care of tags - let tag_name_version = cl::git_tag_sync_check_create_push(&version); - - let github_owner = cargo_toml.github_owner().unwrap(); - let repo_name = cargo_toml.package_name(); - let now_date = cl::now_utc_date_iso(); - let release_name = format!("Version {} ({})", &version, now_date); - let branch = "main"; - - // First, the user must write the content into file RELEASES.md in the section ## Unreleased. - // Then the automation task will copy the content to GitHub release - let body_md_text = cl::body_text_from_releases_md().unwrap(); - - let github_client = github_mod::GitHubClient::new_with_stored_token(); - let json_value = github_client.send_to_github_api(cgl::github_api_create_new_release(&github_owner, &repo_name, &tag_name_version, &release_name, branch, &body_md_text)); - // early exit on error - if let Some(error_message) = json_value.get("message") { - eprintln!("{RED}{error_message}{RESET}"); - if let Some(errors) = json_value.get("errors") { - let errors = errors.as_array().unwrap(); - for error in errors.iter() { - if let Some(code) = error.get("code") { - eprintln!("{RED}{code}{RESET}"); - } - } - } - panic!("{RED}Call to GitHub API returned an error.{RESET}") - } - - // Create a new Version title in RELEASES.md. - cl::create_new_version_in_releases_md(&release_name).unwrap(); - - println!( - " - {YELLOW}New GitHub release created: {release_name}.{RESET} -" - ); - - // region: upload asset only for executables, not for libraries - - let release_id = json_value.get("id").unwrap().as_i64().unwrap().to_string(); - println!( - " - {YELLOW}Now uploading release asset. This can take some time if the files are big. Wait...{RESET} - " - ); - // compress files tar.gz - let tar_name = format!("{repo_name}-{tag_name_version}-x86_64-unknown-linux-gnu.tar.gz"); - - cl::ShellCommandLimitedDoubleQuotesSanitizer::new(r#"tar -zcvf "{tar_name_sanitized_for_double_quote}" "target/release/{repo_name_sanitized_for_double_quote}" "#).unwrap_or_else(|e| panic!("{e}")) - .arg("{tar_name_sanitized_for_double_quote}", &tar_name).unwrap_or_else(|e| panic!("{e}")) - .arg("{repo_name_sanitized_for_double_quote}", &repo_name).unwrap_or_else(|e| panic!("{e}")) - .run().unwrap_or_else(|e| panic!("{e}")); - - // upload asset - cgl::github_api_upload_asset_to_release(&github_client, &github_owner, &repo_name, &release_id, &tar_name); - - cl::ShellCommandLimitedDoubleQuotesSanitizer::new(r#"rm "{tar_name_sanitized_for_double_quote}" "#).unwrap_or_else(|e| panic!("{e}")) - .arg("{tar_name_sanitized_for_double_quote}", &tar_name).unwrap_or_else(|e| panic!("{e}")) - .run().unwrap_or_else(|e| panic!("{e}")); - - println!( - r#" - {YELLOW}Asset uploaded. Open and edit the description on GitHub Releases in the browser.{RESET} - "# - ); - - // endregion: upload asset only for executables, not for libraries - - println!( - r#" -{GREEN}https://github.com/{github_owner}/{repo_name}/releases{RESET} - "# - ); -} -// endregion: tasks -"###, -}); - vec_file.push(crate::FileItem { - file_name: "automation_tasks_rs/src/secrets_always_local_mod.rs", - file_content: r###"// secrets_always_local_mod.rs - -/// Secrets like GitHub API secret_token, crates.io secret_token, SSH private key passphrase and similar -/// must never go out of this crate. Never pass any secret to an external crate library as much as possible. -/// The user has the source code under his fingers in this crate. So he knows nobody will mess with this code -/// once he inspected and reviewed it. -/// All the modules are in one file to avoid clutter in the automation_tasks_rs folder. -/// The simple program flow of functions that need secrets is butchered to avoid secrets leaving this crate. -/// Now it looks like a mess, but the goal is achieved. The secrets never leave this crate. - -pub(crate) mod decrypt_mod { - - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - use secrecy::ExposeSecret; - - /// The secrets must not leave this crate. - /// They are never going into an external library crate. - /// This crate is "user code" and is easy to review and inspect. - pub(crate) struct Decryptor<'a> { - secret_string: secrecy::SecretString, - secret_passcode_bytes: &'a secrecy::SecretVec, - } - - impl<'a> Decryptor<'a> { - pub(crate) fn new_for_decrypt(secret_passcode_bytes: &'a secrecy::SecretVec) -> Self { - Decryptor { - secret_string: secrecy::SecretString::new("".to_string()), - secret_passcode_bytes, - } - } - pub(crate) fn return_secret_string(&self) -> &secrecy::SecretString { - &self.secret_string - } - - /// Decrypts encrypted_string with secret_passcode_bytes - /// - /// secret_passcode_bytes must be 32 bytes or more - /// Returns the secret_string - pub(crate) fn decrypt_symmetric(&mut self, encrypted_string: &cargo_auto_encrypt_secret_lib::EncryptedString) { - let encrypted_bytes = ::decode_vec(&encrypted_string.0).unwrap(); - //only first 32 bytes - let mut secret_passcode_32bytes = [0u8; 32]; - secret_passcode_32bytes.copy_from_slice(&self.secret_passcode_bytes.expose_secret()[0..32]); - - let cipher = ::new(&secret_passcode_32bytes.into()); - // nonce is salt - let nonce = rsa::sha2::digest::generic_array::GenericArray::from_slice(&encrypted_bytes[..12]); - let cipher_text = &encrypted_bytes[12..]; - - let Ok(decrypted_bytes) = aes_gcm::aead::Aead::decrypt(&cipher, nonce, cipher_text) else { - panic!("{RED}Error: Decryption failed. {RESET}"); - }; - let decrypted_string = String::from_utf8(decrypted_bytes).unwrap(); - self.secret_string = secrecy::SecretString::new(decrypted_string) - } - } -} - -pub(crate) mod encrypt_mod { - - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - - // bring trait to scope - use secrecy::ExposeSecret; - - /// The secrets must not leave this crate. - /// They are never going into an external library crate. - /// This crate is "user code" and is easy to review and inspect. - pub(crate) struct Encryptor<'a> { - secret_string: secrecy::SecretString, - secret_passcode_bytes: &'a secrecy::SecretVec, - } - - impl<'a> Encryptor<'a> { - pub(crate) fn new_for_encrypt(secret_string: secrecy::SecretString, secret_passcode_bytes: &'a secrecy::SecretVec) -> Self { - Encryptor { secret_string, secret_passcode_bytes } - } - - /// Encrypts secret_string with secret_passcode_bytes - /// - /// secret_passcode_bytes must be 32 bytes or more - /// returns the encrypted_string - pub(crate) fn encrypt_symmetric(&self) -> Option { - //only first 32 bytes - let mut secret_passcode_32bytes = [0u8; 32]; - secret_passcode_32bytes.copy_from_slice(&self.secret_passcode_bytes.expose_secret()[0..32]); - - let cipher = ::new(&secret_passcode_32bytes.into()); - // nonce is salt - let nonce = ::generate_nonce(&mut aes_gcm::aead::OsRng); - - let Ok(cipher_text) = aes_gcm::aead::Aead::encrypt(&cipher, &nonce, self.secret_string.expose_secret().as_bytes()) else { - panic!("{RED}Error: Encryption failed. {RESET}"); - }; - - let mut encrypted_bytes = nonce.to_vec(); - encrypted_bytes.extend_from_slice(&cipher_text); - let encrypted_string = ::encode_string(&encrypted_bytes); - Some(cargo_auto_encrypt_secret_lib::EncryptedString(encrypted_string)) - } - } -} - -pub(crate) mod secrecy_mod { - - //! The crate secrecy is probably great. - //! But I want to encrypt the content, so I will make a wrapper. - //! The secrets must always be moved to secrecy types as soon as possible. - - use cargo_auto_encrypt_secret_lib::EncryptedString; - - pub struct SecretEncryptedString { - encrypted_string: EncryptedString, - } - - impl SecretEncryptedString { - pub fn new_with_secret_string(secret_string: secrecy::SecretString, session_passcode: &secrecy::SecretVec) -> Self { - let encryptor = super::encrypt_mod::Encryptor::new_for_encrypt(secret_string, &session_passcode); - let encrypted_string = encryptor.encrypt_symmetric().unwrap(); - - SecretEncryptedString { encrypted_string } - } - - pub fn new_with_string(secret_string: String, session_passcode: &secrecy::SecretVec) -> Self { - let secret_string = secrecy::SecretString::new(secret_string); - Self::new_with_secret_string(secret_string, session_passcode) - } - - pub fn expose_decrypted_secret(&self, session_passcode: &secrecy::SecretVec) -> secrecy::SecretString { - let mut decryptor = super::decrypt_mod::Decryptor::new_for_decrypt(&session_passcode); - decryptor.decrypt_symmetric(&self.encrypted_string); - decryptor.return_secret_string().clone() - } - } -} - -pub(crate) mod ssh_mod { - - #[allow(unused_imports)] - use cargo_auto_lib::BLUE; - use cargo_auto_lib::GREEN; - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - use cargo_auto_lib::YELLOW; - - use crate::secrets_always_local_mod::*; - - // bring trait into scope - use secrecy::ExposeSecret; - - pub struct SshContext { - signed_passcode_is_a_secret: secrecy::SecretVec, - decrypted_string: secrecy::SecretString, - } - - impl SshContext { - pub fn new() -> Self { - SshContext { - signed_passcode_is_a_secret: secrecy::SecretVec::new(vec![]), - decrypted_string: secrecy::SecretString::new("".to_string()), - } - } - pub fn get_decrypted_string(&self) -> secrecy::SecretString { - self.decrypted_string.clone() - } - } - - impl cargo_auto_encrypt_secret_lib::SshContextTrait for SshContext { - /// decrypt from file data and write the decrypted secret in private field for later use in this crate, not in external library crates - fn decrypt_from_file_data(&mut self, encrypted_string: &cargo_auto_encrypt_secret_lib::EncryptedString) { - let mut decryptor = decrypt_mod::Decryptor::new_for_decrypt(&self.signed_passcode_is_a_secret); - decryptor.decrypt_symmetric(encrypted_string); - self.decrypted_string = decryptor.return_secret_string().clone(); - } - - /// get token and encrypt - fn get_secret_token_and_encrypt(&self) -> cargo_auto_encrypt_secret_lib::EncryptedString { - /// Internal function used only for test configuration - /// - /// It is not interactive, but reads from a env var. - #[cfg(test)] - fn get_secret_token() -> secrecy::SecretString { - secrecy::SecretString::new(std::env::var("TEST_TOKEN").unwrap()) - } - /// Internal function get_passphrase interactively ask user to type the passphrase - /// - /// This is used for normal code execution. - #[cfg(not(test))] - fn get_secret_token() -> secrecy::SecretString { - eprintln!(" "); - eprintln!(" {BLUE}Enter the API secret_token to encrypt:{RESET}"); - secrecy::SecretString::new( - inquire::Password::new("") - .without_confirmation() - .with_display_mode(inquire::PasswordDisplayMode::Masked) - .prompt() - .unwrap(), - ) - } - let token_is_a_secret = get_secret_token(); - // use this signed as password for symmetric encryption - let encryptor = encrypt_mod::Encryptor::new_for_encrypt(token_is_a_secret, &self.signed_passcode_is_a_secret); - - let encrypted_token = encryptor.encrypt_symmetric().unwrap(); - // return - encrypted_token - } - - /// Sign with ssh-agent or with identity_file - /// - /// get passphrase interactively - /// returns secret_password_bytes:Vec u8 - fn sign_with_ssh_agent_or_identity_file(&mut self, identity_private_file_path: &camino::Utf8Path, seed_bytes_not_a_secret: &[u8; 32]) { - /// Internal function used only for test configuration - /// - /// It is not interactive, but reads from a env var. - #[cfg(test)] - fn get_passphrase() -> secrecy::SecretString { - secrecy::SecretString::new(std::env::var("TEST_PASSPHRASE").unwrap()) - } - /// Internal function get_passphrase interactively ask user to type the passphrase - /// - /// This is used for normal code execution. - #[cfg(not(test))] - fn get_passphrase() -> secrecy::SecretString { - eprintln!(" "); - eprintln!(" {BLUE}Enter the passphrase for the SSH private key:{RESET}"); - secrecy::SecretString::new( - inquire::Password::new("") - .without_confirmation() - .with_display_mode(inquire::PasswordDisplayMode::Masked) - .prompt() - .unwrap(), - ) - } - - let identity_private_file_path_expanded = expand_path_check_private_key_exists(identity_private_file_path); - - let fingerprint_from_file = cargo_auto_encrypt_secret_lib::get_fingerprint_from_file(&identity_private_file_path_expanded); - - let mut ssh_agent_client = cargo_auto_encrypt_secret_lib::crate_ssh_agent_client(); - match cargo_auto_encrypt_secret_lib::ssh_add_list_contains_fingerprint(&mut ssh_agent_client, &fingerprint_from_file) { - Some(public_key) => { - // sign with public key from ssh-agent - let signature_is_the_new_secret_password = ssh_agent_client.sign(&public_key, seed_bytes_not_a_secret).unwrap(); - // only the data part of the signature goes into as_bytes. - self.signed_passcode_is_a_secret = secrecy::SecretVec::new(signature_is_the_new_secret_password.as_bytes().to_owned()); - } - None => { - // ask user to think about adding with ssh-add - eprintln!(" {YELLOW}SSH key for encrypted token is not found in the ssh-agent.{RESET}"); - eprintln!(" {YELLOW}Without ssh-agent, you will have to type the private key passphrase every time. This is more secure, but inconvenient.{RESET}"); - eprintln!(" {YELLOW}You can manually add the SSH identity to ssh-agent for 1 hour:{RESET}"); - eprintln!(" {YELLOW}WARNING: using ssh-agent is less secure, because there is no need for user interaction.{RESET}"); - eprintln!("{GREEN}ssh-add -t 1h {identity_private_file_path_expanded}{RESET}"); - - // just for test purpose I will use env var to read this passphrase. Don't use it in production. - - let passphrase_is_a_secret = get_passphrase(); - let private_key = ssh_key::PrivateKey::read_openssh_file(identity_private_file_path_expanded.as_std_path()).unwrap(); - let mut private_key = private_key.decrypt(passphrase_is_a_secret.expose_secret()).unwrap(); - - // FYI: this type of signature is compatible with ssh-agent because it does not involve namespace - let signature_is_the_new_secret_password = rsa::signature::SignerMut::try_sign(&mut private_key, seed_bytes_not_a_secret).unwrap(); - - // only the data part of the signature goes into as_bytes. - self.signed_passcode_is_a_secret = secrecy::SecretVec::new(signature_is_the_new_secret_password.as_bytes().to_owned()); - } - } - } - } - /// Expand path and check if identity file exists - /// - /// Inform the user how to generate identity file. - pub fn expand_path_check_private_key_exists(identity_private_file_path: &camino::Utf8Path) -> camino::Utf8PathBuf { - let identity_private_file_path_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(identity_private_file_path); - if !camino::Utf8Path::new(&identity_private_file_path_expanded).exists() { - eprintln!("{RED}Identity file {identity_private_file_path_expanded} that contains the SSH private key does not exist! {RESET}"); - eprintln!(" {YELLOW}Create the SSH key manually in bash with this command:{RESET}"); - if identity_private_file_path_expanded.as_str().contains("github_api") { - eprintln!(r#"{GREEN}ssh-keygen -t ed25519 -f "{identity_private_file_path_expanded}" -C "github API secret_token"{RESET}"#); - } else if identity_private_file_path_expanded.as_str().contains("crates_io") { - eprintln!(r#"{GREEN}ssh-keygen -t ed25519 -f "{identity_private_file_path_expanded}" -C "crates io secret_token"{RESET}"#); - } - eprintln!(" "); - panic!("{RED}Error: File {identity_private_file_path_expanded} does not exist! {RESET}"); - } - identity_private_file_path_expanded - } -} - -pub(crate) mod github_mod { - - //! Every API call needs the GitHub API secret_token. This is a secret important just like a password. - //! I don't want to pass this secret to an "obscure" library crate that is difficult to review. - //! This secret will stay here in this codebase that every developer can easily inspect. - //! Instead of the token, I will pass the struct GitHubClient with the trait SendToGitHubApi. - //! This way, the secret token will be encapsulated. - - use cargo_auto_github_lib as cgl; - - use cargo_auto_lib::BLUE; - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - - use reqwest::Client; - // bring trait into scope - use secrecy::ExposeSecret; - - /// Struct GitHubClient contains only private fields - /// This fields are accessible only to methods in implementation of traits. - pub struct GitHubClient { - /// Passcode for encrypt the token_is_a_secret to encrypted_token in memory. - /// So that the secret is in memory as little as possible as plain text. - /// For every session (program start) a new random passcode is created. - session_passcode: secrecy::SecretVec, - - /// private field is set only once in the new() constructor - encrypted_token: super::secrecy_mod::SecretEncryptedString, - } - - impl GitHubClient { - /// Create new GitHub client - /// - /// Interactively ask the user to input the GitHub token. - pub fn new_interactive_input_token() -> Self { - let mut github_client = Self::new_wo_token(); - - println!("{BLUE}Enter the GitHub API secret_token:{RESET}"); - github_client.encrypted_token = - super::secrecy_mod::SecretEncryptedString::new_with_string(inquire::Password::new("").without_confirmation().prompt().unwrap(), &github_client.session_passcode); - - // return - github_client - } - - /// Create new GitHub client without token - fn new_wo_token() -> Self { - /// Internal function Generate a random password - fn random_byte_passcode() -> [u8; 32] { - let mut password = [0_u8; 32]; - use aes_gcm::aead::rand_core::RngCore; - aes_gcm::aead::OsRng.fill_bytes(&mut password); - password - } - - let session_passcode = secrecy::SecretVec::new(random_byte_passcode().to_vec()); - let encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_string("".to_string(), &session_passcode); - - GitHubClient { session_passcode, encrypted_token } - } - - /// Use the stored API secret_token - /// - /// If the token not exists ask user to interactively input the token. - /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the token. - /// it is then possible also to have the ssh key in ssh-agent and write the passphrase only once. - /// But this great user experience comes with security concerns. The token is accessible if the attacker is very dedicated. - pub fn new_with_stored_token() -> Self { - /// Internal function for DRY Don't Repeat Yourself - fn read_token_and_decrypt_return_github_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> GitHubClient { - // read the token and decrypt - cargo_auto_encrypt_secret_lib::decrypt_with_ssh_interactive_from_file(&mut ssh_context, encrypted_string_file_path); - let token_is_a_secret = ssh_context.get_decrypted_string(); - let mut github_client = GitHubClient::new_wo_token(); - github_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(token_is_a_secret, &github_client.session_passcode); - github_client - } - - let encrypted_string_file_path = camino::Utf8Path::new("~/.ssh/github_api_token_encrypted.txt"); - let encrypted_string_file_path_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(encrypted_string_file_path); - - let identity_private_file_path = camino::Utf8Path::new("~/.ssh/github_api_token_ssh_1"); - let _identity_private_file_path_expanded = crate::secrets_always_local_mod::ssh_mod::expand_path_check_private_key_exists(identity_private_file_path); - - if !encrypted_string_file_path_expanded.exists() { - // ask interactive - println!(" {BLUE}Do you want to store the GitHub API secret_token encrypted with an SSH key? (y/n){RESET}"); - let answer = inquire::Text::new("").prompt().unwrap(); - if answer.to_lowercase() != "y" { - // enter the token manually, not storing - return Self::new_interactive_input_token(); - } else { - // get the passphrase and token interactively - let mut ssh_context = super::ssh_mod::SshContext::new(); - // encrypt and save the encrypted token - cargo_auto_encrypt_secret_lib::encrypt_with_ssh_interactive_save_file(&mut ssh_context, identity_private_file_path, encrypted_string_file_path); - // read the token and decrypt, return GitHubClient - read_token_and_decrypt_return_github_client(ssh_context, encrypted_string_file_path) - } - } else { - // file exists - let ssh_context = super::ssh_mod::SshContext::new(); - // read the token and decrypt, return GitHubClient - read_token_and_decrypt_return_github_client(ssh_context, encrypted_string_file_path) - } - } - - /// decrypts the secret token in memory - #[allow(dead_code)] - pub fn decrypt_token_in_memory(&self) -> secrecy::SecretString { - self.encrypted_token.expose_decrypted_secret(&self.session_passcode) - } - } - - /// trait from the crate library, so the 2 crates can share a function - impl cgl::SendToGitHubApi for GitHubClient { - /// Send GitHub API request - /// - /// This function encapsulates the secret API secret_token. - /// The RequestBuilder is created somewhere in the library crate. - /// The client can be passed to the library. It will not reveal the secret token. - fn send_to_github_api(&self, req: reqwest::blocking::RequestBuilder) -> serde_json::Value { - // I must build the request to be able then to inspect it. - let req = req.bearer_auth(self.decrypt_token_in_memory().expose_secret()).build().unwrap(); - - // region: Assert the correct url and https - // It is important that the request coming from a external crate/library - // is only sent always and only to GitHub API and not some other malicious url, - // because the request contains the secret GitHub API secret_token. - // And it must always use https - let host_str = req.url().host_str().unwrap(); - assert!(host_str == "api.github.com", "{RED}Error: Url is not correct: {host_str}. It must be always api.github.com.{RESET}"); - let scheme = req.url().scheme(); - assert!(scheme == "https", "{RED}Error: Scheme is not correct: {scheme}. It must be always https.{RESET}"); - // endregion: Assert the correct url and https - - let reqwest_client = reqwest::blocking::Client::new(); - let response_text = reqwest_client.execute(req).unwrap().text().unwrap(); - - let json_value: serde_json::Value = serde_json::from_str(&response_text).unwrap(); - - // panic if "message": String("Bad credentials"), - if let Some(m) = json_value.get("message") { - if m == "Bad credentials" { - panic!("{RED}Error: Bad credentials for GitHub API. {RESET}"); - } - } - - // return - json_value - } - - /// Upload to GitHub - /// - /// This function encapsulates the secret API secret_token. - /// The RequestBuilder is created somewhere in the library crate. - /// The client can be passed to the library. It will not reveal the secret token. - /// This is basically an async fn, but use of `async fn` in public traits is discouraged... - async fn upload_to_github(&self, req: reqwest::RequestBuilder) -> serde_json::Value { - // I must build the request to be able then to inspect it. - let req = req.bearer_auth(self.decrypt_token_in_memory().expose_secret()).build().unwrap(); - - // region: Assert the correct url and https - // It is important that the request coming from a external crate/library - // is only sent always and only to GitHub uploads and not some other malicious url, - // because the request contains the secret GitHub API secret_token. - // And it must always use https - let host_str = req.url().host_str().unwrap(); - assert!(host_str == "uploads.github.com", "{RED}Error: Url is not correct: {host_str}. It must be always api.github.com.{RESET}"); - let scheme = req.url().scheme(); - assert!(scheme == "https", "{RED}Error: Scheme is not correct: {scheme}. It must be always https.{RESET}"); - // endregion: Assert the correct url and https - - let reqwest_client = Client::new(); - let response_text = reqwest_client.execute(req).await.unwrap().text().await.unwrap(); - - let json_value: serde_json::Value = serde_json::from_str(&response_text).unwrap(); - - // panic if "message": String("Bad credentials"), - if let Some(m) = json_value.get("message") { - if m == "Bad credentials" { - panic!("{RED}Error: Bad credentials for GitHub API. {RESET}"); - } - } - - // return - json_value - } - } -} - -pub(crate) mod crates_io_mod { - - //! Publish to crates.io needs the crates.io secret_token. This is a secret important just like a password. - //! I don't want to pass this secret to an "obscure" library crate that is difficult to review. - //! This secret will stay here in this codebase that every developer can easily inspect. - //! Instead of the token, I will pass the struct CratesIoClient with the trait SendToCratesIo. - //! This way, the secret token will be encapsulated. - - use cargo_auto_lib::BLUE; - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - use cargo_auto_lib::YELLOW; - - // bring trait into scope - use secrecy::ExposeSecret; - - /// Struct CratesIoClient contains only private fields - /// This fields are accessible only to methods in implementation of traits. - pub struct CratesIoClient { - /// Passcode for encrypt the token_is_a_secret to encrypted_token in memory. - /// So that the secret is in memory as little as possible as plain text. - /// For every session (program start) a new random passcode is created. - session_passcode: secrecy::SecretVec, - - /// private field is set only once in the new() constructor - encrypted_token: super::secrecy_mod::SecretEncryptedString, - } - - impl CratesIoClient { - /// Create new CratesIo client - /// - /// Interactively ask the user to input the crates.io secret_token. - #[allow(dead_code)] - pub fn new_interactive_input_token() -> Self { - let mut crates_io_client = Self::new_wo_token(); - - println!("{BLUE}Enter the crates.io secret_token:{RESET}"); - crates_io_client.encrypted_token = - super::secrecy_mod::SecretEncryptedString::new_with_string(inquire::Password::new("").without_confirmation().prompt().unwrap(), &crates_io_client.session_passcode); - - // return - crates_io_client - } - - /// Create new CratesIo client without token - #[allow(dead_code)] - fn new_wo_token() -> Self { - /// Internal function Generate a random password - fn random_byte_passcode() -> [u8; 32] { - let mut password = [0_u8; 32]; - use aes_gcm::aead::rand_core::RngCore; - aes_gcm::aead::OsRng.fill_bytes(&mut password); - password - } - - let session_passcode = secrecy::SecretVec::new(random_byte_passcode().to_vec()); - let encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_string("".to_string(), &session_passcode); - - CratesIoClient { session_passcode, encrypted_token } + let body = http_response.unwrap().bytes().unwrap(); + // Get the content of the response + std::fs::write(path, &body).expect(&format!("Download failed for {file_name}")); + } + + // decompress into folder_path + let tar_gz = std::fs::File::open(path).unwrap(); + let tar = flate2::read::GzDecoder::new(tar_gz); + let mut archive = tar::Archive::new(tar); + archive.unpack(folder_path).unwrap(); + std::fs::remove_file(path).unwrap(); + + // replace placeholders inside text files + for entry in walkdir::WalkDir::new(folder_path).into_iter().filter_map(Result::ok) { + if entry.file_type().is_file() { + // template has only valid utf8 files + println!("replace: {}", entry.path().to_string_lossy()); + let content = std::fs::read_to_string(entry.path()).unwrap(); + let content = content.replace("cargo_auto_template_new_cli", project_name); + let content = content.replace("automation-tasks-rs", github_owner_or_organization); + let content = content.replace("automation--tasks--rs", "automation-tasks-rs"); + std::fs::write(entry.path(), content).unwrap(); + } + } + // renaming files is tricky and must be traverse in reverse. + let mut traverse_reverse: Vec = walkdir::WalkDir::new(folder_path).into_iter().filter_map(Result::ok).collect(); + traverse_reverse.reverse(); + for entry in traverse_reverse.iter() { + if entry.file_name() == "cargo_auto_template_new_cli" { + println!("rename: {}", entry.path().to_string_lossy()); + std::fs::rename(entry.path(), entry.path().parent().unwrap().join(project_name)).unwrap(); } - - /// Use the stored crates.io secret_token - /// - /// If the token not exists ask user to interactively input the token. - /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the token. - /// It is then possible also to have the ssh key in ssh-agent and write the passphrase only once. - /// But this great user experience comes with security concerns. The token is accessible if the attacker is very dedicated. - #[allow(dead_code)] - pub fn new_with_stored_token() -> Self { - /// Internal function for DRY Don't Repeat Yourself - fn read_token_and_decrypt_return_crates_io_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> CratesIoClient { - cargo_auto_encrypt_secret_lib::decrypt_with_ssh_interactive_from_file(&mut ssh_context, encrypted_string_file_path); - let token_is_a_secret = ssh_context.get_decrypted_string(); - let mut crates_io_client = CratesIoClient::new_wo_token(); - crates_io_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(token_is_a_secret, &crates_io_client.session_passcode); - crates_io_client - } - - let encrypted_string_file_path = camino::Utf8Path::new("~/.ssh/crates_io_token_encrypted.txt"); - let encrypted_string_file_path_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(encrypted_string_file_path); - - let identity_private_file_path = camino::Utf8Path::new("~/.ssh/crates_io_token_ssh_1"); - - let _identity_private_file_path_expanded = crate::secrets_always_local_mod::ssh_mod::expand_path_check_private_key_exists(identity_private_file_path); - - if !encrypted_string_file_path_expanded.exists() { - // ask interactive - println!(" {BLUE}Do you want to store the crates.io secret_token encrypted with an SSH key? (y/n){RESET}"); - let answer = inquire::Text::new("").prompt().unwrap(); - if answer.to_lowercase() != "y" { - // enter the token manually, not storing - return Self::new_interactive_input_token(); - } else { - // get the passphrase and token interactively - let mut ssh_context = super::ssh_mod::SshContext::new(); - // encrypt and save the encrypted token - cargo_auto_encrypt_secret_lib::encrypt_with_ssh_interactive_save_file(&mut ssh_context, identity_private_file_path, encrypted_string_file_path); - // read the token and decrypt, return CratesIoClient - read_token_and_decrypt_return_crates_io_client(ssh_context, encrypted_string_file_path) - } - } else { - // file exists - let ssh_context = super::ssh_mod::SshContext::new(); - // read the token and decrypt, return CratesIoClient - read_token_and_decrypt_return_crates_io_client(ssh_context, encrypted_string_file_path) - } - } - - /// decrypts the secret token in memory - #[allow(dead_code)] - pub fn decrypt_token_in_memory(&self) -> secrecy::SecretString { - self.encrypted_token.expose_decrypted_secret(&self.session_passcode) - } - - /// Publish to crates.io - /// - /// This function encapsulates the secret crates.io secret_token. - /// The client can be passed to the library. It will not reveal the secret token. - #[allow(dead_code)] - pub fn publish_to_crates_io(&self) { - // print command without the token - println!("{YELLOW}cargo publish --token [REDACTED]{RESET}"); - let shell_command = format!("cargo publish --token {}", self.decrypt_token_in_memory().expose_secret()); - let status = std::process::Command::new("sh").arg("-c").arg(shell_command).spawn().unwrap().wait().unwrap(); - let exit_code = status.code().expect(&format!("{RED}Error: publish to crates.io error. {RESET}")); - if exit_code != 0 { - panic!("{RED}Error: publish to crates.io error {exit_code}. {RESET}"); - } - } - } -} -"###, - }); - vec_file.push(crate::FileItem { - file_name: "automation_tasks_rs/Cargo.toml", - file_content: r###"[package] -name = "automation_tasks_rs" -version = "1.0.0" -authors = ["bestia.dev"] -homepage = "https://bestia.dev" -edition = "2021" -description = "Automation tasks coded in Rust language for the workflow of Rust projects" -publish = false - -[dependencies] -cargo_auto_lib = "2.4.8" -cargo_auto_github_lib = "1.1.6" -cargo_auto_encrypt_secret_lib = "1.1.7" - -inquire = "0.7.0" -serde_json = {version= "1.0.114", features=["std"]} - -# the version of reqwest must be the same as the version in the library cargo_auto_github_lib -reqwest = { version = "0.12.3", features = ["blocking", "stream"] } - -camino = "1.1.6" -aes-gcm = "0.10.3" -ssh-key = { version = "0.6.4", features = [ "rsa", "encryption"] } -rsa = { version = "0.9.6", features = ["sha2","pem"] } -secrecy = { version="0.8.0", features=["alloc"]} -base64ct = {version = "1.6.0", features = ["alloc"] } - -tracing = "0.1.40" -tracing-subscriber = { version = "0.3.18", features = ["env-filter", "std", "fmt", "time"] } -tracing-appender="0.2.2" -time = {version="0.3.36", features=["macros","local-offset"]} -"###, - }); - vec_file.push(crate::FileItem { - file_name: "automation_tasks_rs/.vscode/settings.json", - file_content: r###"{ - "workbench.colorCustomizations": { - "titleBar.activeForeground": "#fff", - "titleBar.inactiveForeground": "#ffffffcc", - "titleBar.activeBackground": "#a81c1c", - "titleBar.inactiveBackground": "#630b0bcc" - }, - "spellright.language": [ - "en" - ], - "spellright.documentTypes": [ - "markdown", - "latex", - "plaintext" - ], - "files.associations": { - "LICENSE": "plain text" - }, - "rust-analyzer.showUnlinkedFileNotification": false, - "cSpell.words": [ - "Alla", - "alloc", - "appender", - "bestia", - "bestiadev", - "camino", - "CRUSTDE", - "decryptor", - "encryptor", - "endregion", - "keygen", - "Nazdravlje", - "new_cli", - "octocrab", - "passcode", - "plantuml", - "Prost", - "reqwest", - "rustdevuser", - "rustprojects", - "serde", - "sshadd", - "struct", - "subsecond", - "substack", - "thiserror", - "zcvf", - "zdravje", - "zeroize" - ] -}"###, - }); - vec_file.push(crate::FileItem { - file_name: "RELEASES.md", - file_content: r###"# Releases changelog of cargo_auto_template_new_cli - -All notable changes to this project will be documented in this file. -This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -The library releases will be published on crates.io. -The cargo-auto automation task will use the content of the section `## Unreleased` to create -the GitHub release consistently with this file. -The ongoing changes that are not released, are visible in the git commits and GitHub pull requests. -The TODO section is part of the [README.md](https://github.com/github_owner/cargo_auto_template_new_cli). - -## Unreleased - -## Version 0.0.1 - -- Rust project created with `cargo auto new_cli cargo_auto_template_new_cli` -"###, - }); - vec_file.push(crate::FileItem { - file_name: "src/bin/cargo_auto_template_new_cli/main.rs", - file_content: r###"//! src/bin/cargo_auto_template_new_cli/main.rs - -// This `main.rs` is the code for the CLI application. -// The build of this project will create the CLI application. -// The `main.rs` has all the stdin and stdout. -// The `lib.rs` must be in/out agnostic. That is the responsibility of the `main.rs` -// This `lib.rs` can be used as dependency crate for other projects. - -// The `main.rs` uses the `anyhow` error library. -// The `lib.rs` uses the `thiserror` library. - -// Linux terminal colors -use cargo_auto_template_new_cli_lib::{GREEN, RED, RESET, YELLOW}; - -/// entry point into the bin-executable -fn main() { - // logging is essential for every project - pretty_env_logger::init(); - - // super simple argument parsing. There are crates that can parse more complex arguments. - match std::env::args().nth(1).as_deref() { - None | Some("--help") | Some("-h") => print_help(), - Some("print") => match std::env::args().nth(2).as_deref() { - // second argument - Some(greet_name) => { - print_greet_name(greet_name); - } - None => println!("{RED}Error: Missing arguments `greet_name`.{RESET}"), - }, - Some("upper") => match std::env::args().nth(2).as_deref() { - // second argument - Some(greet_name) => { - // this can return an error. Here is the last place I can deal with the error. - match upper_greet_name(greet_name) { - // do nothing - Ok(()) => (), - // log error from anyhow - Err(err) => println!("{RED}Error: {err}{RESET}"), - } - } - None => println!("{RED}Error: Missing arguments `greet_name`.{RESET}"), - }, - _ => println!("{RED}Error: Unrecognized arguments. Try `cargo_auto_template_new_cli --help`{RESET}"), } } - -/// print help -fn print_help() { - println!( - r#" - {YELLOW}Welcome to cargo_auto_template_new_cli ! - This is a simple yet complete template for a CLI program written in Rust.{RESET} - -{GREEN}cargo_auto_template_new_cli --help{RESET} -{GREEN}cargo_auto_template_new_cli print world{RESET} -{GREEN}cargo_auto_template_new_cli upper world{RESET} - - {YELLOW}This command should return an error:{RESET} -{GREEN}cargo_auto_template_new_cli upper WORLD{RESET} - - {YELLOW}© 2024 bestia.dev MIT License github.com/automation-tasks-rs/cargo-auto{RESET} -"# - ); -} - -/// print my name -fn print_greet_name(greet_name: &str) { - // call the function from the `lib.rs` - println!("{}", cargo_auto_template_new_cli_lib::format_hello_phrase(greet_name)); -} - -/// print my name upper, can return error -fn upper_greet_name(greet_name: &str) -> anyhow::Result<()> { - // the function from `lib.rs`, can return error - // use the ? syntax to bubble the error up one level or continue (early return) - let upper = cargo_auto_template_new_cli_lib::format_upper_hello_phrase(greet_name)?; - println!("{}", upper); - // return - Ok(()) -} -"###, - }); - vec_file.push(crate::FileItem { - file_name: "src/lib.rs", - file_content: r###"// cargo_auto_template_new_cli/src/lib.rs - -// The `main.rs` has all the stdin and stdout. -// The `lib.rs` must be in/out agnostic. That is the responsibility of the `main.rs` -// The `lib.rs` does not have any real code. All the code is in modules in separate files. -// The `lib.rs` has just the list of modules, it publishes module's functions or class for the caller -// and it has some global stuff like the Error enum. - -// region: auto_md_to_doc_comments include README.md A //! -//! # cargo-auto -//! -//! **Automation tasks coded in Rust language for the workflow of Rust projects** -//! ***version: 2024.422.214 date: 2024-04-22 author: [bestia.dev](https://bestia.dev) repository: [GitHub](https://github.com/automation-tasks-rs/cargo-auto)*** -//! -//! ![maintained](https://img.shields.io/badge/maintained-green) -//! ![ready-for-use](https://img.shields.io/badge/ready_for_use-green) -//! ![rustlang](https://img.shields.io/badge/rustlang-orange) -//! ![automation](https://img.shields.io/badge/automation-orange) -//! ![workflow](https://img.shields.io/badge/workflow-orange) -//! -//! ![logo](https://raw.githubusercontent.com/automation-tasks-rs/cargo-auto/main/images/logo/logo_cargo_auto.svg) -//! cargo-auto is part of the [automation_tasks_rs](https://github.com/automation-tasks-rs) project -//! -//! [![crates.io](https://img.shields.io/crates/v/cargo-auto.svg)](https://crates.io/crates/cargo-auto) -//! [![Documentation](https://docs.rs/cargo-auto/badge.svg)](https://docs.rs/cargo-auto/) -//! [![crev reviews](https://web.crev.dev/rust-reviews/badge/crev_count/cargo-auto.svg)](https://web.crev.dev/rust-reviews/crate/cargo-auto/) -//! [![Lib.rs](https://img.shields.io/badge/Lib.rs-rust-orange.svg)](https://lib.rs/crates/cargo-auto/) -//! [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/automation-tasks-rs/cargo-auto/blob/master/LICENSE) -//! [![Rust](https://github.com/automation-tasks-rs/cargo-auto/workflows/rust_fmt_auto_build_test/badge.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -//! [![Newest docs](https://img.shields.io/badge/newest_docs-blue.svg)](https://automation-tasks-rs.github.io/cargo-auto/cargo_auto/index.html) -//! ![cargo-auto](https://bestia.dev/webpage_hit_counter/get_svg_image/959103982.svg) -//! -//! [![Lines in Rust code](https://img.shields.io/badge/Lines_in_Rust-3145-green.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -//! [![Lines in Doc comments](https://img.shields.io/badge/Lines_in_Doc_comments-1203-blue.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -//! [![Lines in Comments](https://img.shields.io/badge/Lines_in_comments-704-purple.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -//! [![Lines in examples](https://img.shields.io/badge/Lines_in_examples-0-yellow.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -//! [![Lines in tests](https://img.shields.io/badge/Lines_in_tests-10949-orange.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -//! -//! Hashtags: #maintained #ready-for-use #rustlang #automation #workflow -//! My projects on GitHub are more like a tutorial than a finished product: [bestia-dev tutorials](https://github.com/bestia-dev/tutorials_rust_wasm). -//! I recommend using the [CRUSTDE - Containerized Rust Development Environment](https://github.com/CRUSTDE-ContainerizedRustDevEnvrustde_cnt_img_pod) to write Rust projects on Linux, isolated from your system. -//! -//! ## Try it -//! -//! First, we will use `cargo-auto` to create a new empty CLI Rust project similar to `cargo new`, but with a more complete project structure. -//! -//! ```bash -//! cargo install cargo-auto -//! cargo auto new_cli my_hello_project -//! cd my_hello_project -//! cargo auto -//! # it lists all the prepared automation tasks -//! # try a few -//! cargo auto build -//! cargo auto release -//! cargo auto doc -//! cargo auto test -//! ``` -//! -//! We can also add `automation tasks` to an existing Rust project. -//! Inside your Rust project directory (the one with Cargo.toml) run: -//! -//! ```bash -//! cargo auto new_auto -//! cargo auto -//! # it lists all the prepared automation tasks -//! # try to build -//! cargo auto build -//! ``` -//! -//! Congratulations! You are already using `cargo-auto`. Simple as that. -//! Now you can modify the tasks to your needs. It is all Rust language. -//! -//! ## Motivation -//! -//! Cargo is a great tool for building Rust projects. It has all the basics: `cargo build`, `cargo build --release`, `cargo fmt`, `cargo test`, `cargo doc`,... -//! But sometimes we need to do more things like copying some files, publishing to FTP, or entering long commands. These repetitive tasks must be automated. -//! Task automation makes work easier and faster, and simplifies the workflow while improving the consistency and accuracy of workflows. -//! This is also sometimes referred to as "workflow automation." -//! There are many different build systems and task runners there: `make`, `cmake`, `shell scripts`, `cargo-xtask`, `cargo-make`, `cargo-task`, `cargo-script`, `cargo-run-script`, `runner`, `python scripts`, `powershell scripts`, `cmd prompt scripts`, ... -//! Sadly there is no standard in the Rust community for now. -//! I want something similar to [build.rs](https://doc.rust-lang.org/cargo/reference/build-scripts.html), so I can write my "tasks" in pure Rust I don't want to learn another meta language with weird syntax and difficulty to debug. So I will make something really simple, easy, rusty, and extensible. -//! -//! ## cargo auto subcommand -//! -//! The command `cargo install cargo-auto` will add a new subcommand to cargo: -//! -//! ```bash -//! cargo auto -//! ``` -//! -//! This binary is super simple. It has only 1 trivial dependency: `lazy_static`. -//! The binary only reads the CLI arguments and runs the `automation_tasks_rs` binary with them. If needed it will compile `automation_tasks_rs` first. -//! The code-flow of the source code of `cargo-auto` is simple, fully commented, and straightforward to audit. -//! The source code is on [GitHub](https://github.com/automation-tasks-rs/cargo-auto) with MIT open-source licensing. -//! -//! ## bash auto-completion -//! -//! With the help of the crate [dev_bestia_cargo_completion](https://crates.io/crates/dev_bestia_cargo_completion), the commands `cargo` and `cargo auto` get bash auto-completion. Try it! -//! -//! ## cargo auto new_cli -//! -//! I like very much that Rust has the command `cargo new project_name`. It creates a super simple Rust Hello project that can be built and run immediately. But this example is too simple. It lacks the basic file structures of a serious CLI program. -//! I composed an opinionated template for a Rust CLI project. It is easy to run: -//! -//! ```bash -//! cargo auto new_cli project_name -//! # then -//! cd project_name -//! cargo auto build -//! # then follow detailed instructions -//! ``` -//! -//! ## cargo auto new_wasm -//! -//! I composed an opinionated template for a simple Rust WASM project for a browser. It is very similar to the new_cli template but for WASM. -//! It is easy to run: -//! -//! ```bash -//! cargo auto new_wasm project_name -//! # then -//! cd project_name -//! cargo auto build -//! # then follow detailed instructions -//! ``` -//! -//! ## cargo auto new_pwa_wasm -//! -//! I composed an opinionated template for a simple Rust PWA-WASM project for a browser. It is very similar to the new_cli template but for WASM. It adds the PWA standard functionality to work as an offline app. -//! The template needs the title, name, long name, and description inside a `pwa.json5` file and the `icon512x512.png` file for the icons. -//! It is easy to run: -//! -//! ```bash -//! cargo auto new_pwa_wasm -//! # on first run it will just create the `pwa.json5` and `icon512x512.png` files -//! # modify these files with data for your new app and then repeat -//! cargo auto new_pwa_wasm -//! # then -//! cd project_name -//! cargo auto build -//! # then follow detailed instructions -//! ``` -//! -//! ## scripting with rust -//! -//! Rust is a compiled language. It is not really a scripting or interpreted language. But the compilation of small projects is really fast and can be ignored. Subsequent calls will use the already-built binary so the speed will be even faster. -//! This tool `cargo-auto` is meant for Rust projects, so it means that all the Rust infrastructure is already in place. -//! -//! ## automation_tasks_rs Rust sub-project -//! -//! The command `cargo auto new_auto` will create a new Rust sub-project`automation_tasks_rs` inside your `Rust project`. It should not interfere with the main Rust project. This directory will be added to git commits and pushed to remote repositories as part of the main project. It has its own `.gitignore` to avoid committing to its target directory. -//! The `automation_tasks_rs` helper project contains user-defined tasks in Rust code. Your tasks. This helper project should be opened in a new editor starting from the `automation_tasks_rs` directory. It does not share dependencies with the main project. It is completely separate and independent. -//! You can edit it and add your dependencies and Rust code. No limits. Freedom of expression. -//! This is now your code, your tasks, and your helper Rust project! -//! Because only you know what you want to automate and how to do it. -//! Never write secrets, passwords, passphrases, or tokens inside your Rust code. Because then it is pushed to GitHub and the whole world can read it in the next second! -//! Basic example (most of the useful functions are already there): -//! -//! ```rust ignore -//! /// match arguments and call tasks functions -//! fn match_arguments_and_call_tasks(mut args: std::env::Args){ -//! // the first argument is the user defined task: (no argument for help), build, release,... -//! let arg_1 = args.next(); -//! match arg_1 { -//! None => print_help(), -//! Some(task) => { -//! println!("Running auto task: {}", &task); -//! if &task == "build"{ -//! task_build(); -//! } else if &task == "release" { -//! task_release(); -//! } else if &task == "doc" { -//! task_doc(); -//! } else { -//! println!("Task {} is unknown.", &task); -//! print_help(); -//! } -//! } -//! } -//! } -//! -//! /// write a comprehensible help for user defined tasks -//! fn print_help() { -//! println!(r#" -//! User defined tasks in automation_tasks_rs: -//! cargo auto build - builds the crate in debug mode -//! cargo auto release - builds the crate in release mode -//! cargo auto docs - builds the docs -//! "#); -//! } -//! -//! // region: tasks -//! -//! /// cargo build -//! fn task_build() { -//! run_shell_command("cargo fmt"); -//! run_shell_command("cargo build"); -//! } -//! -//! /// cargo build --release -//! fn task_release() { -//! run_shell_command("cargo fmt"); -//! run_shell_command("cargo build --release"); -//! } -//! -//! /// cargo doc, then copies to /docs/ folder, because this is a github standard folder -//! fn task_doc() { -//! run_shell_command("cargo doc --no-deps --document-private-items"); -//! // copy target/doc into docs/ because it is github standard -//! run_shell_command("rsync -a --info=progress2 --delete-after target/doc/ docs/"); -//! // Create simple index.html file in docs directory -//! run_shell_command(&format!( -//! "printf \"\\n\" > docs/index.html", -//! cargo_toml.package_name().replace("-","_") -//! )); -//! run_shell_command("cargo fmt"); -//! } -//! -//! // endregion: tasks -//! -//! ``` -//! -//! ## more complex tasks -//! -//! You can write more complex tasks in Rust language. -//! For example in this project I use automation to create GitHub Releases: -//! Here is a pretty complex workspace with more sub-projects: -//! -//! There is no end to your imagination. If you write something that looks like it can help other developers, please share it with me and I will add it here. -//! -//! ## Development details -//! -//! Read the development details in a separate md file: -//! [DEVELOPMENT.md](https://github.com/automation-tasks-rs/cargo-auto/blob/main/DEVELOPMENT.md) -//! -//! ## Releases changelog -//! -//! Read the changelog in a separate md file: -//! [RELEASES.md](https://github.com/automation-tasks-rs/cargo-auto/blob/main/RELEASES.md) -//! -//! ## TODO -//! -//! Nothing big in the near future. -//! -//! ## Open-source and free as a beer -//! -//! My open-source projects are free as a beer (MIT license). -//! I just love programming. -//! But I need also to drink. If you find my projects and tutorials helpful, please buy me a beer by donating to my [PayPal](https://paypal.me/LucianoBestia). -//! You know the price of a beer in your local bar ;-) -//! So I can drink a free beer for your health :-) -//! [Na zdravje!](https://translate.google.com/?hl=en&sl=sl&tl=en&text=Na%20zdravje&op=translate) [Alla salute!](https://dictionary.cambridge.org/dictionary/italian-english/alla-salute) [Prost!](https://dictionary.cambridge.org/dictionary/german-english/prost) [Nazdravlje!](https://matadornetwork.com/nights/how-to-say-cheers-in-50-languages/) 🍻 -//! -//! [//bestia.dev](https://bestia.dev) -//! [//github.com/automation-tasks-rs](https://github.com/automation-tasks-rs) -//! [//bestiadev.substack.com](https://bestiadev.substack.com) -//! [//youtube.com/@bestia-dev-tutorials](https://youtube.com/@bestia-dev-tutorials) -//! -// endregion: auto_md_to_doc_comments include README.md A //! - -// access to modules -mod hello_mod; - -// `pub use` allows the caller of the lib to access modules functions, structs or all(*) -pub use hello_mod::format_hello_phrase; -pub use hello_mod::format_upper_hello_phrase; - -// The `main.rs` uses the `anyhow` error library. -// The `lib.rs` uses the `thiserror` library. -use thiserror::Error; - -/// all possible library errors for `thiserror` -#[derive(Error, Debug)] -pub enum LibraryError { - #[error("Name `{0}` is already uppercase.")] - Uppercase(String), - #[error("Unknown error.")] - Unknown, -} - -// ANSI colors for Linux terminal -// https://github.com/shiena/ansicolor/blob/master/README.md -#[allow(dead_code)] -pub const RED: &str = "\x1b[31m"; -#[allow(dead_code)] -pub const YELLOW: &str = "\x1b[33m"; -#[allow(dead_code)] -pub const GREEN: &str = "\x1b[32m"; -#[allow(dead_code)] -pub const RESET: &str = "\x1b[0m"; -"###, - }); - vec_file.push(crate::FileItem { - file_name: "src/hello_mod.rs", - file_content: r###"// cargo_auto_template_new_cli/src/hello_mod.rs - -//! All the real code is inside modules in separate files. -//! -//! This doc-comments will be compiled into the `docs`. - -/// format the hello phrase -pub fn format_hello_phrase(greet_name: &str) -> String { - log::info!("start format_hello_phrase()"); - // return - format!("Hello {}!", greet_name) -} - -/// format the hello phrase with uppercase name -/// if it is already uppercase, return error with thiserror -pub fn format_upper_hello_phrase(greet_name: &str) -> Result { - log::info!("start format_upper_hello_phrase()"); - // shadowing the same variable name: - let upper_greet_name = make_uppercase(greet_name); - if upper_greet_name == greet_name { - return Err(crate::LibraryError::Uppercase(greet_name.to_string())); - } - - // return - Ok(format!("Hello {}!", &upper_greet_name)) -} - -/// return uppercase -pub fn make_uppercase(greet_name: &str) -> String { - // return - greet_name.to_uppercase() -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - pub fn test_format_upper_hello_phrase() { - assert_eq!(format_upper_hello_phrase("abcd").expect("error"), "Hello ABCD!"); - assert!(format_upper_hello_phrase("ABCD").is_err()); - } - - #[test] - pub fn test_make_uppercase() { - assert_eq!(make_uppercase("abcd"), "ABCD"); - assert_eq!(make_uppercase("1234abcd"), "1234ABCD"); - assert_eq!(make_uppercase("čšž"), "ČŠŽ"); - } -} -"###, - }); - vec_file.push(crate::FileItem { - file_name: ".github/workflows/clear_all_cache.yml", - file_content: r###"name: cleanup caches on main - -# Configure Manual Trigger with workflow_dispatch -on: - workflow_dispatch: - -jobs: - cleanup: - runs-on: ubuntu-latest - permissions: - # `actions:write` permission is required to delete caches - # See also: https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28#delete-a-github-actions-cache-for-a-repository-using-a-cache-id - actions: write - contents: read - steps: - - name: checkout - uses: actions/checkout@v4 - - - name: Cleanup - run: | - gh extension install actions/gh-actions-cache - - REPO=${{ github.repository }} - printf "$REPO\n" - BRANCH=main - printf "$BRANCH\n" - - # loop until the list is empty, because it deletes only 30 per page - has_items=true - while [ "$has_items" = true ] - do - printf "\033[0;33m Fetching list of cache key\n\033[0m\n" - printf "\033[0;32m gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 \n\033[0m\n" - cache_keys=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 ) - # printf "$cache_keys\n" - if [ -z "$cache_keys" ]; then - printf "\033[0;35m gh actions-cache list returned nothing.\n\033[0m\n" - has_items=false - fi - ## Setting this to not fail the workflow while deleting cache keys. - set +e - for cacheKey in $cache_keys - do - # printf "\033[0;32m gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm\n\033[0m\n" - gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm - done - done - printf "\033[0;33m Done\n\033[0m\n" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -"###, - }); - vec_file.push(crate::FileItem { - file_name: ".github/workflows/rust_fmt_auto_build_test.yml", - file_content: r###"name: rust_fmt_auto_build_test - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -env: - CARGO_TERM_COLOR: always - -jobs: - rust_fmt_auto_build_test: - - runs-on: ubuntu-latest - - steps: - - name: checkout - uses: actions/checkout@v4 - - - name: cargo fmt -- --check - run: cargo fmt -- --check - - - name: Run cache for rust dependencies - uses: Swatinem/rust-cache@v2.7.3 - - - name: Configure sccache - run: printf "RUSTC_WRAPPER=sccache\n" >> $GITHUB_ENV; printf "SCCACHE_GHA_ENABLED=true\n" >> $GITHUB_ENV - - - name: Run sccache-cache for artifacts - uses: mozilla-actions/sccache-action@v0.0.4 - - - name: install and cache cargo-auto - uses: baptiste0928/cargo-install@v3.0.0 - with: - crate: cargo-auto - - - name: Cache for automation tasks - uses: actions/cache@v4.0.0 - with: - path: | - /home/runner/work/cargo-auto/cargo-auto/automation_tasks_rs/.file_hashes.json - /home/runner/work/cargo-auto/cargo-auto/automation_tasks_rs/target - /home/runner/work/cargo-auto/cargo-auto/automation_tasks_rs/Cargo.toml - key: automation_tasks_rs - - - name: cargo auto build - run: cargo auto build - - - name: cargo auto test - run: cargo auto test - -"###, - }); - vec_file.push(crate::FileItem { - file_name: ".github/workflows/docs_pages.yml", - file_content: r###"# Simple workflow for deploying static content to GitHub Pages -name: docs_pages - -on: - # Runs on pushes targeting the default branch - push: - branches: ["main"] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - # Single deploy job since we're just deploying - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Pages - uses: actions/configure-pages@v4 - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - # Upload entire repository - path: 'docs' - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 -"###, - }); - vec_file.push(crate::FileItem { - file_name: "Cargo.toml", - file_content: r###"[package] -name = "cargo_auto_template_new_cli" -version = "0.0.1" -description = "Basic Rust project template for CLI and library, more than just `cargo new hello`" -authors = ["bestia.dev"] -homepage = "https://bestia.dev" -edition = "2021" -license = "MIT" -readme = "README.md" -repository = "https://github.com/github_owner/cargo_auto_template_new_cli" -# Keyword must be only one word: lowercase letters, hyphens(-) or numbers, less then 35 characters, at most 5 keywords per crate -keywords = ["maintained", "work-in-progress", "rustlang"] -categories = ["command-line-interface"] -# publish as a cargo tool. Only this files. -publish = false -include = [ - "Cargo.toml", - "LICENSE", - "README.md", - "src/*" -] - -[dependencies] -log = "0.4" -pretty_env_logger="0.5.0" -thiserror = "1.0.30" -anyhow="1.0.56" - -[lib] -name = "cargo_auto_template_new_cli_lib" -path = "src/lib.rs" -# A flag for enabling documentation of this target. This is used by `cargo doc`. -doc = true - -[[bin]] -name = "cargo_auto_template_new_cli" -path = "src/bin/cargo_auto_template_new_cli/main.rs" -# A flag for enabling documentation of this target. This is used by `cargo doc`. -doc = true -"###, - }); - vec_file.push(crate::FileItem{ - file_name :"DEVELOPMENT.md", - file_content : r###"# Development details - -## CRUSTDE - Containerized Rust Development Environment - -I recommend using the CRUSTDE - Containerized Rust Development Environment to write Rust projects. Follow the instructions here . - -It is an isolated development environment that will not mess with you system. -It will work on Linux (tested on Debian) and inside WSL (Windows Subsystem for Linux). - -You just need to install the newer alternative to Docker: [podman](https://podman.io/). Then you download the prepared container image from DockerHub (3GB). And then a little juggling with ssh keys. All this is simplified by running a few bash scripts. Just follow the easy instructions. - -The container image contains cargo, rustc, wasm-pack, basic-http-server, cargo-auto and other utils that a Rust project needs. - -## Workflow with automation_tasks_rs and cargo-auto - -For easy workflow, use the automation tasks that are already coded in the sub-project `automation_tasks_rs`. This is a basic workflow: - -```bash -cargo auto build -cargo auto release -cargo auto doc -cargo auto test -cargo auto commit_and push -cargo auto publish_to_crates_io -cargo auto github_new_release -``` - -Every task finishes with instructions how to proceed. -The [cargo-auto](https://github.com/automation-tasks-rs/cargo-auto) and [dev_bestia_cargo_completion](https://github.com/automation-tasks-rs/dev_bestia_cargo_completion) are already installed inside the CRUSTDE container. - -You can open the automation sub-project in VSCode and then code your own tasks in Rust. - -```bash -code automation_tasks_rs -``` - -## Separate main.rs and lib.rs - -It is always good to split the project between a `main.rs` (executable) and a `lib.rs` (library crate). - -Even for the smallest project. Maybe some other program will use the library eventually. - -All the input/output is coded in the `main.rs`: keyboard and monitor (stdin and stdout), access to files, and some access to the network. -The library must not operate directly with the stdin/stdout, because some other caller of the library can have other ideas around input-output options. Maybe it is a Graphical user interface that does things completely different than CLI applications. - -A separate `lib.rs` enables one to make good tests and examples without worrying about input-output. - -## super simple argument parsing - -I use a super simple code to parse CLI arguments inside the `src/bin/cargo_auto_template_new_cli/main.rs`. There are crate libraries that enable very complex argument parsing if needed. - -## Modules - -I added one module `hello_mod.rs` just to showcase how modules are used in separate files. - -## Markdown - -README.md and all the doc-comments are in markdown. To separate paragraphs in markdown use an empty line between them. -I tried other variants like double-space or backslash, but an empty line is the most used in the wild. - -## tests - -I added a unit-test, just to show how it looks. And an integration-test. So it is "ready-to-go". -Run them with `cargo test`. - -## examples - -In the directory `examples` every rs file is a bin-executable. -Run it with: - -```bash -cargo run --example example_1 -``` - -## Error handling thiserror and anyhow - -Rule number one is never to use `.unwrap()` in your real Rust code. It is a sign, you are not Error handling properly. -Maybe `unwrap()` can be fine for some fast learning examples, but for any real-life Rust code, you must use some `Error handling`. There are many different ways to do that in Rust. I choose the pair of libraries `thiserror` and `anyhow`. The first is made for libraries, the second is made for bin-executables. -The library needs an Enum with all the possible errors that this library can return. With `#[derive(Error)]` this enum gets everything needed to be a true Rust error struct. Every error can have a formatting string and a struct of data. -The bin-executable does not want to be involved in every possible error separately. It needs an umbrella for all possible errors with `anyhow::Result`. -Inside the code, mostly propagate the errors with the `?` Operator after the `Result` value instead of unwrap() or the match expression. -In the tests we don't want to work with Error handling. There, instead of `.unwrap()`, use the similar function `.expect(&str)` that has an additional description string. I use expect() when I am 100% sure the panic cannot happen because I checked some conditions before it. -"###, -}); - vec_file.push(crate::FileItem { - file_name: "examples/example_1.rs", - file_content: r###"// examples/example_1.rs - -//! A simple example how to use the `lib.rs` -//! You can run it with `cargo run --example example_1` - -use cargo_auto_template_new_cli_lib::*; - -/// example how to use format_hello_phrase() and format_upper_hello_phrase() -fn main() { - let greet_name = "world"; - let phrase = format_hello_phrase(greet_name); - println!("{}", phrase); - - // possible error must be processed - match format_upper_hello_phrase(greet_name) { - Ok(phrase) => println!("{}", phrase), - Err(err) => log::error!("Error: {}", err), - } -} -"###, - }); - vec_file.push(crate::FileItem { - file_name: "LICENSE", - file_content: r###"MIT License - -Copyright (c) 2024 bestia.dev - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -"###, - }); - vec_file.push(crate::FileItem{ - file_name :"README.md", - file_content : r###"[//]: # (auto_md_to_doc_comments segment start A) - -# cargo_auto_template_new_cli - -[//]: # (auto_cargo_toml_to_md start) - -**Basic Rust project template for CLI and library, more than just `cargo new hello`** -***version: 1.0.4 date: 2024-04-21 author: [bestia.dev](https://bestia.dev) repository: [GitHub](https://github.com/github_owner/cargo_auto_template_new_cli)*** - -[//]: # (auto_cargo_toml_to_md end) - - [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/github_owner/cargo_auto_template_new_cli/blob/main/LICENSE) - [![Rust](https://github.com/github_owner/cargo_auto_template_new_cli/workflows/rust_fmt_auto_build_test/badge.svg)](https://github.com/github_owner/cargo_auto_template_new_cli/) - -[//]: # (auto_lines_of_code start) - -[![Lines in Rust code](https://img.shields.io/badge/Lines_in_Rust-89-green.svg)](https://github.com/github_owner/cargo_auto_template_new_cli/) -[![Lines in Doc comments](https://img.shields.io/badge/Lines_in_Doc_comments-13-blue.svg)](https://github.com/github_owner/cargo_auto_template_new_cli/) -[![Lines in Comments](https://img.shields.io/badge/Lines_in_comments-36-purple.svg)](https://github.com/github_owner/cargo_auto_template_new_cli/) -[![Lines in examples](https://img.shields.io/badge/Lines_in_examples-19-yellow.svg)](https://github.com/github_owner/cargo_auto_template_new_cli/) -[![Lines in tests](https://img.shields.io/badge/Lines_in_tests-30-orange.svg)](https://github.com/github_owner/cargo_auto_template_new_cli/) - -[//]: # (auto_lines_of_code end) - -Hashtags: #maintained #ready-for-use #rustlang #automation #workflow -My projects on GitHub are more like a tutorial than a finished product: [bestia-dev tutorials](https://github.com/bestia-dev/tutorials_rust_wasm). -I recommend using the [CRUSTDE - Containerized Rust Development Environment](https://github.com/CRUSTDE-ContainerizedRustDevEnv/crustde_cnt_img_pod) to write Rust projects on Linux, isolated from your system. - -## This template - -Just like `cargo new` makes a soft and gentle introduction to Rust projects and development, I want to make a similar command that creates a real-life Rust project. - -```bash -cargo auto new_cli project_name -``` - -Extremely simple, just the basic moving parts and use cases. - -## Development details - -Read the development details in a separate md file: -[DEVELOPMENT.md](DEVELOPMENT.md) - -## Releases changelog - -Read the releases changelog in a separate md file: -[RELEASES.md](RELEASES.md) - -## TODO - -And code happily ever after... - -## Open-source and free as a beer - -My open-source projects are free as a beer (MIT license). -I just love programming. -But I need also to drink. If you find my projects and tutorials helpful, please buy me a beer by donating to my [PayPal](https://paypal.me/LucianoBestia). -You know the price of a beer in your local bar ;-) -So I can drink a free beer for your health :-) -[Na zdravje!](https://translate.google.com/?hl=en&sl=sl&tl=en&text=Na%20zdravje&op=translate) [Alla salute!](https://dictionary.cambridge.org/dictionary/italian-english/alla-salute) [Prost!](https://dictionary.cambridge.org/dictionary/german-english/prost) [Nazdravlje!](https://matadornetwork.com/nights/how-to-say-cheers-in-50-languages/) 🍻 - -[//bestia.dev](https://bestia.dev) -[//github.com/bestia-dev](https://github.com/bestia-dev) -[//bestiadev.substack.com](https://bestiadev.substack.com) -[//youtube.com/@bestia-dev-tutorials](https://youtube.com/@bestia-dev-tutorials) - -[//]: # (auto_md_to_doc_comments segment end A) -"###, -}); - vec_file.push(crate::FileItem { - file_name: "tests/integration_test.rs", - file_content: r###"// tests/integration_test.rs - -use cargo_auto_template_new_cli_lib::*; - -#[test] -fn integration_test_01() { - assert_eq!(format_hello_phrase("abcd"), "Hello abcd!"); - assert_eq!(format_upper_hello_phrase("abcd").expect("error"), "Hello ABCD!"); -} - -#[test] -fn integration_test_02_error_check() { - assert!(format_upper_hello_phrase("ABCD").is_err()); -} -"###, - }); - vec_file.push(crate::FileItem { - file_name: ".vscode/settings.json", - file_content: r###"{ - "workbench.colorCustomizations": { - "titleBar.activeForeground": "#fff", - "titleBar.inactiveForeground": "#ffffffcc", - "titleBar.activeBackground": "#404040", - "titleBar.inactiveBackground": "#2d2d2dcc" - }, - "spellright.language": [ - "en" - ], - "spellright.documentTypes": [ - "markdown", - "latex", - "plaintext" - ], - "files.associations": { - "LICENSE": "plain text" - }, - "rust-analyzer.showUnlinkedFileNotification": false, - "cSpell.words": [ - "Alla", - "alloc", - "appender", - "bestia", - "bestiadev", - "camino", - "CRUSTDE", - "Decryptor", - "Encryptor", - "endregion", - "keygen", - "Nazdravlje", - "passcode", - "plantuml", - "Prost", - "reqwest", - "rustdevuser", - "rustlang", - "rustprojects", - "serde", - "struct", - "subsecond", - "substack", - "thiserror", - "zcvf", - "zdravje" - ] -}"###, - }); - // endregion: files copied into strings by automation tasks - - // return - vec_file -} diff --git a/src/template_new_pwa_wasm_mod.rs b/src/template_new_pwa_wasm_mod.rs index 58a46991..7a103419 100644 --- a/src/template_new_pwa_wasm_mod.rs +++ b/src/template_new_pwa_wasm_mod.rs @@ -770,7 +770,7 @@ fn task_github_new_release() { file_name: "automation_tasks_rs/src/secrets_always_local_mod.rs", file_content: r###"// secrets_always_local_mod.rs -/// Secrets like GitHub API secret_token, crates.io secret_token, SSH private key passphrase and similar +/// Secrets like GitHub API secret_token, crates.io secret token, docker hub secret_token, SSH private key passphrase and similar /// must never go out of this crate. Never pass any secret to an external crate library as much as possible. /// The user has the source code under his fingers in this crate. So he knows nobody will mess with this code /// once he inspected and reviewed it. @@ -778,11 +778,27 @@ fn task_github_new_release() { /// The simple program flow of functions that need secrets is butchered to avoid secrets leaving this crate. /// Now it looks like a mess, but the goal is achieved. The secrets never leave this crate. +// region: Public API constants +// ANSI colors for Linux terminal +// https://github.com/shiena/ansicolor/blob/master/README.md +/// ANSI color +pub const RED: &str = "\x1b[31m"; +/// ANSI color +pub const GREEN: &str = "\x1b[32m"; +/// ANSI color +pub const YELLOW: &str = "\x1b[33m"; +/// ANSI color +pub const BLUE: &str = "\x1b[34m"; +/// ANSI color +pub const RESET: &str = "\x1b[0m"; +// endregion: Public API constants + +pub use cargo_auto_encrypt_secret_lib::EncryptedString; +pub use secrecy::ExposeSecret; + pub(crate) mod decrypt_mod { - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - use secrecy::ExposeSecret; + use crate::secrets_always_local_mod::*; /// The secrets must not leave this crate. /// They are never going into an external library crate. @@ -828,12 +844,7 @@ pub(crate) mod decrypt_mod { } pub(crate) mod encrypt_mod { - - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - - // bring trait to scope - use secrecy::ExposeSecret; + use crate::secrets_always_local_mod::*; /// The secrets must not leave this crate. /// They are never going into an external library crate. @@ -879,7 +890,7 @@ pub(crate) mod secrecy_mod { //! But I want to encrypt the content, so I will make a wrapper. //! The secrets must always be moved to secrecy types as soon as possible. - use cargo_auto_encrypt_secret_lib::EncryptedString; + use crate::secrets_always_local_mod::*; pub struct SecretEncryptedString { encrypted_string: EncryptedString, @@ -908,18 +919,8 @@ pub(crate) mod secrecy_mod { pub(crate) mod ssh_mod { - #[allow(unused_imports)] - use cargo_auto_lib::BLUE; - use cargo_auto_lib::GREEN; - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - use cargo_auto_lib::YELLOW; - use crate::secrets_always_local_mod::*; - // bring trait into scope - use secrecy::ExposeSecret; - pub struct SshContext { signed_passcode_is_a_secret: secrecy::SecretVec, decrypted_string: secrecy::SecretString, @@ -945,7 +946,7 @@ pub(crate) mod ssh_mod { self.decrypted_string = decryptor.return_secret_string().clone(); } - /// get token and encrypt + /// get secret_token and encrypt fn get_secret_token_and_encrypt(&self) -> cargo_auto_encrypt_secret_lib::EncryptedString { /// Internal function used only for test configuration /// @@ -960,7 +961,7 @@ pub(crate) mod ssh_mod { #[cfg(not(test))] fn get_secret_token() -> secrecy::SecretString { eprintln!(" "); - eprintln!(" {BLUE}Enter the API secret_token to encrypt:{RESET}"); + eprintln!(" {BLUE}Enter the secret_token to encrypt:{RESET}"); secrecy::SecretString::new( inquire::Password::new("") .without_confirmation() @@ -969,9 +970,9 @@ pub(crate) mod ssh_mod { .unwrap(), ) } - let token_is_a_secret = get_secret_token(); + let secret_token = get_secret_token(); // use this signed as password for symmetric encryption - let encryptor = encrypt_mod::Encryptor::new_for_encrypt(token_is_a_secret, &self.signed_passcode_is_a_secret); + let encryptor = encrypt_mod::Encryptor::new_for_encrypt(secret_token, &self.signed_passcode_is_a_secret); let encrypted_token = encryptor.encrypt_symmetric().unwrap(); // return @@ -1020,7 +1021,7 @@ pub(crate) mod ssh_mod { } None => { // ask user to think about adding with ssh-add - eprintln!(" {YELLOW}SSH key for encrypted token is not found in the ssh-agent.{RESET}"); + eprintln!(" {YELLOW}SSH key for encrypted secret_token is not found in the ssh-agent.{RESET}"); eprintln!(" {YELLOW}Without ssh-agent, you will have to type the private key passphrase every time. This is more secure, but inconvenient.{RESET}"); eprintln!(" {YELLOW}You can manually add the SSH identity to ssh-agent for 1 hour:{RESET}"); eprintln!(" {YELLOW}WARNING: using ssh-agent is less secure, because there is no need for user interaction.{RESET}"); @@ -1041,6 +1042,7 @@ pub(crate) mod ssh_mod { } } } + /// Expand path and check if identity file exists /// /// Inform the user how to generate identity file. @@ -1050,9 +1052,11 @@ pub(crate) mod ssh_mod { eprintln!("{RED}Identity file {identity_private_file_path_expanded} that contains the SSH private key does not exist! {RESET}"); eprintln!(" {YELLOW}Create the SSH key manually in bash with this command:{RESET}"); if identity_private_file_path_expanded.as_str().contains("github_api") { - eprintln!(r#"{GREEN}ssh-keygen -t ed25519 -f "{identity_private_file_path_expanded}" -C "github API secret_token"{RESET}"#); + eprintln!(r#"{GREEN}ssh-keygen -t ed25519 -f "{identity_private_file_path_expanded}" -C "github api secret_token"{RESET}"#); } else if identity_private_file_path_expanded.as_str().contains("crates_io") { eprintln!(r#"{GREEN}ssh-keygen -t ed25519 -f "{identity_private_file_path_expanded}" -C "crates io secret_token"{RESET}"#); + } else if identity_private_file_path_expanded.as_str().contains("docker_hub") { + eprintln!(r#"{GREEN}ssh-keygen -t ed25519 -f "{identity_private_file_path_expanded}" -C "docker hub secret_token"{RESET}"#); } eprintln!(" "); panic!("{RED}Error: File {identity_private_file_path_expanded} does not exist! {RESET}"); @@ -1066,23 +1070,17 @@ pub(crate) mod github_mod { //! Every API call needs the GitHub API secret_token. This is a secret important just like a password. //! I don't want to pass this secret to an "obscure" library crate that is difficult to review. //! This secret will stay here in this codebase that every developer can easily inspect. - //! Instead of the token, I will pass the struct GitHubClient with the trait SendToGitHubApi. - //! This way, the secret token will be encapsulated. + //! Instead of the secret_token, I will pass the struct GitHubClient with the trait SendToGitHubApi. + //! This way, the secret_token will be encapsulated. + use crate::secrets_always_local_mod::*; use cargo_auto_github_lib as cgl; - - use cargo_auto_lib::BLUE; - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - use reqwest::Client; - // bring trait into scope - use secrecy::ExposeSecret; /// Struct GitHubClient contains only private fields /// This fields are accessible only to methods in implementation of traits. pub struct GitHubClient { - /// Passcode for encrypt the token_is_a_secret to encrypted_token in memory. + /// Passcode for encrypt the secret_token to encrypted_token in memory. /// So that the secret is in memory as little as possible as plain text. /// For every session (program start) a new random passcode is created. session_passcode: secrecy::SecretVec, @@ -1094,9 +1092,9 @@ pub(crate) mod github_mod { impl GitHubClient { /// Create new GitHub client /// - /// Interactively ask the user to input the GitHub token. - pub fn new_interactive_input_token() -> Self { - let mut github_client = Self::new_wo_token(); + /// Interactively ask the user to input the GitHub secret_token. + pub fn new_interactive_input_secret_token() -> Self { + let mut github_client = Self::new_wo_secret_token(); println!("{BLUE}Enter the GitHub API secret_token:{RESET}"); github_client.encrypted_token = @@ -1106,8 +1104,8 @@ pub(crate) mod github_mod { github_client } - /// Create new GitHub client without token - fn new_wo_token() -> Self { + /// Create new GitHub client without secret_token + fn new_wo_secret_token() -> Self { /// Internal function Generate a random password fn random_byte_passcode() -> [u8; 32] { let mut password = [0_u8; 32]; @@ -1124,25 +1122,25 @@ pub(crate) mod github_mod { /// Use the stored API secret_token /// - /// If the token not exists ask user to interactively input the token. - /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the token. + /// If the secret_token not exists ask user to interactively input the secret_token. + /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the secret_token. /// it is then possible also to have the ssh key in ssh-agent and write the passphrase only once. - /// But this great user experience comes with security concerns. The token is accessible if the attacker is very dedicated. - pub fn new_with_stored_token() -> Self { + /// But this great user experience comes with security concerns. The secret_token is accessible if the attacker is very dedicated. + pub fn new_with_stored_secret_token() -> Self { /// Internal function for DRY Don't Repeat Yourself - fn read_token_and_decrypt_return_github_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> GitHubClient { - // read the token and decrypt + fn read_secret_token_and_decrypt_return_github_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> GitHubClient { + // read the secret_token and decrypt cargo_auto_encrypt_secret_lib::decrypt_with_ssh_interactive_from_file(&mut ssh_context, encrypted_string_file_path); - let token_is_a_secret = ssh_context.get_decrypted_string(); - let mut github_client = GitHubClient::new_wo_token(); - github_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(token_is_a_secret, &github_client.session_passcode); + let secret_token = ssh_context.get_decrypted_string(); + let mut github_client = GitHubClient::new_wo_secret_token(); + github_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(secret_token, &github_client.session_passcode); github_client } - let encrypted_string_file_path = camino::Utf8Path::new("~/.ssh/github_api_token_encrypted.txt"); + let encrypted_string_file_path = camino::Utf8Path::new("~/.ssh/github_api_secret_token_encrypted.txt"); let encrypted_string_file_path_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(encrypted_string_file_path); - let identity_private_file_path = camino::Utf8Path::new("~/.ssh/github_api_token_ssh_1"); + let identity_private_file_path = camino::Utf8Path::new("~/.ssh/github_api_secret_token_ssh_1"); let _identity_private_file_path_expanded = crate::secrets_always_local_mod::ssh_mod::expand_path_check_private_key_exists(identity_private_file_path); if !encrypted_string_file_path_expanded.exists() { @@ -1150,27 +1148,27 @@ pub(crate) mod github_mod { println!(" {BLUE}Do you want to store the GitHub API secret_token encrypted with an SSH key? (y/n){RESET}"); let answer = inquire::Text::new("").prompt().unwrap(); if answer.to_lowercase() != "y" { - // enter the token manually, not storing - return Self::new_interactive_input_token(); + // enter the secret_token manually, not storing + return Self::new_interactive_input_secret_token(); } else { - // get the passphrase and token interactively + // get the passphrase and secret_token interactively let mut ssh_context = super::ssh_mod::SshContext::new(); - // encrypt and save the encrypted token + // encrypt and save the encrypted secret_token cargo_auto_encrypt_secret_lib::encrypt_with_ssh_interactive_save_file(&mut ssh_context, identity_private_file_path, encrypted_string_file_path); - // read the token and decrypt, return GitHubClient - read_token_and_decrypt_return_github_client(ssh_context, encrypted_string_file_path) + // read the secret_token and decrypt, return GitHubClient + read_secret_token_and_decrypt_return_github_client(ssh_context, encrypted_string_file_path) } } else { // file exists let ssh_context = super::ssh_mod::SshContext::new(); - // read the token and decrypt, return GitHubClient - read_token_and_decrypt_return_github_client(ssh_context, encrypted_string_file_path) + // read the secret_token and decrypt, return GitHubClient + read_secret_token_and_decrypt_return_github_client(ssh_context, encrypted_string_file_path) } } - /// decrypts the secret token in memory + /// decrypts the secret_token in memory #[allow(dead_code)] - pub fn decrypt_token_in_memory(&self) -> secrecy::SecretString { + pub fn decrypt_secret_token_in_memory(&self) -> secrecy::SecretString { self.encrypted_token.expose_decrypted_secret(&self.session_passcode) } } @@ -1181,10 +1179,10 @@ pub(crate) mod github_mod { /// /// This function encapsulates the secret API secret_token. /// The RequestBuilder is created somewhere in the library crate. - /// The client can be passed to the library. It will not reveal the secret token. + /// The client can be passed to the library. It will not reveal the secret_token. fn send_to_github_api(&self, req: reqwest::blocking::RequestBuilder) -> serde_json::Value { // I must build the request to be able then to inspect it. - let req = req.bearer_auth(self.decrypt_token_in_memory().expose_secret()).build().unwrap(); + let req = req.bearer_auth(self.decrypt_secret_token_in_memory().expose_secret()).build().unwrap(); // region: Assert the correct url and https // It is important that the request coming from a external crate/library @@ -1217,11 +1215,11 @@ pub(crate) mod github_mod { /// /// This function encapsulates the secret API secret_token. /// The RequestBuilder is created somewhere in the library crate. - /// The client can be passed to the library. It will not reveal the secret token. + /// The client can be passed to the library. It will not reveal the secret_token. /// This is basically an async fn, but use of `async fn` in public traits is discouraged... async fn upload_to_github(&self, req: reqwest::RequestBuilder) -> serde_json::Value { // I must build the request to be able then to inspect it. - let req = req.bearer_auth(self.decrypt_token_in_memory().expose_secret()).build().unwrap(); + let req = req.bearer_auth(self.decrypt_secret_token_in_memory().expose_secret()).build().unwrap(); // region: Assert the correct url and https // It is important that the request coming from a external crate/library @@ -1257,21 +1255,17 @@ pub(crate) mod crates_io_mod { //! Publish to crates.io needs the crates.io secret_token. This is a secret important just like a password. //! I don't want to pass this secret to an "obscure" library crate that is difficult to review. //! This secret will stay here in this codebase that every developer can easily inspect. - //! Instead of the token, I will pass the struct CratesIoClient with the trait SendToCratesIo. - //! This way, the secret token will be encapsulated. - - use cargo_auto_lib::BLUE; - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - use cargo_auto_lib::YELLOW; + //! Instead of the secret_token, I will pass the struct CratesIoClient with the trait SendToCratesIo. + //! This way, the secret_token will be encapsulated. - // bring trait into scope - use secrecy::ExposeSecret; + use crate::secrets_always_local_mod::*; + use cargo_auto_lib::ShellCommandLimitedDoubleQuotesSanitizer; + use cargo_auto_lib::ShellCommandLimitedDoubleQuotesSanitizerTrait; /// Struct CratesIoClient contains only private fields /// This fields are accessible only to methods in implementation of traits. pub struct CratesIoClient { - /// Passcode for encrypt the token_is_a_secret to encrypted_token in memory. + /// Passcode for encrypt the secret_token to encrypted_token in memory. /// So that the secret is in memory as little as possible as plain text. /// For every session (program start) a new random passcode is created. session_passcode: secrecy::SecretVec, @@ -1285,8 +1279,8 @@ pub(crate) mod crates_io_mod { /// /// Interactively ask the user to input the crates.io secret_token. #[allow(dead_code)] - pub fn new_interactive_input_token() -> Self { - let mut crates_io_client = Self::new_wo_token(); + pub fn new_interactive_input_secret_token() -> Self { + let mut crates_io_client = Self::new_wo_secret_token(); println!("{BLUE}Enter the crates.io secret_token:{RESET}"); crates_io_client.encrypted_token = @@ -1296,9 +1290,9 @@ pub(crate) mod crates_io_mod { crates_io_client } - /// Create new CratesIo client without token + /// Create new CratesIo client without secret_token #[allow(dead_code)] - fn new_wo_token() -> Self { + fn new_wo_secret_token() -> Self { /// Internal function Generate a random password fn random_byte_passcode() -> [u8; 32] { let mut password = [0_u8; 32]; @@ -1315,26 +1309,35 @@ pub(crate) mod crates_io_mod { /// Use the stored crates.io secret_token /// - /// If the token not exists ask user to interactively input the token. - /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the token. + /// If the secret_token not exists ask user to interactively input the secret_token. + /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the secret_token. /// It is then possible also to have the ssh key in ssh-agent and write the passphrase only once. - /// But this great user experience comes with security concerns. The token is accessible if the attacker is very dedicated. + /// But this great user experience comes with security concerns. The secret_token is accessible if the attacker is very dedicated. #[allow(dead_code)] - pub fn new_with_stored_token() -> Self { + pub fn new_with_stored_secret_token() -> Self { /// Internal function for DRY Don't Repeat Yourself - fn read_token_and_decrypt_return_crates_io_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> CratesIoClient { + fn read_secret_token_and_decrypt_return_crates_io_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> CratesIoClient { cargo_auto_encrypt_secret_lib::decrypt_with_ssh_interactive_from_file(&mut ssh_context, encrypted_string_file_path); - let token_is_a_secret = ssh_context.get_decrypted_string(); - let mut crates_io_client = CratesIoClient::new_wo_token(); - crates_io_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(token_is_a_secret, &crates_io_client.session_passcode); + let secret_token = ssh_context.get_decrypted_string(); + let mut crates_io_client = CratesIoClient::new_wo_secret_token(); + crates_io_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(secret_token, &crates_io_client.session_passcode); crates_io_client } - let encrypted_string_file_path = camino::Utf8Path::new("~/.ssh/crates_io_token_encrypted.txt"); + // check if the plain-text file from `podman login` exists and warn the user because it is a security vulnerability. + let file_auth = "~/.cargo/credentials.toml"; + let file_auth = camino::Utf8Path::new(file_auth); + // TODO: check for env variable also? + let file_auth_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(file_auth); + let file_auth_expanded = camino::Utf8Path::new(&file_auth_expanded); + if file_auth_expanded.exists() { + eprintln!("{RED}Security vulnerability: Found the cargo file with plain-text secret_token: {file_auth_expanded}. It would be better to inspect and remove it. {RESET}") + } + + let encrypted_string_file_path = camino::Utf8Path::new("~/.ssh/crates_io_secret_token_encrypted.txt"); let encrypted_string_file_path_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(encrypted_string_file_path); - let identity_private_file_path = camino::Utf8Path::new("~/.ssh/crates_io_token_ssh_1"); - + let identity_private_file_path = camino::Utf8Path::new("~/.ssh/crates_io_secret_token_ssh_1"); let _identity_private_file_path_expanded = crate::secrets_always_local_mod::ssh_mod::expand_path_check_private_key_exists(identity_private_file_path); if !encrypted_string_file_path_expanded.exists() { @@ -1342,45 +1345,190 @@ pub(crate) mod crates_io_mod { println!(" {BLUE}Do you want to store the crates.io secret_token encrypted with an SSH key? (y/n){RESET}"); let answer = inquire::Text::new("").prompt().unwrap(); if answer.to_lowercase() != "y" { - // enter the token manually, not storing - return Self::new_interactive_input_token(); + // enter the secret_token manually, not storing + return Self::new_interactive_input_secret_token(); } else { - // get the passphrase and token interactively + // get the passphrase and secret_token interactively let mut ssh_context = super::ssh_mod::SshContext::new(); - // encrypt and save the encrypted token + // encrypt and save the encrypted secret_token cargo_auto_encrypt_secret_lib::encrypt_with_ssh_interactive_save_file(&mut ssh_context, identity_private_file_path, encrypted_string_file_path); - // read the token and decrypt, return CratesIoClient - read_token_and_decrypt_return_crates_io_client(ssh_context, encrypted_string_file_path) + // read the secret_token and decrypt, return CratesIoClient + read_secret_token_and_decrypt_return_crates_io_client(ssh_context, encrypted_string_file_path) } } else { // file exists let ssh_context = super::ssh_mod::SshContext::new(); - // read the token and decrypt, return CratesIoClient - read_token_and_decrypt_return_crates_io_client(ssh_context, encrypted_string_file_path) + // read the secret_token and decrypt, return CratesIoClient + read_secret_token_and_decrypt_return_crates_io_client(ssh_context, encrypted_string_file_path) } } - /// decrypts the secret token in memory + /// decrypts the secret_token in memory #[allow(dead_code)] - pub fn decrypt_token_in_memory(&self) -> secrecy::SecretString { + pub fn decrypt_secret_token_in_memory(&self) -> secrecy::SecretString { self.encrypted_token.expose_decrypted_secret(&self.session_passcode) } /// Publish to crates.io /// /// This function encapsulates the secret crates.io secret_token. - /// The client can be passed to the library. It will not reveal the secret token. + /// The client can be passed to the library. It will not reveal the secret_token. #[allow(dead_code)] pub fn publish_to_crates_io(&self) { - // print command without the token - println!("{YELLOW}cargo publish --token [REDACTED]{RESET}"); - let shell_command = format!("cargo publish --token {}", self.decrypt_token_in_memory().expose_secret()); - let status = std::process::Command::new("sh").arg("-c").arg(shell_command).spawn().unwrap().wait().unwrap(); - let exit_code = status.code().expect(&format!("{RED}Error: publish to crates.io error. {RESET}")); - if exit_code != 0 { - panic!("{RED}Error: publish to crates.io error {exit_code}. {RESET}"); + // the secret_token is redacted when print on screen + ShellCommandLimitedDoubleQuotesSanitizer::new(r#"cargo publish --token "{secret_token}" "#) + .unwrap_or_else(|e| panic!("{e}")) + .arg_secret("{secret_token}", &self.decrypt_secret_token_in_memory()) + .unwrap_or_else(|e| panic!("{e}")) + .run() + .unwrap_or_else(|e| panic!("{e}")); + } + } +} + +pub(crate) mod docker_hub_mod { + + //! Push to docker-hub needs the docker hub secret_token. This is a secret important just like a password. + //! I don't want to pass this secret to an "obscure" library crate that is difficult to review. + //! This secret will stay here in this codebase that every developer can easily inspect. + //! Instead of the secret_token, I will pass the struct DockerHubClient with the trait SendToDockerHub. + //! This way, the secret_token will be encapsulated. + + use crate::secrets_always_local_mod::*; + use cargo_auto_lib::ShellCommandLimitedDoubleQuotesSanitizer; + use cargo_auto_lib::ShellCommandLimitedDoubleQuotesSanitizerTrait; + + /// Struct DockerHubClient contains only private fields + /// This fields are accessible only to methods in implementation of traits. + pub struct DockerHubClient { + /// Passcode for encrypt the secret_token to encrypted_token in memory. + /// So that the secret is in memory as little as possible as plain text. + /// For every session (program start) a new random passcode is created. + session_passcode: secrecy::SecretVec, + + /// private field is set only once in the new() constructor + encrypted_token: super::secrecy_mod::SecretEncryptedString, + } + + impl DockerHubClient { + /// Create new DockerHub client + /// + /// Interactively ask the user to input the docker hub secret_token. + #[allow(dead_code)] + pub fn new_interactive_input_secret_token() -> Self { + let mut docker_hub_client = Self::new_wo_secret_token(); + + println!("{BLUE}Enter the docker hub secret_token:{RESET}"); + docker_hub_client.encrypted_token = + super::secrecy_mod::SecretEncryptedString::new_with_string(inquire::Password::new("").without_confirmation().prompt().unwrap(), &docker_hub_client.session_passcode); + + // return + docker_hub_client + } + + /// Create new DockerHub client without secret_token + #[allow(dead_code)] + fn new_wo_secret_token() -> Self { + /// Internal function Generate a random password + fn random_byte_passcode() -> [u8; 32] { + let mut password = [0_u8; 32]; + use aes_gcm::aead::rand_core::RngCore; + aes_gcm::aead::OsRng.fill_bytes(&mut password); + password + } + + let session_passcode = secrecy::SecretVec::new(random_byte_passcode().to_vec()); + let encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_string("".to_string(), &session_passcode); + + DockerHubClient { session_passcode, encrypted_token } + } + + /// Use the stored docker hub secret_token + /// + /// If the secret_token not exists ask user to interactively input the secret_token. + /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the secret_token. + /// It is then possible also to have the ssh key in ssh-agent and write the passphrase only once. + /// But this great user experience comes with security concerns. The secret_token is accessible if the attacker is very dedicated. + #[allow(dead_code)] + pub fn new_with_stored_secret_token(user_name: &str, registry: &str) -> Self { + /// Internal function for DRY Don't Repeat Yourself + fn read_secret_token_and_decrypt_return_docker_hub_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> DockerHubClient { + cargo_auto_encrypt_secret_lib::decrypt_with_ssh_interactive_from_file(&mut ssh_context, encrypted_string_file_path); + let secret_token = ssh_context.get_decrypted_string(); + let mut docker_hub_client = DockerHubClient::new_wo_secret_token(); + docker_hub_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(secret_token, &docker_hub_client.session_passcode); + docker_hub_client + } + + // check if the plain-text file from `podman login` exists and warn the user because it is a security vulnerability. + let file_auth = "${XDG_RUNTIME_DIR}/containers/auth.json"; + // TODO: check for env variable also? + if let Some(xdg_runtime_dir) = std::env::var_os("XDG_RUNTIME_DIR"){ + let xdg_runtime_dir=xdg_runtime_dir.to_string_lossy().to_string(); + let file_auth_expanded = file_auth.replace("${XDG_RUNTIME_DIR}", &xdg_runtime_dir); + let file_auth_expanded = camino::Utf8Path::new(&file_auth_expanded); + if file_auth_expanded.exists() { + eprintln!("{RED}Security vulnerability: Found the docker hub file with plain-text secret_token: {file_auth_expanded}. It would be better to inspect and remove it. {RESET}") + } + } + + // registry: docker.io -> replace dot into "--"" + // username: bestiadev + let registry_escaped = registry.replace(".", "--"); + let encrypted_string_file_path = format!("~/.ssh/docker_hub_{registry_escaped}_{user_name}.txt"); + let encrypted_string_file_path = camino::Utf8Path::new(&encrypted_string_file_path); + let encrypted_string_file_path_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(encrypted_string_file_path); + + let identity_private_file_path = camino::Utf8Path::new("~/.ssh/docker_hub_secret_token_ssh_1"); + let _identity_private_file_path_expanded = crate::secrets_always_local_mod::ssh_mod::expand_path_check_private_key_exists(identity_private_file_path); + + if !encrypted_string_file_path_expanded.exists() { + // ask interactive + println!(" {BLUE}Do you want to store the docker hub secret_token encrypted with an SSH key? (y/n){RESET}"); + let answer = inquire::Text::new("").prompt().unwrap(); + if answer.to_lowercase() != "y" { + // enter the secret_token manually, not storing + return Self::new_interactive_input_secret_token(); + } else { + // get the passphrase and secret_token interactively + let mut ssh_context = super::ssh_mod::SshContext::new(); + // encrypt and save the encrypted secret_token + cargo_auto_encrypt_secret_lib::encrypt_with_ssh_interactive_save_file(&mut ssh_context, identity_private_file_path, encrypted_string_file_path); + // read the secret_token and decrypt, return DockerHubClient + read_secret_token_and_decrypt_return_docker_hub_client(ssh_context, encrypted_string_file_path) + } + } else { + // file exists + let ssh_context = super::ssh_mod::SshContext::new(); + // read the secret_token and decrypt, return DockerHubClient + read_secret_token_and_decrypt_return_docker_hub_client(ssh_context, encrypted_string_file_path) } } + + /// decrypts the secret_token in memory + #[allow(dead_code)] + pub fn decrypt_secret_token_in_memory(&self) -> secrecy::SecretString { + self.encrypted_token.expose_decrypted_secret(&self.session_passcode) + } + + /// Push to docker hub + /// + /// This function encapsulates the secret docker hub secret_token. + /// The client can be passed to the library. It will not reveal the secret_token. + #[allow(dead_code)] + pub fn push_to_docker_hub(&self, image_url: &str, user_name: &str) { + // the secret_token can be used in place of the password in --cred + ShellCommandLimitedDoubleQuotesSanitizer::new(r#"podman push --creds "{user_name}:{secret_token}" "{image_url}" "#) + .unwrap_or_else(|e| panic!("{e}")) + .arg("{user_name}", user_name) + .unwrap_or_else(|e| panic!("{e}")) + .arg_secret("{secret_token}", &self.decrypt_secret_token_in_memory()) + .unwrap_or_else(|e| panic!("{e}")) + .arg("{image_url}", image_url) + .unwrap_or_else(|e| panic!("{e}")) + .run() + .unwrap_or_else(|e| panic!("{e}")); + } } } "###, @@ -2020,7 +2168,7 @@ pub const RESET: &str = "\x1b[0m"; //! # cargo-auto //! //! **Automation tasks coded in Rust language for the workflow of Rust projects** -//! ***version: 2024.422.214 date: 2024-04-22 author: [bestia.dev](https://bestia.dev) repository: [GitHub](https://github.com/automation-tasks-rs/cargo-auto)*** +//! ***version: 2024.501.55 date: 2024-05-01 author: [bestia.dev](https://bestia.dev) repository: [GitHub](https://github.com/automation-tasks-rs/cargo-auto)*** //! //! ![maintained](https://img.shields.io/badge/maintained-green) //! ![ready-for-use](https://img.shields.io/badge/ready_for_use-green) @@ -2040,11 +2188,11 @@ pub const RESET: &str = "\x1b[0m"; //! [![Newest docs](https://img.shields.io/badge/newest_docs-blue.svg)](https://automation-tasks-rs.github.io/cargo-auto/cargo_auto/index.html) //! ![cargo-auto](https://bestia.dev/webpage_hit_counter/get_svg_image/959103982.svg) //! -//! [![Lines in Rust code](https://img.shields.io/badge/Lines_in_Rust-3145-green.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -//! [![Lines in Doc comments](https://img.shields.io/badge/Lines_in_Doc_comments-1203-blue.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -//! [![Lines in Comments](https://img.shields.io/badge/Lines_in_comments-704-purple.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +//! [![Lines in Rust code](https://img.shields.io/badge/Lines_in_Rust-2565-green.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +//! [![Lines in Doc comments](https://img.shields.io/badge/Lines_in_Doc_comments-1159-blue.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +//! [![Lines in Comments](https://img.shields.io/badge/Lines_in_comments-615-purple.svg)](https://github.com/automation-tasks-rs/cargo-auto/) //! [![Lines in examples](https://img.shields.io/badge/Lines_in_examples-0-yellow.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -//! [![Lines in tests](https://img.shields.io/badge/Lines_in_tests-10949-orange.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +//! [![Lines in tests](https://img.shields.io/badge/Lines_in_tests-10243-orange.svg)](https://github.com/automation-tasks-rs/cargo-auto/) //! //! Hashtags: #maintained #ready-for-use #rustlang #automation #workflow //! My projects on GitHub are more like a tutorial than a finished product: [bestia-dev tutorials](https://github.com/bestia-dev/tutorials_rust_wasm). @@ -2457,7 +2605,7 @@ jobs: // but the new service worker will not be activated until all // tabs with this webapp are closed. -const CACHE_NAME = '2024.422.204'; +const CACHE_NAME = '2024.501.46'; self.addEventListener('install', event => { console.log('event install ', CACHE_NAME); diff --git a/src/template_new_wasm_mod.rs b/src/template_new_wasm_mod.rs index bcc9cbfa..823bfa08 100644 --- a/src/template_new_wasm_mod.rs +++ b/src/template_new_wasm_mod.rs @@ -636,7 +636,7 @@ fn task_github_new_release() { file_name: "automation_tasks_rs/src/secrets_always_local_mod.rs", file_content: r###"// secrets_always_local_mod.rs -/// Secrets like GitHub API secret_token, crates.io secret_token, SSH private key passphrase and similar +/// Secrets like GitHub API secret_token, crates.io secret token, docker hub secret_token, SSH private key passphrase and similar /// must never go out of this crate. Never pass any secret to an external crate library as much as possible. /// The user has the source code under his fingers in this crate. So he knows nobody will mess with this code /// once he inspected and reviewed it. @@ -644,11 +644,27 @@ fn task_github_new_release() { /// The simple program flow of functions that need secrets is butchered to avoid secrets leaving this crate. /// Now it looks like a mess, but the goal is achieved. The secrets never leave this crate. +// region: Public API constants +// ANSI colors for Linux terminal +// https://github.com/shiena/ansicolor/blob/master/README.md +/// ANSI color +pub const RED: &str = "\x1b[31m"; +/// ANSI color +pub const GREEN: &str = "\x1b[32m"; +/// ANSI color +pub const YELLOW: &str = "\x1b[33m"; +/// ANSI color +pub const BLUE: &str = "\x1b[34m"; +/// ANSI color +pub const RESET: &str = "\x1b[0m"; +// endregion: Public API constants + +pub use cargo_auto_encrypt_secret_lib::EncryptedString; +pub use secrecy::ExposeSecret; + pub(crate) mod decrypt_mod { - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - use secrecy::ExposeSecret; + use crate::secrets_always_local_mod::*; /// The secrets must not leave this crate. /// They are never going into an external library crate. @@ -694,12 +710,7 @@ pub(crate) mod decrypt_mod { } pub(crate) mod encrypt_mod { - - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - - // bring trait to scope - use secrecy::ExposeSecret; + use crate::secrets_always_local_mod::*; /// The secrets must not leave this crate. /// They are never going into an external library crate. @@ -745,7 +756,7 @@ pub(crate) mod secrecy_mod { //! But I want to encrypt the content, so I will make a wrapper. //! The secrets must always be moved to secrecy types as soon as possible. - use cargo_auto_encrypt_secret_lib::EncryptedString; + use crate::secrets_always_local_mod::*; pub struct SecretEncryptedString { encrypted_string: EncryptedString, @@ -774,18 +785,8 @@ pub(crate) mod secrecy_mod { pub(crate) mod ssh_mod { - #[allow(unused_imports)] - use cargo_auto_lib::BLUE; - use cargo_auto_lib::GREEN; - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - use cargo_auto_lib::YELLOW; - use crate::secrets_always_local_mod::*; - // bring trait into scope - use secrecy::ExposeSecret; - pub struct SshContext { signed_passcode_is_a_secret: secrecy::SecretVec, decrypted_string: secrecy::SecretString, @@ -811,7 +812,7 @@ pub(crate) mod ssh_mod { self.decrypted_string = decryptor.return_secret_string().clone(); } - /// get token and encrypt + /// get secret_token and encrypt fn get_secret_token_and_encrypt(&self) -> cargo_auto_encrypt_secret_lib::EncryptedString { /// Internal function used only for test configuration /// @@ -826,7 +827,7 @@ pub(crate) mod ssh_mod { #[cfg(not(test))] fn get_secret_token() -> secrecy::SecretString { eprintln!(" "); - eprintln!(" {BLUE}Enter the API secret_token to encrypt:{RESET}"); + eprintln!(" {BLUE}Enter the secret_token to encrypt:{RESET}"); secrecy::SecretString::new( inquire::Password::new("") .without_confirmation() @@ -835,9 +836,9 @@ pub(crate) mod ssh_mod { .unwrap(), ) } - let token_is_a_secret = get_secret_token(); + let secret_token = get_secret_token(); // use this signed as password for symmetric encryption - let encryptor = encrypt_mod::Encryptor::new_for_encrypt(token_is_a_secret, &self.signed_passcode_is_a_secret); + let encryptor = encrypt_mod::Encryptor::new_for_encrypt(secret_token, &self.signed_passcode_is_a_secret); let encrypted_token = encryptor.encrypt_symmetric().unwrap(); // return @@ -886,7 +887,7 @@ pub(crate) mod ssh_mod { } None => { // ask user to think about adding with ssh-add - eprintln!(" {YELLOW}SSH key for encrypted token is not found in the ssh-agent.{RESET}"); + eprintln!(" {YELLOW}SSH key for encrypted secret_token is not found in the ssh-agent.{RESET}"); eprintln!(" {YELLOW}Without ssh-agent, you will have to type the private key passphrase every time. This is more secure, but inconvenient.{RESET}"); eprintln!(" {YELLOW}You can manually add the SSH identity to ssh-agent for 1 hour:{RESET}"); eprintln!(" {YELLOW}WARNING: using ssh-agent is less secure, because there is no need for user interaction.{RESET}"); @@ -907,6 +908,7 @@ pub(crate) mod ssh_mod { } } } + /// Expand path and check if identity file exists /// /// Inform the user how to generate identity file. @@ -916,9 +918,11 @@ pub(crate) mod ssh_mod { eprintln!("{RED}Identity file {identity_private_file_path_expanded} that contains the SSH private key does not exist! {RESET}"); eprintln!(" {YELLOW}Create the SSH key manually in bash with this command:{RESET}"); if identity_private_file_path_expanded.as_str().contains("github_api") { - eprintln!(r#"{GREEN}ssh-keygen -t ed25519 -f "{identity_private_file_path_expanded}" -C "github API secret_token"{RESET}"#); + eprintln!(r#"{GREEN}ssh-keygen -t ed25519 -f "{identity_private_file_path_expanded}" -C "github api secret_token"{RESET}"#); } else if identity_private_file_path_expanded.as_str().contains("crates_io") { eprintln!(r#"{GREEN}ssh-keygen -t ed25519 -f "{identity_private_file_path_expanded}" -C "crates io secret_token"{RESET}"#); + } else if identity_private_file_path_expanded.as_str().contains("docker_hub") { + eprintln!(r#"{GREEN}ssh-keygen -t ed25519 -f "{identity_private_file_path_expanded}" -C "docker hub secret_token"{RESET}"#); } eprintln!(" "); panic!("{RED}Error: File {identity_private_file_path_expanded} does not exist! {RESET}"); @@ -932,23 +936,17 @@ pub(crate) mod github_mod { //! Every API call needs the GitHub API secret_token. This is a secret important just like a password. //! I don't want to pass this secret to an "obscure" library crate that is difficult to review. //! This secret will stay here in this codebase that every developer can easily inspect. - //! Instead of the token, I will pass the struct GitHubClient with the trait SendToGitHubApi. - //! This way, the secret token will be encapsulated. + //! Instead of the secret_token, I will pass the struct GitHubClient with the trait SendToGitHubApi. + //! This way, the secret_token will be encapsulated. + use crate::secrets_always_local_mod::*; use cargo_auto_github_lib as cgl; - - use cargo_auto_lib::BLUE; - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - use reqwest::Client; - // bring trait into scope - use secrecy::ExposeSecret; /// Struct GitHubClient contains only private fields /// This fields are accessible only to methods in implementation of traits. pub struct GitHubClient { - /// Passcode for encrypt the token_is_a_secret to encrypted_token in memory. + /// Passcode for encrypt the secret_token to encrypted_token in memory. /// So that the secret is in memory as little as possible as plain text. /// For every session (program start) a new random passcode is created. session_passcode: secrecy::SecretVec, @@ -960,9 +958,9 @@ pub(crate) mod github_mod { impl GitHubClient { /// Create new GitHub client /// - /// Interactively ask the user to input the GitHub token. - pub fn new_interactive_input_token() -> Self { - let mut github_client = Self::new_wo_token(); + /// Interactively ask the user to input the GitHub secret_token. + pub fn new_interactive_input_secret_token() -> Self { + let mut github_client = Self::new_wo_secret_token(); println!("{BLUE}Enter the GitHub API secret_token:{RESET}"); github_client.encrypted_token = @@ -972,8 +970,8 @@ pub(crate) mod github_mod { github_client } - /// Create new GitHub client without token - fn new_wo_token() -> Self { + /// Create new GitHub client without secret_token + fn new_wo_secret_token() -> Self { /// Internal function Generate a random password fn random_byte_passcode() -> [u8; 32] { let mut password = [0_u8; 32]; @@ -990,25 +988,25 @@ pub(crate) mod github_mod { /// Use the stored API secret_token /// - /// If the token not exists ask user to interactively input the token. - /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the token. + /// If the secret_token not exists ask user to interactively input the secret_token. + /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the secret_token. /// it is then possible also to have the ssh key in ssh-agent and write the passphrase only once. - /// But this great user experience comes with security concerns. The token is accessible if the attacker is very dedicated. - pub fn new_with_stored_token() -> Self { + /// But this great user experience comes with security concerns. The secret_token is accessible if the attacker is very dedicated. + pub fn new_with_stored_secret_token() -> Self { /// Internal function for DRY Don't Repeat Yourself - fn read_token_and_decrypt_return_github_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> GitHubClient { - // read the token and decrypt + fn read_secret_token_and_decrypt_return_github_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> GitHubClient { + // read the secret_token and decrypt cargo_auto_encrypt_secret_lib::decrypt_with_ssh_interactive_from_file(&mut ssh_context, encrypted_string_file_path); - let token_is_a_secret = ssh_context.get_decrypted_string(); - let mut github_client = GitHubClient::new_wo_token(); - github_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(token_is_a_secret, &github_client.session_passcode); + let secret_token = ssh_context.get_decrypted_string(); + let mut github_client = GitHubClient::new_wo_secret_token(); + github_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(secret_token, &github_client.session_passcode); github_client } - let encrypted_string_file_path = camino::Utf8Path::new("~/.ssh/github_api_token_encrypted.txt"); + let encrypted_string_file_path = camino::Utf8Path::new("~/.ssh/github_api_secret_token_encrypted.txt"); let encrypted_string_file_path_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(encrypted_string_file_path); - let identity_private_file_path = camino::Utf8Path::new("~/.ssh/github_api_token_ssh_1"); + let identity_private_file_path = camino::Utf8Path::new("~/.ssh/github_api_secret_token_ssh_1"); let _identity_private_file_path_expanded = crate::secrets_always_local_mod::ssh_mod::expand_path_check_private_key_exists(identity_private_file_path); if !encrypted_string_file_path_expanded.exists() { @@ -1016,27 +1014,27 @@ pub(crate) mod github_mod { println!(" {BLUE}Do you want to store the GitHub API secret_token encrypted with an SSH key? (y/n){RESET}"); let answer = inquire::Text::new("").prompt().unwrap(); if answer.to_lowercase() != "y" { - // enter the token manually, not storing - return Self::new_interactive_input_token(); + // enter the secret_token manually, not storing + return Self::new_interactive_input_secret_token(); } else { - // get the passphrase and token interactively + // get the passphrase and secret_token interactively let mut ssh_context = super::ssh_mod::SshContext::new(); - // encrypt and save the encrypted token + // encrypt and save the encrypted secret_token cargo_auto_encrypt_secret_lib::encrypt_with_ssh_interactive_save_file(&mut ssh_context, identity_private_file_path, encrypted_string_file_path); - // read the token and decrypt, return GitHubClient - read_token_and_decrypt_return_github_client(ssh_context, encrypted_string_file_path) + // read the secret_token and decrypt, return GitHubClient + read_secret_token_and_decrypt_return_github_client(ssh_context, encrypted_string_file_path) } } else { // file exists let ssh_context = super::ssh_mod::SshContext::new(); - // read the token and decrypt, return GitHubClient - read_token_and_decrypt_return_github_client(ssh_context, encrypted_string_file_path) + // read the secret_token and decrypt, return GitHubClient + read_secret_token_and_decrypt_return_github_client(ssh_context, encrypted_string_file_path) } } - /// decrypts the secret token in memory + /// decrypts the secret_token in memory #[allow(dead_code)] - pub fn decrypt_token_in_memory(&self) -> secrecy::SecretString { + pub fn decrypt_secret_token_in_memory(&self) -> secrecy::SecretString { self.encrypted_token.expose_decrypted_secret(&self.session_passcode) } } @@ -1047,10 +1045,10 @@ pub(crate) mod github_mod { /// /// This function encapsulates the secret API secret_token. /// The RequestBuilder is created somewhere in the library crate. - /// The client can be passed to the library. It will not reveal the secret token. + /// The client can be passed to the library. It will not reveal the secret_token. fn send_to_github_api(&self, req: reqwest::blocking::RequestBuilder) -> serde_json::Value { // I must build the request to be able then to inspect it. - let req = req.bearer_auth(self.decrypt_token_in_memory().expose_secret()).build().unwrap(); + let req = req.bearer_auth(self.decrypt_secret_token_in_memory().expose_secret()).build().unwrap(); // region: Assert the correct url and https // It is important that the request coming from a external crate/library @@ -1083,11 +1081,11 @@ pub(crate) mod github_mod { /// /// This function encapsulates the secret API secret_token. /// The RequestBuilder is created somewhere in the library crate. - /// The client can be passed to the library. It will not reveal the secret token. + /// The client can be passed to the library. It will not reveal the secret_token. /// This is basically an async fn, but use of `async fn` in public traits is discouraged... async fn upload_to_github(&self, req: reqwest::RequestBuilder) -> serde_json::Value { // I must build the request to be able then to inspect it. - let req = req.bearer_auth(self.decrypt_token_in_memory().expose_secret()).build().unwrap(); + let req = req.bearer_auth(self.decrypt_secret_token_in_memory().expose_secret()).build().unwrap(); // region: Assert the correct url and https // It is important that the request coming from a external crate/library @@ -1123,21 +1121,17 @@ pub(crate) mod crates_io_mod { //! Publish to crates.io needs the crates.io secret_token. This is a secret important just like a password. //! I don't want to pass this secret to an "obscure" library crate that is difficult to review. //! This secret will stay here in this codebase that every developer can easily inspect. - //! Instead of the token, I will pass the struct CratesIoClient with the trait SendToCratesIo. - //! This way, the secret token will be encapsulated. - - use cargo_auto_lib::BLUE; - use cargo_auto_lib::RED; - use cargo_auto_lib::RESET; - use cargo_auto_lib::YELLOW; + //! Instead of the secret_token, I will pass the struct CratesIoClient with the trait SendToCratesIo. + //! This way, the secret_token will be encapsulated. - // bring trait into scope - use secrecy::ExposeSecret; + use crate::secrets_always_local_mod::*; + use cargo_auto_lib::ShellCommandLimitedDoubleQuotesSanitizer; + use cargo_auto_lib::ShellCommandLimitedDoubleQuotesSanitizerTrait; /// Struct CratesIoClient contains only private fields /// This fields are accessible only to methods in implementation of traits. pub struct CratesIoClient { - /// Passcode for encrypt the token_is_a_secret to encrypted_token in memory. + /// Passcode for encrypt the secret_token to encrypted_token in memory. /// So that the secret is in memory as little as possible as plain text. /// For every session (program start) a new random passcode is created. session_passcode: secrecy::SecretVec, @@ -1151,8 +1145,8 @@ pub(crate) mod crates_io_mod { /// /// Interactively ask the user to input the crates.io secret_token. #[allow(dead_code)] - pub fn new_interactive_input_token() -> Self { - let mut crates_io_client = Self::new_wo_token(); + pub fn new_interactive_input_secret_token() -> Self { + let mut crates_io_client = Self::new_wo_secret_token(); println!("{BLUE}Enter the crates.io secret_token:{RESET}"); crates_io_client.encrypted_token = @@ -1162,9 +1156,9 @@ pub(crate) mod crates_io_mod { crates_io_client } - /// Create new CratesIo client without token + /// Create new CratesIo client without secret_token #[allow(dead_code)] - fn new_wo_token() -> Self { + fn new_wo_secret_token() -> Self { /// Internal function Generate a random password fn random_byte_passcode() -> [u8; 32] { let mut password = [0_u8; 32]; @@ -1181,26 +1175,35 @@ pub(crate) mod crates_io_mod { /// Use the stored crates.io secret_token /// - /// If the token not exists ask user to interactively input the token. - /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the token. + /// If the secret_token not exists ask user to interactively input the secret_token. + /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the secret_token. /// It is then possible also to have the ssh key in ssh-agent and write the passphrase only once. - /// But this great user experience comes with security concerns. The token is accessible if the attacker is very dedicated. + /// But this great user experience comes with security concerns. The secret_token is accessible if the attacker is very dedicated. #[allow(dead_code)] - pub fn new_with_stored_token() -> Self { + pub fn new_with_stored_secret_token() -> Self { /// Internal function for DRY Don't Repeat Yourself - fn read_token_and_decrypt_return_crates_io_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> CratesIoClient { + fn read_secret_token_and_decrypt_return_crates_io_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> CratesIoClient { cargo_auto_encrypt_secret_lib::decrypt_with_ssh_interactive_from_file(&mut ssh_context, encrypted_string_file_path); - let token_is_a_secret = ssh_context.get_decrypted_string(); - let mut crates_io_client = CratesIoClient::new_wo_token(); - crates_io_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(token_is_a_secret, &crates_io_client.session_passcode); + let secret_token = ssh_context.get_decrypted_string(); + let mut crates_io_client = CratesIoClient::new_wo_secret_token(); + crates_io_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(secret_token, &crates_io_client.session_passcode); crates_io_client } - let encrypted_string_file_path = camino::Utf8Path::new("~/.ssh/crates_io_token_encrypted.txt"); + // check if the plain-text file from `podman login` exists and warn the user because it is a security vulnerability. + let file_auth = "~/.cargo/credentials.toml"; + let file_auth = camino::Utf8Path::new(file_auth); + // TODO: check for env variable also? + let file_auth_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(file_auth); + let file_auth_expanded = camino::Utf8Path::new(&file_auth_expanded); + if file_auth_expanded.exists() { + eprintln!("{RED}Security vulnerability: Found the cargo file with plain-text secret_token: {file_auth_expanded}. It would be better to inspect and remove it. {RESET}") + } + + let encrypted_string_file_path = camino::Utf8Path::new("~/.ssh/crates_io_secret_token_encrypted.txt"); let encrypted_string_file_path_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(encrypted_string_file_path); - let identity_private_file_path = camino::Utf8Path::new("~/.ssh/crates_io_token_ssh_1"); - + let identity_private_file_path = camino::Utf8Path::new("~/.ssh/crates_io_secret_token_ssh_1"); let _identity_private_file_path_expanded = crate::secrets_always_local_mod::ssh_mod::expand_path_check_private_key_exists(identity_private_file_path); if !encrypted_string_file_path_expanded.exists() { @@ -1208,45 +1211,190 @@ pub(crate) mod crates_io_mod { println!(" {BLUE}Do you want to store the crates.io secret_token encrypted with an SSH key? (y/n){RESET}"); let answer = inquire::Text::new("").prompt().unwrap(); if answer.to_lowercase() != "y" { - // enter the token manually, not storing - return Self::new_interactive_input_token(); + // enter the secret_token manually, not storing + return Self::new_interactive_input_secret_token(); } else { - // get the passphrase and token interactively + // get the passphrase and secret_token interactively let mut ssh_context = super::ssh_mod::SshContext::new(); - // encrypt and save the encrypted token + // encrypt and save the encrypted secret_token cargo_auto_encrypt_secret_lib::encrypt_with_ssh_interactive_save_file(&mut ssh_context, identity_private_file_path, encrypted_string_file_path); - // read the token and decrypt, return CratesIoClient - read_token_and_decrypt_return_crates_io_client(ssh_context, encrypted_string_file_path) + // read the secret_token and decrypt, return CratesIoClient + read_secret_token_and_decrypt_return_crates_io_client(ssh_context, encrypted_string_file_path) } } else { // file exists let ssh_context = super::ssh_mod::SshContext::new(); - // read the token and decrypt, return CratesIoClient - read_token_and_decrypt_return_crates_io_client(ssh_context, encrypted_string_file_path) + // read the secret_token and decrypt, return CratesIoClient + read_secret_token_and_decrypt_return_crates_io_client(ssh_context, encrypted_string_file_path) } } - /// decrypts the secret token in memory + /// decrypts the secret_token in memory #[allow(dead_code)] - pub fn decrypt_token_in_memory(&self) -> secrecy::SecretString { + pub fn decrypt_secret_token_in_memory(&self) -> secrecy::SecretString { self.encrypted_token.expose_decrypted_secret(&self.session_passcode) } /// Publish to crates.io /// /// This function encapsulates the secret crates.io secret_token. - /// The client can be passed to the library. It will not reveal the secret token. + /// The client can be passed to the library. It will not reveal the secret_token. #[allow(dead_code)] pub fn publish_to_crates_io(&self) { - // print command without the token - println!("{YELLOW}cargo publish --token [REDACTED]{RESET}"); - let shell_command = format!("cargo publish --token {}", self.decrypt_token_in_memory().expose_secret()); - let status = std::process::Command::new("sh").arg("-c").arg(shell_command).spawn().unwrap().wait().unwrap(); - let exit_code = status.code().expect(&format!("{RED}Error: publish to crates.io error. {RESET}")); - if exit_code != 0 { - panic!("{RED}Error: publish to crates.io error {exit_code}. {RESET}"); + // the secret_token is redacted when print on screen + ShellCommandLimitedDoubleQuotesSanitizer::new(r#"cargo publish --token "{secret_token}" "#) + .unwrap_or_else(|e| panic!("{e}")) + .arg_secret("{secret_token}", &self.decrypt_secret_token_in_memory()) + .unwrap_or_else(|e| panic!("{e}")) + .run() + .unwrap_or_else(|e| panic!("{e}")); + } + } +} + +pub(crate) mod docker_hub_mod { + + //! Push to docker-hub needs the docker hub secret_token. This is a secret important just like a password. + //! I don't want to pass this secret to an "obscure" library crate that is difficult to review. + //! This secret will stay here in this codebase that every developer can easily inspect. + //! Instead of the secret_token, I will pass the struct DockerHubClient with the trait SendToDockerHub. + //! This way, the secret_token will be encapsulated. + + use crate::secrets_always_local_mod::*; + use cargo_auto_lib::ShellCommandLimitedDoubleQuotesSanitizer; + use cargo_auto_lib::ShellCommandLimitedDoubleQuotesSanitizerTrait; + + /// Struct DockerHubClient contains only private fields + /// This fields are accessible only to methods in implementation of traits. + pub struct DockerHubClient { + /// Passcode for encrypt the secret_token to encrypted_token in memory. + /// So that the secret is in memory as little as possible as plain text. + /// For every session (program start) a new random passcode is created. + session_passcode: secrecy::SecretVec, + + /// private field is set only once in the new() constructor + encrypted_token: super::secrecy_mod::SecretEncryptedString, + } + + impl DockerHubClient { + /// Create new DockerHub client + /// + /// Interactively ask the user to input the docker hub secret_token. + #[allow(dead_code)] + pub fn new_interactive_input_secret_token() -> Self { + let mut docker_hub_client = Self::new_wo_secret_token(); + + println!("{BLUE}Enter the docker hub secret_token:{RESET}"); + docker_hub_client.encrypted_token = + super::secrecy_mod::SecretEncryptedString::new_with_string(inquire::Password::new("").without_confirmation().prompt().unwrap(), &docker_hub_client.session_passcode); + + // return + docker_hub_client + } + + /// Create new DockerHub client without secret_token + #[allow(dead_code)] + fn new_wo_secret_token() -> Self { + /// Internal function Generate a random password + fn random_byte_passcode() -> [u8; 32] { + let mut password = [0_u8; 32]; + use aes_gcm::aead::rand_core::RngCore; + aes_gcm::aead::OsRng.fill_bytes(&mut password); + password + } + + let session_passcode = secrecy::SecretVec::new(random_byte_passcode().to_vec()); + let encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_string("".to_string(), &session_passcode); + + DockerHubClient { session_passcode, encrypted_token } + } + + /// Use the stored docker hub secret_token + /// + /// If the secret_token not exists ask user to interactively input the secret_token. + /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the secret_token. + /// It is then possible also to have the ssh key in ssh-agent and write the passphrase only once. + /// But this great user experience comes with security concerns. The secret_token is accessible if the attacker is very dedicated. + #[allow(dead_code)] + pub fn new_with_stored_secret_token(user_name: &str, registry: &str) -> Self { + /// Internal function for DRY Don't Repeat Yourself + fn read_secret_token_and_decrypt_return_docker_hub_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> DockerHubClient { + cargo_auto_encrypt_secret_lib::decrypt_with_ssh_interactive_from_file(&mut ssh_context, encrypted_string_file_path); + let secret_token = ssh_context.get_decrypted_string(); + let mut docker_hub_client = DockerHubClient::new_wo_secret_token(); + docker_hub_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(secret_token, &docker_hub_client.session_passcode); + docker_hub_client + } + + // check if the plain-text file from `podman login` exists and warn the user because it is a security vulnerability. + let file_auth = "${XDG_RUNTIME_DIR}/containers/auth.json"; + // TODO: check for env variable also? + if let Some(xdg_runtime_dir) = std::env::var_os("XDG_RUNTIME_DIR"){ + let xdg_runtime_dir=xdg_runtime_dir.to_string_lossy().to_string(); + let file_auth_expanded = file_auth.replace("${XDG_RUNTIME_DIR}", &xdg_runtime_dir); + let file_auth_expanded = camino::Utf8Path::new(&file_auth_expanded); + if file_auth_expanded.exists() { + eprintln!("{RED}Security vulnerability: Found the docker hub file with plain-text secret_token: {file_auth_expanded}. It would be better to inspect and remove it. {RESET}") + } + } + + // registry: docker.io -> replace dot into "--"" + // username: bestiadev + let registry_escaped = registry.replace(".", "--"); + let encrypted_string_file_path = format!("~/.ssh/docker_hub_{registry_escaped}_{user_name}.txt"); + let encrypted_string_file_path = camino::Utf8Path::new(&encrypted_string_file_path); + let encrypted_string_file_path_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(encrypted_string_file_path); + + let identity_private_file_path = camino::Utf8Path::new("~/.ssh/docker_hub_secret_token_ssh_1"); + let _identity_private_file_path_expanded = crate::secrets_always_local_mod::ssh_mod::expand_path_check_private_key_exists(identity_private_file_path); + + if !encrypted_string_file_path_expanded.exists() { + // ask interactive + println!(" {BLUE}Do you want to store the docker hub secret_token encrypted with an SSH key? (y/n){RESET}"); + let answer = inquire::Text::new("").prompt().unwrap(); + if answer.to_lowercase() != "y" { + // enter the secret_token manually, not storing + return Self::new_interactive_input_secret_token(); + } else { + // get the passphrase and secret_token interactively + let mut ssh_context = super::ssh_mod::SshContext::new(); + // encrypt and save the encrypted secret_token + cargo_auto_encrypt_secret_lib::encrypt_with_ssh_interactive_save_file(&mut ssh_context, identity_private_file_path, encrypted_string_file_path); + // read the secret_token and decrypt, return DockerHubClient + read_secret_token_and_decrypt_return_docker_hub_client(ssh_context, encrypted_string_file_path) + } + } else { + // file exists + let ssh_context = super::ssh_mod::SshContext::new(); + // read the secret_token and decrypt, return DockerHubClient + read_secret_token_and_decrypt_return_docker_hub_client(ssh_context, encrypted_string_file_path) } } + + /// decrypts the secret_token in memory + #[allow(dead_code)] + pub fn decrypt_secret_token_in_memory(&self) -> secrecy::SecretString { + self.encrypted_token.expose_decrypted_secret(&self.session_passcode) + } + + /// Push to docker hub + /// + /// This function encapsulates the secret docker hub secret_token. + /// The client can be passed to the library. It will not reveal the secret_token. + #[allow(dead_code)] + pub fn push_to_docker_hub(&self, image_url: &str, user_name: &str) { + // the secret_token can be used in place of the password in --cred + ShellCommandLimitedDoubleQuotesSanitizer::new(r#"podman push --creds "{user_name}:{secret_token}" "{image_url}" "#) + .unwrap_or_else(|e| panic!("{e}")) + .arg("{user_name}", user_name) + .unwrap_or_else(|e| panic!("{e}")) + .arg_secret("{secret_token}", &self.decrypt_secret_token_in_memory()) + .unwrap_or_else(|e| panic!("{e}")) + .arg("{image_url}", image_url) + .unwrap_or_else(|e| panic!("{e}")) + .run() + .unwrap_or_else(|e| panic!("{e}")); + } } } "###, @@ -1743,7 +1891,7 @@ pub const RESET: &str = "\x1b[0m"; //! # cargo-auto //! //! **Automation tasks coded in Rust language for the workflow of Rust projects** -//! ***version: 2024.422.214 date: 2024-04-22 author: [bestia.dev](https://bestia.dev) repository: [GitHub](https://github.com/automation-tasks-rs/cargo-auto)*** +//! ***version: 2024.501.55 date: 2024-05-01 author: [bestia.dev](https://bestia.dev) repository: [GitHub](https://github.com/automation-tasks-rs/cargo-auto)*** //! //! ![maintained](https://img.shields.io/badge/maintained-green) //! ![ready-for-use](https://img.shields.io/badge/ready_for_use-green) @@ -1763,11 +1911,11 @@ pub const RESET: &str = "\x1b[0m"; //! [![Newest docs](https://img.shields.io/badge/newest_docs-blue.svg)](https://automation-tasks-rs.github.io/cargo-auto/cargo_auto/index.html) //! ![cargo-auto](https://bestia.dev/webpage_hit_counter/get_svg_image/959103982.svg) //! -//! [![Lines in Rust code](https://img.shields.io/badge/Lines_in_Rust-3145-green.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -//! [![Lines in Doc comments](https://img.shields.io/badge/Lines_in_Doc_comments-1203-blue.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -//! [![Lines in Comments](https://img.shields.io/badge/Lines_in_comments-704-purple.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +//! [![Lines in Rust code](https://img.shields.io/badge/Lines_in_Rust-2565-green.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +//! [![Lines in Doc comments](https://img.shields.io/badge/Lines_in_Doc_comments-1159-blue.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +//! [![Lines in Comments](https://img.shields.io/badge/Lines_in_comments-615-purple.svg)](https://github.com/automation-tasks-rs/cargo-auto/) //! [![Lines in examples](https://img.shields.io/badge/Lines_in_examples-0-yellow.svg)](https://github.com/automation-tasks-rs/cargo-auto/) -//! [![Lines in tests](https://img.shields.io/badge/Lines_in_tests-10949-orange.svg)](https://github.com/automation-tasks-rs/cargo-auto/) +//! [![Lines in tests](https://img.shields.io/badge/Lines_in_tests-10243-orange.svg)](https://github.com/automation-tasks-rs/cargo-auto/) //! //! Hashtags: #maintained #ready-for-use #rustlang #automation #workflow //! My projects on GitHub are more like a tutorial than a finished product: [bestia-dev tutorials](https://github.com/bestia-dev/tutorials_rust_wasm). diff --git a/template_new_cli/.gitattributes b/template_new_cli/.gitattributes deleted file mode 100644 index bfc13b39..00000000 --- a/template_new_cli/.gitattributes +++ /dev/null @@ -1,16 +0,0 @@ -# Specific git config for the project - -# Declare files that will always have LF line endings on checkout. -*.rs text eol=lf -*.toml text eol=lf -*.md text eol=lf -*.json text eol=lf -*.json5 text eol=lf -*.lock text eol=lf -*.yml text eol=lf -*.html text eol=lf -*.js text eol=lf -*.css text eol=lf -LICENSE text eol=lf -.gitignore text eol=lf -.gitattributes text eol=lf \ No newline at end of file diff --git a/template_new_cli/.github/workflows/clear_all_cache.yml b/template_new_cli/.github/workflows/clear_all_cache.yml deleted file mode 100644 index 475fd5df..00000000 --- a/template_new_cli/.github/workflows/clear_all_cache.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: cleanup caches on main - -# Configure Manual Trigger with workflow_dispatch -on: - workflow_dispatch: - -jobs: - cleanup: - runs-on: ubuntu-latest - permissions: - # `actions:write` permission is required to delete caches - # See also: https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28#delete-a-github-actions-cache-for-a-repository-using-a-cache-id - actions: write - contents: read - steps: - - name: checkout - uses: actions/checkout@v4 - - - name: Cleanup - run: | - gh extension install actions/gh-actions-cache - - REPO=${{ github.repository }} - printf "$REPO\n" - BRANCH=main - printf "$BRANCH\n" - - # loop until the list is empty, because it deletes only 30 per page - has_items=true - while [ "$has_items" = true ] - do - printf "\033[0;33m Fetching list of cache key\n\033[0m\n" - printf "\033[0;32m gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 \n\033[0m\n" - cache_keys=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 ) - # printf "$cache_keys\n" - if [ -z "$cache_keys" ]; then - printf "\033[0;35m gh actions-cache list returned nothing.\n\033[0m\n" - has_items=false - fi - ## Setting this to not fail the workflow while deleting cache keys. - set +e - for cacheKey in $cache_keys - do - # printf "\033[0;32m gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm\n\033[0m\n" - gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm - done - done - printf "\033[0;33m Done\n\033[0m\n" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - diff --git a/template_new_cli/.github/workflows/docs_pages.yml b/template_new_cli/.github/workflows/docs_pages.yml deleted file mode 100644 index 7ec37ea3..00000000 --- a/template_new_cli/.github/workflows/docs_pages.yml +++ /dev/null @@ -1,43 +0,0 @@ -# Simple workflow for deploying static content to GitHub Pages -name: docs_pages - -on: - # Runs on pushes targeting the default branch - push: - branches: ["main"] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - # Single deploy job since we're just deploying - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Pages - uses: actions/configure-pages@v4 - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - # Upload entire repository - path: 'docs' - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/template_new_cli/.github/workflows/rust_fmt_auto_build_test.yml b/template_new_cli/.github/workflows/rust_fmt_auto_build_test.yml deleted file mode 100644 index 3ae7d4cd..00000000 --- a/template_new_cli/.github/workflows/rust_fmt_auto_build_test.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: rust_fmt_auto_build_test - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -env: - CARGO_TERM_COLOR: always - -jobs: - rust_fmt_auto_build_test: - - runs-on: ubuntu-latest - - steps: - - name: checkout - uses: actions/checkout@v4 - - - name: cargo fmt -- --check - run: cargo fmt -- --check - - - name: Run cache for rust dependencies - uses: Swatinem/rust-cache@v2.7.3 - - - name: Configure sccache - run: printf "RUSTC_WRAPPER=sccache\n" >> $GITHUB_ENV; printf "SCCACHE_GHA_ENABLED=true\n" >> $GITHUB_ENV - - - name: Run sccache-cache for artifacts - uses: mozilla-actions/sccache-action@v0.0.4 - - - name: install and cache cargo-auto - uses: baptiste0928/cargo-install@v3.0.0 - with: - crate: cargo-auto - - - name: Cache for automation tasks - uses: actions/cache@v4.0.0 - with: - path: | - /home/runner/work/cargo-auto/cargo-auto/automation_tasks_rs/.file_hashes.json - /home/runner/work/cargo-auto/cargo-auto/automation_tasks_rs/target - /home/runner/work/cargo-auto/cargo-auto/automation_tasks_rs/Cargo.toml - key: automation_tasks_rs - - - name: cargo auto build - run: cargo auto build - - - name: cargo auto test - run: cargo auto test - diff --git a/template_new_cli/.gitignore b/template_new_cli/.gitignore deleted file mode 100644 index a96601e5..00000000 --- a/template_new_cli/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -/target - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -# Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk - -# not needed in commits, but also not a problem if they are committed -/.automation_tasks_rs_file_hashes.json \ No newline at end of file diff --git a/template_new_cli/.vscode/settings.json b/template_new_cli/.vscode/settings.json deleted file mode 100644 index e635ea73..00000000 --- a/template_new_cli/.vscode/settings.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "workbench.colorCustomizations": { - "titleBar.activeForeground": "#fff", - "titleBar.inactiveForeground": "#ffffffcc", - "titleBar.activeBackground": "#404040", - "titleBar.inactiveBackground": "#2d2d2dcc" - }, - "spellright.language": [ - "en" - ], - "spellright.documentTypes": [ - "markdown", - "latex", - "plaintext" - ], - "files.associations": { - "LICENSE": "plain text" - }, - "rust-analyzer.showUnlinkedFileNotification": false, - "cSpell.words": [ - "Alla", - "alloc", - "appender", - "bestia", - "bestiadev", - "camino", - "CRUSTDE", - "Decryptor", - "Encryptor", - "endregion", - "keygen", - "Nazdravlje", - "passcode", - "plantuml", - "Prost", - "reqwest", - "rustdevuser", - "rustlang", - "rustprojects", - "serde", - "struct", - "subsecond", - "substack", - "thiserror", - "zcvf", - "zdravje" - ] -} \ No newline at end of file diff --git a/template_new_cli/Cargo.toml b/template_new_cli/Cargo.toml deleted file mode 100644 index 36e1e829..00000000 --- a/template_new_cli/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "cargo_auto_template_new_cli" -version = "0.0.1" -description = "Basic Rust project template for CLI and library, more than just `cargo new hello`" -authors = ["bestia.dev"] -homepage = "https://bestia.dev" -edition = "2021" -license = "MIT" -readme = "README.md" -repository = "https://github.com/github_owner/cargo_auto_template_new_cli" -# Keyword must be only one word: lowercase letters, hyphens(-) or numbers, less then 35 characters, at most 5 keywords per crate -keywords = ["maintained", "work-in-progress", "rustlang"] -categories = ["command-line-interface"] -# publish as a cargo tool. Only this files. -publish = false -include = [ - "Cargo.toml", - "LICENSE", - "README.md", - "src/*" -] - -[dependencies] -log = "0.4" -pretty_env_logger="0.5.0" -thiserror = "1.0.30" -anyhow="1.0.56" - -[lib] -name = "cargo_auto_template_new_cli_lib" -path = "src/lib.rs" -# A flag for enabling documentation of this target. This is used by `cargo doc`. -doc = true - -[[bin]] -name = "cargo_auto_template_new_cli" -path = "src/bin/cargo_auto_template_new_cli/main.rs" -# A flag for enabling documentation of this target. This is used by `cargo doc`. -doc = true diff --git a/template_new_cli/DEVELOPMENT.md b/template_new_cli/DEVELOPMENT.md deleted file mode 100644 index dffec0b0..00000000 --- a/template_new_cli/DEVELOPMENT.md +++ /dev/null @@ -1,82 +0,0 @@ -# Development details - -## CRUSTDE - Containerized Rust Development Environment - -I recommend using the CRUSTDE - Containerized Rust Development Environment to write Rust projects. Follow the instructions here . - -It is an isolated development environment that will not mess with you system. -It will work on Linux (tested on Debian) and inside WSL (Windows Subsystem for Linux). - -You just need to install the newer alternative to Docker: [podman](https://podman.io/). Then you download the prepared container image from DockerHub (3GB). And then a little juggling with ssh keys. All this is simplified by running a few bash scripts. Just follow the easy instructions. - -The container image contains cargo, rustc, wasm-pack, basic-http-server, cargo-auto and other utils that a Rust project needs. - -## Workflow with automation_tasks_rs and cargo-auto - -For easy workflow, use the automation tasks that are already coded in the sub-project `automation_tasks_rs`. This is a basic workflow: - -```bash -cargo auto build -cargo auto release -cargo auto doc -cargo auto test -cargo auto commit_and push -cargo auto publish_to_crates_io -cargo auto github_new_release -``` - -Every task finishes with instructions how to proceed. -The [cargo-auto](https://github.com/automation-tasks-rs/cargo-auto) and [dev_bestia_cargo_completion](https://github.com/automation-tasks-rs/dev_bestia_cargo_completion) are already installed inside the CRUSTDE container. - -You can open the automation sub-project in VSCode and then code your own tasks in Rust. - -```bash -code automation_tasks_rs -``` - -## Separate main.rs and lib.rs - -It is always good to split the project between a `main.rs` (executable) and a `lib.rs` (library crate). - -Even for the smallest project. Maybe some other program will use the library eventually. - -All the input/output is coded in the `main.rs`: keyboard and monitor (stdin and stdout), access to files, and some access to the network. -The library must not operate directly with the stdin/stdout, because some other caller of the library can have other ideas around input-output options. Maybe it is a Graphical user interface that does things completely different than CLI applications. - -A separate `lib.rs` enables one to make good tests and examples without worrying about input-output. - -## super simple argument parsing - -I use a super simple code to parse CLI arguments inside the `src/bin/cargo_auto_template_new_cli/main.rs`. There are crate libraries that enable very complex argument parsing if needed. - -## Modules - -I added one module `hello_mod.rs` just to showcase how modules are used in separate files. - -## Markdown - -README.md and all the doc-comments are in markdown. To separate paragraphs in markdown use an empty line between them. -I tried other variants like double-space or backslash, but an empty line is the most used in the wild. - -## tests - -I added a unit-test, just to show how it looks. And an integration-test. So it is "ready-to-go". -Run them with `cargo test`. - -## examples - -In the directory `examples` every rs file is a bin-executable. -Run it with: - -```bash -cargo run --example example_1 -``` - -## Error handling thiserror and anyhow - -Rule number one is never to use `.unwrap()` in your real Rust code. It is a sign, you are not Error handling properly. -Maybe `unwrap()` can be fine for some fast learning examples, but for any real-life Rust code, you must use some `Error handling`. There are many different ways to do that in Rust. I choose the pair of libraries `thiserror` and `anyhow`. The first is made for libraries, the second is made for bin-executables. -The library needs an Enum with all the possible errors that this library can return. With `#[derive(Error)]` this enum gets everything needed to be a true Rust error struct. Every error can have a formatting string and a struct of data. -The bin-executable does not want to be involved in every possible error separately. It needs an umbrella for all possible errors with `anyhow::Result`. -Inside the code, mostly propagate the errors with the `?` Operator after the `Result` value instead of unwrap() or the match expression. -In the tests we don't want to work with Error handling. There, instead of `.unwrap()`, use the similar function `.expect(&str)` that has an additional description string. I use expect() when I am 100% sure the panic cannot happen because I checked some conditions before it. diff --git a/template_new_cli/LICENSE b/template_new_cli/LICENSE deleted file mode 100644 index d7586859..00000000 --- a/template_new_cli/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 bestia.dev - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/template_new_cli/README.md b/template_new_cli/README.md deleted file mode 100644 index fd9504c9..00000000 --- a/template_new_cli/README.md +++ /dev/null @@ -1,67 +0,0 @@ -[//]: # (auto_md_to_doc_comments segment start A) - -# cargo_auto_template_new_cli - -[//]: # (auto_cargo_toml_to_md start) - -**Basic Rust project template for CLI and library, more than just `cargo new hello`** -***version: 1.0.4 date: 2024-04-21 author: [bestia.dev](https://bestia.dev) repository: [GitHub](https://github.com/github_owner/cargo_auto_template_new_cli)*** - -[//]: # (auto_cargo_toml_to_md end) - - [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/github_owner/cargo_auto_template_new_cli/blob/main/LICENSE) - [![Rust](https://github.com/github_owner/cargo_auto_template_new_cli/workflows/rust_fmt_auto_build_test/badge.svg)](https://github.com/github_owner/cargo_auto_template_new_cli/) - -[//]: # (auto_lines_of_code start) - -[![Lines in Rust code](https://img.shields.io/badge/Lines_in_Rust-89-green.svg)](https://github.com/github_owner/cargo_auto_template_new_cli/) -[![Lines in Doc comments](https://img.shields.io/badge/Lines_in_Doc_comments-13-blue.svg)](https://github.com/github_owner/cargo_auto_template_new_cli/) -[![Lines in Comments](https://img.shields.io/badge/Lines_in_comments-36-purple.svg)](https://github.com/github_owner/cargo_auto_template_new_cli/) -[![Lines in examples](https://img.shields.io/badge/Lines_in_examples-19-yellow.svg)](https://github.com/github_owner/cargo_auto_template_new_cli/) -[![Lines in tests](https://img.shields.io/badge/Lines_in_tests-30-orange.svg)](https://github.com/github_owner/cargo_auto_template_new_cli/) - -[//]: # (auto_lines_of_code end) - -Hashtags: #maintained #ready-for-use #rustlang #automation #workflow -My projects on GitHub are more like a tutorial than a finished product: [bestia-dev tutorials](https://github.com/bestia-dev/tutorials_rust_wasm). -I recommend using the [CRUSTDE - Containerized Rust Development Environment](https://github.com/CRUSTDE-ContainerizedRustDevEnv/crustde_cnt_img_pod) to write Rust projects on Linux, isolated from your system. - -## This template - -Just like `cargo new` makes a soft and gentle introduction to Rust projects and development, I want to make a similar command that creates a real-life Rust project. - -```bash -cargo auto new_cli project_name -``` - -Extremely simple, just the basic moving parts and use cases. - -## Development details - -Read the development details in a separate md file: -[DEVELOPMENT.md](DEVELOPMENT.md) - -## Releases changelog - -Read the releases changelog in a separate md file: -[RELEASES.md](RELEASES.md) - -## TODO - -And code happily ever after... - -## Open-source and free as a beer - -My open-source projects are free as a beer (MIT license). -I just love programming. -But I need also to drink. If you find my projects and tutorials helpful, please buy me a beer by donating to my [PayPal](https://paypal.me/LucianoBestia). -You know the price of a beer in your local bar ;-) -So I can drink a free beer for your health :-) -[Na zdravje!](https://translate.google.com/?hl=en&sl=sl&tl=en&text=Na%20zdravje&op=translate) [Alla salute!](https://dictionary.cambridge.org/dictionary/italian-english/alla-salute) [Prost!](https://dictionary.cambridge.org/dictionary/german-english/prost) [Nazdravlje!](https://matadornetwork.com/nights/how-to-say-cheers-in-50-languages/) 🍻 - -[//bestia.dev](https://bestia.dev) -[//github.com/bestia-dev](https://github.com/bestia-dev) -[//bestiadev.substack.com](https://bestiadev.substack.com) -[//youtube.com/@bestia-dev-tutorials](https://youtube.com/@bestia-dev-tutorials) - -[//]: # (auto_md_to_doc_comments segment end A) diff --git a/template_new_cli/RELEASES.md b/template_new_cli/RELEASES.md deleted file mode 100644 index 5dc81456..00000000 --- a/template_new_cli/RELEASES.md +++ /dev/null @@ -1,15 +0,0 @@ -# Releases changelog of cargo_auto_template_new_cli - -All notable changes to this project will be documented in this file. -This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -The library releases will be published on crates.io. -The cargo-auto automation task will use the content of the section `## Unreleased` to create -the GitHub release consistently with this file. -The ongoing changes that are not released, are visible in the git commits and GitHub pull requests. -The TODO section is part of the [README.md](https://github.com/github_owner/cargo_auto_template_new_cli). - -## Unreleased - -## Version 0.0.1 - -- Rust project created with `cargo auto new_cli cargo_auto_template_new_cli` diff --git a/template_new_cli/automation_tasks_rs/.gitignore b/template_new_cli/automation_tasks_rs/.gitignore deleted file mode 100644 index 4aa0e28e..00000000 --- a/template_new_cli/automation_tasks_rs/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -/target -/logs - -# so the GitHub action gets the fresh libraries -Cargo.lock - -# not needed in commits, but also not a problem if they are committed -/.file_hashes.json -/.old_metadata.json diff --git a/template_new_cli/automation_tasks_rs/.vscode/settings.json b/template_new_cli/automation_tasks_rs/.vscode/settings.json deleted file mode 100644 index 48903b1b..00000000 --- a/template_new_cli/automation_tasks_rs/.vscode/settings.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "workbench.colorCustomizations": { - "titleBar.activeForeground": "#fff", - "titleBar.inactiveForeground": "#ffffffcc", - "titleBar.activeBackground": "#a81c1c", - "titleBar.inactiveBackground": "#630b0bcc" - }, - "spellright.language": [ - "en" - ], - "spellright.documentTypes": [ - "markdown", - "latex", - "plaintext" - ], - "files.associations": { - "LICENSE": "plain text" - }, - "rust-analyzer.showUnlinkedFileNotification": false, - "cSpell.words": [ - "Alla", - "alloc", - "appender", - "bestia", - "bestiadev", - "camino", - "CRUSTDE", - "decryptor", - "encryptor", - "endregion", - "keygen", - "Nazdravlje", - "new_cli", - "octocrab", - "passcode", - "plantuml", - "Prost", - "reqwest", - "rustdevuser", - "rustprojects", - "serde", - "sshadd", - "struct", - "subsecond", - "substack", - "thiserror", - "zcvf", - "zdravje", - "zeroize" - ] -} \ No newline at end of file diff --git a/template_new_cli/automation_tasks_rs/Cargo.toml b/template_new_cli/automation_tasks_rs/Cargo.toml deleted file mode 100644 index dfc65e18..00000000 --- a/template_new_cli/automation_tasks_rs/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "automation_tasks_rs" -version = "1.0.0" -authors = ["bestia.dev"] -homepage = "https://bestia.dev" -edition = "2021" -description = "Automation tasks coded in Rust language for the workflow of Rust projects" -publish = false - -[dependencies] -cargo_auto_lib = "2.4.8" -cargo_auto_github_lib = "1.1.6" -cargo_auto_encrypt_secret_lib = "1.1.7" - -inquire = "0.7.0" -serde_json = {version= "1.0.114", features=["std"]} - -# the version of reqwest must be the same as the version in the library cargo_auto_github_lib -reqwest = { version = "0.12.3", features = ["blocking", "stream"] } - -camino = "1.1.6" -aes-gcm = "0.10.3" -ssh-key = { version = "0.6.4", features = [ "rsa", "encryption"] } -rsa = { version = "0.9.6", features = ["sha2","pem"] } -secrecy = { version="0.8.0", features=["alloc"]} -base64ct = {version = "1.6.0", features = ["alloc"] } - -tracing = "0.1.40" -tracing-subscriber = { version = "0.3.18", features = ["env-filter", "std", "fmt", "time"] } -tracing-appender="0.2.2" -time = {version="0.3.36", features=["macros","local-offset"]} diff --git a/template_new_cli/automation_tasks_rs/rustfmt.toml b/template_new_cli/automation_tasks_rs/rustfmt.toml deleted file mode 100644 index 420a4a84..00000000 --- a/template_new_cli/automation_tasks_rs/rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -max_width = 200 \ No newline at end of file diff --git a/template_new_cli/automation_tasks_rs/src/main.rs b/template_new_cli/automation_tasks_rs/src/main.rs deleted file mode 100644 index fe63f184..00000000 --- a/template_new_cli/automation_tasks_rs/src/main.rs +++ /dev/null @@ -1,463 +0,0 @@ -// automation_tasks_rs for cargo_auto_template_new_cli - -// region: library and modules with basic automation tasks - -// for projects that don't use GitHub, delete all the mentions of GitHub -mod secrets_always_local_mod; -use crate::secrets_always_local_mod::crates_io_mod; -use crate::secrets_always_local_mod::github_mod; - -use cargo_auto_github_lib as cgl; -use cargo_auto_lib as cl; - -use cl::GREEN; -use cl::RED; -use cl::RESET; -use cl::YELLOW; - -// traits must be in scope (Rust strangeness) -use cgl::SendToGitHubApi; -use cl::CargoTomlPublicApiMethods; -use cl::ShellCommandLimitedDoubleQuotesSanitizerTrait; - -// region: library with basic automation tasks - -fn main() { - std::panic::set_hook(Box::new(|panic_info| panic_set_hook(panic_info))); - tracing_init(); - cl::exit_if_not_run_in_rust_project_root_directory(); - - // get CLI arguments - let mut args = std::env::args(); - // the zero argument is the name of the program - let _arg_0 = args.next(); - match_arguments_and_call_tasks(args); -} - -// region: general functions - -/// Initialize tracing to file logs/automation_tasks_rs.log -/// -/// The folder logs/ is in .gitignore and will not be committed. -pub fn tracing_init() { - // uncomment this line to enable tracing to file - // let file_appender = tracing_appender::rolling::daily("logs", "automation_tasks_rs.log"); - - let offset = time::UtcOffset::current_local_offset().expect("should get local offset!"); - let timer = tracing_subscriber::fmt::time::OffsetTime::new(offset, time::macros::format_description!("[hour]:[minute]:[second].[subsecond digits:6]")); - - // Filter out logs from: hyper_util, reqwest - // A filter consists of one or more comma-separated directives - // target[span{field=value}]=level - // examples: tokio::net=info - // directives can be added with the RUST_LOG environment variable: - // export RUST_LOG=automation_tasks_rs=trace - // Unset the environment variable RUST_LOG - // unset RUST_LOG - let filter = tracing_subscriber::EnvFilter::from_default_env() - .add_directive("hyper_util=error".parse().unwrap_or_else(|e| panic!("{e}"))) - .add_directive("reqwest=error".parse().unwrap_or_else(|e| panic!("{e}"))); - - tracing_subscriber::fmt() - .with_file(true) - .with_max_level(tracing::Level::DEBUG) - .with_timer(timer) - .with_line_number(true) - .with_ansi(false) - //.with_writer(file_appender) - .with_env_filter(filter) - .init(); -} - -/// The original Rust report of the panic is ugly for the end user -/// -/// I use panics extensively to stop the execution. I am lazy to implement a super complicated error handling. -/// I just need to stop the execution on every little bit of error. This utility is for developers. They will understand me. -/// For errors I print the location. If the message contains "Exiting..." than it is a "not-error exit" and the location is not important. -fn panic_set_hook(panic_info: &std::panic::PanicInfo) { - let mut string_message = "".to_string(); - if let Some(message) = panic_info.payload().downcast_ref::() { - string_message = message.to_owned(); - } - if let Some(message) = panic_info.payload().downcast_ref::<&str>() { - string_message.push_str(message); - } - - tracing::debug!("{string_message}"); - eprintln!("{string_message}"); - - if !string_message.contains("Exiting...") { - let file = panic_info.location().unwrap().file(); - let line = panic_info.location().unwrap().line(); - let column = panic_info.location().unwrap().column(); - tracing::debug!("Location: {file}:{line}:{column}"); - eprintln!("Location: {file}:{line}:{column}"); - } -} - -// endregion: general functions - -// region: match, help and completion - -/// match arguments and call tasks functions -fn match_arguments_and_call_tasks(mut args: std::env::Args) { - // the first argument is the user defined task: (no argument for help), build, release,... - let arg_1 = args.next(); - match arg_1 { - None => print_help(), - Some(task) => { - if &task == "completion" { - completion(); - } else { - println!("{YELLOW}Running automation task: {task}{RESET}"); - if &task == "build" { - task_build(); - } else if &task == "release" { - task_release(); - } else if &task == "doc" { - task_doc(); - } else if &task == "test" { - task_test(); - } else if &task == "commit_and_push" { - let arg_2 = args.next(); - task_commit_and_push(arg_2); - } else if &task == "publish_to_crates_io" { - task_publish_to_crates_io(); - } else if &task == "github_new_release" { - task_github_new_release(); - } else { - eprintln!("{RED}Error: Task {task} is unknown.{RESET}"); - print_help(); - } - } - } - } -} - -/// write a comprehensible help for user defined tasks -fn print_help() { - println!( - r#" - {YELLOW}Welcome to cargo-auto !{RESET} - {YELLOW}This program automates your custom tasks when developing a Rust project.{RESET} - - {YELLOW}User defined tasks in automation_tasks_rs:{RESET} -{GREEN}cargo auto build{RESET} - {YELLOW}builds the crate in debug mode, fmt, increment version{RESET} -{GREEN}cargo auto release{RESET} - {YELLOW}builds the crate in release mode, fmt, increment version{RESET} -{GREEN}cargo auto doc{RESET} - {YELLOW}builds the docs, copy to docs directory{RESET} -{GREEN}cargo auto test{RESET} - {YELLOW}runs all the tests{RESET} -{GREEN}cargo auto commit_and_push "message"{RESET} - {YELLOW}commits with message and push with mandatory message{RESET} - {YELLOW}It is preferred to use SSH for git push to GitHub.{RESET} - {YELLOW}{YELLOW} - {YELLOW}On the very first commit, this task will initialize a new local git repository and create a remote GitHub repo.{RESET} - {YELLOW}For the GitHub API the task needs the Personal Access secret_token Classic from {RESET} - {YELLOW}You can choose to type the secret_token every time or to store it in a file encrypted with an SSH key.{RESET} - {YELLOW}Then you can type the passphrase of the private key every time. This is pretty secure.{RESET} - {YELLOW}Somewhat less secure (but more comfortable) way is to store the private key in ssh-agent.{RESET} -{GREEN}cargo auto publish_to_crates_io{RESET} - {YELLOW}publish to crates.io, git tag{RESET} - {YELLOW}You need the API secret_token for publishing. Get the secret_token on .{RESET} - {YELLOW}You can choose to type the secret_token every time or to store it in a file encrypted with an SSH key.{RESET} - {YELLOW}Then you can type the passphrase of the private key every time. This is pretty secure.{RESET} - {YELLOW}Somewhat less secure (but more comfortable) way is to store the private key in ssh-agent.{RESET} -{GREEN}cargo auto github_new_release{RESET} - {YELLOW}creates new release on GitHub{RESET} - {YELLOW}For the GitHub API the task needs the Personal Access secret_token Classic from {RESET} - {YELLOW}You can choose to type the secret_token every time or to store it in a file encrypted with an SSH key.{RESET} - {YELLOW}Then you can type the passphrase of the private key every time. This is pretty secure.{RESET} - {YELLOW}Somewhat less secure (but more comfortable) way is to store the private key in ssh-agent.{RESET} - - {YELLOW}© 2024 bestia.dev MIT License github.com/automation-tasks-rs/cargo-auto{RESET} -"# - ); - print_examples_cmd(); -} - -/// all example commands in one place -fn print_examples_cmd() { -/* - println!( - r#" - {YELLOW}run examples:{RESET} -{GREEN}cargo run --example plantuml1{RESET} -"# - ); -*/ -} - -/// sub-command for bash auto-completion of `cargo auto` using the crate `dev_bestia_cargo_completion` -fn completion() { - let args: Vec = std::env::args().collect(); - let word_being_completed = args[2].as_str(); - let last_word = args[3].as_str(); - - if last_word == "cargo-auto" || last_word == "auto" { - let sub_commands = vec!["build", "release", "doc", "test", "commit_and_push", "publish_to_crates_io", "github_new_release"]; - cl::completion_return_one_or_more_sub_commands(sub_commands, word_being_completed); - } - /* - // the second level if needed - else if last_word == "new" { - let sub_commands = vec!["x"]; - cl::completion_return_one_or_more_sub_commands(sub_commands, word_being_completed); - } - */ -} - -// endregion: match, help and completion - -// region: tasks - -/// cargo build -fn task_build() { - let cargo_toml = cl::CargoToml::read(); - cl::auto_version_increment_semver_or_date(); - cl::run_shell_command_static("cargo fmt").unwrap_or_else(|e| panic!("{e}")); - cl::run_shell_command_static("cargo build").unwrap_or_else(|e| panic!("{e}")); - println!( - r#" - {YELLOW}After `cargo auto build`, run the compiled binary, examples and/or tests{RESET} -{GREEN}./target/debug/{package_name} print world{RESET} - {YELLOW}If ok then{RESET} -{GREEN}./target/debug/{package_name} upper world{RESET} - {YELLOW}If ok then{RESET} -{GREEN}./target/debug/{package_name} upper WORLD{RESET} - {YELLOW}if ok then{RESET} -{GREEN}cargo auto release{RESET} -"#, - package_name = cargo_toml.package_name(), - ); - print_examples_cmd(); -} - -/// cargo build --release -fn task_release() { - let cargo_toml = cl::CargoToml::read(); - cl::auto_version_increment_semver_or_date(); - cl::auto_cargo_toml_to_md(); - cl::auto_lines_of_code(""); - - cl::run_shell_command_static("cargo fmt").unwrap_or_else(|e| panic!("{e}")); - cl::run_shell_command_static("cargo build --release").unwrap_or_else(|e| panic!("{e}")); - - cl::ShellCommandLimitedDoubleQuotesSanitizer::new(r#"strip "target/release/{package_name}" "#).unwrap_or_else(|e| panic!("{e}")) - .arg("{package_name}", &cargo_toml.package_name()).unwrap_or_else(|e| panic!("{e}")) - .run().unwrap_or_else(|e| panic!("{e}")); - - println!( - r#" - {YELLOW}After `cargo auto release`, run the compiled binary, examples and/or tests{RESET} -{GREEN}./target/release/{package_name} print world{RESET} - {YELLOW}If ok then{RESET} -{GREEN}./target/release/{package_name} upper world{RESET} - {YELLOW}If ok then{RESET} -{GREEN}./target/release/{package_name} upper WORLD{RESET} - {YELLOW}if ok then{RESET} -{GREEN}cargo auto doc{RESET} -"#, - package_name = cargo_toml.package_name(), - ); - print_examples_cmd(); -} - -/// cargo doc, then copies to /docs/ folder, because this is a GitHub standard folder -fn task_doc() { - let cargo_toml = cl::CargoToml::read(); - cl::auto_cargo_toml_to_md(); - cl::auto_lines_of_code(""); - cl::auto_plantuml(&cargo_toml.package_repository().unwrap()); - cl::auto_playground_run_code(); - cl::auto_md_to_doc_comments(); - - cl::run_shell_command_static("cargo doc --no-deps --document-private-items").unwrap_or_else(|e| panic!("{e}")); - // copy target/doc into docs/ because it is GitHub standard - cl::run_shell_command_static("rsync -a --info=progress2 --delete-after target/doc/ docs/").unwrap_or_else(|e| panic!("{e}")); - - // Create simple index.html file in docs directory - cl::ShellCommandLimitedDoubleQuotesSanitizer::new(r#"printf "\n" > docs/index.html"#) - .unwrap_or_else(|e| panic!("{e}")) - .arg("{url_sanitized_for_double_quote}", &cargo_toml.package_name().replace("-", "_")) - .unwrap_or_else(|e| panic!("{e}")) - .run() - .unwrap_or_else(|e| panic!("{e}")); - - // pretty html - cl::auto_doc_tidy_html().unwrap_or_else(|e| panic!("{e}")); - cl::run_shell_command_static("cargo fmt").unwrap_or_else(|e| panic!("{e}")); - // message to help user with next move - println!( - r#" - {YELLOW}After `cargo auto doc`, ctrl-click on `docs/index.html`. - It will show the index.html in VSCode Explorer, then right-click and choose "Show Preview". - This works inside the CRUSTDE container, because of the extension "Live Preview" - - If ok then run the tests in code and the documentation code examples.{RESET} -{GREEN}cargo auto test{RESET} -"# - ); -} - -/// cargo test -fn task_test() { - cl::run_shell_command_static("cargo test").unwrap_or_else(|e| panic!("{e}")); - println!( - r#" - {YELLOW}After `cargo auto test`. If ok then {RESET} - {YELLOW}(commit message is mandatory){RESET} -{GREEN}cargo auto commit_and_push "message"{RESET} -"# - ); -} - -/// commit and push -fn task_commit_and_push(arg_2: Option) { - let Some(message) = arg_2 else { - eprintln!("{RED}Error: Message for commit is mandatory.{RESET}"); - // early exit - return; - }; - - // If needed, ask to create new local git repository - if !cl::git_is_local_repository() { - cl::new_local_repository(&message).unwrap(); - } - - // If needed, ask to create a GitHub remote repository - if !cgl::git_has_remote() || !cgl::git_has_upstream() { - let github_client = github_mod::GitHubClient::new_with_stored_secret_token(); - cgl::new_remote_github_repository(&github_client).unwrap(); - cgl::description_and_topics_to_github(&github_client); - } else { - let github_client = github_mod::GitHubClient::new_with_stored_secret_token(); - // if description or topics/keywords/tags have changed - cgl::description_and_topics_to_github(&github_client); - - // separate commit for docs if they changed, to not make a lot of noise in the real commit - if std::path::Path::new("docs").exists() { - cl::run_shell_command_static(r#"git add docs && git diff --staged --quiet || git commit -m "update docs" "#).unwrap_or_else(|e| panic!("{e}")); - } - - cl::add_message_to_unreleased(&message); - // the real commit of code - cl::ShellCommandLimitedDoubleQuotesSanitizer::new(r#"git add -A && git diff --staged --quiet || git commit -m "{message_sanitized_for_double_quote}" "#) - .unwrap_or_else(|e| panic!("{e}")) - .arg("{message_sanitized_for_double_quote}", &message) - .unwrap_or_else(|e| panic!("{e}")) - .run() - .unwrap_or_else(|e| panic!("{e}")); - - cl::run_shell_command_static("git push").unwrap_or_else(|e| panic!("{e}")); - } - - println!( - r#" - {YELLOW}After `cargo auto commit_and_push "message"`{RESET} -{GREEN}cargo auto publish_to_crates_io{RESET} -"# - ); -} - -/// publish to crates.io and git tag -fn task_publish_to_crates_io() { - let cargo_toml = cl::CargoToml::read(); - let package_name = cargo_toml.package_name(); - let version = cargo_toml.package_version(); - // take care of tags - let tag_name_version = cl::git_tag_sync_check_create_push(&version); - - // cargo publish with encrypted secret secret_token - let crates_io_client = crates_io_mod::CratesIoClient::new_with_stored_secret_token(); - crates_io_client.publish_to_crates_io(); - - println!( - r#" - {YELLOW}After `cargo auto publish_to_crates_io`, check in browser{RESET} -{GREEN}https://crates.io/crates/{package_name}{RESET} - {YELLOW}Add the dependency to your Rust project and check how it works.{RESET} -{GREEN}{package_name} = "{version}"{RESET} - - {YELLOW}First write the content of the release in the RELEASES.md in the `## Unreleased` section, then{RESET} - {YELLOW}Then create the GitHub-Release for {tag_name_version}.{RESET} -{GREEN}cargo auto github_new_release{RESET} -"# - ); -} - -/// create a new release on github -fn task_github_new_release() { - let cargo_toml = cl::CargoToml::read(); - let version = cargo_toml.package_version(); - // take care of tags - let tag_name_version = cl::git_tag_sync_check_create_push(&version); - - let github_owner = cargo_toml.github_owner().unwrap(); - let repo_name = cargo_toml.package_name(); - let now_date = cl::now_utc_date_iso(); - let release_name = format!("Version {} ({})", &version, now_date); - let branch = "main"; - - // First, the user must write the content into file RELEASES.md in the section ## Unreleased. - // Then the automation task will copy the content to GitHub release - let body_md_text = cl::body_text_from_releases_md().unwrap(); - - let github_client = github_mod::GitHubClient::new_with_stored_token(); - let json_value = github_client.send_to_github_api(cgl::github_api_create_new_release(&github_owner, &repo_name, &tag_name_version, &release_name, branch, &body_md_text)); - // early exit on error - if let Some(error_message) = json_value.get("message") { - eprintln!("{RED}{error_message}{RESET}"); - if let Some(errors) = json_value.get("errors") { - let errors = errors.as_array().unwrap(); - for error in errors.iter() { - if let Some(code) = error.get("code") { - eprintln!("{RED}{code}{RESET}"); - } - } - } - panic!("{RED}Call to GitHub API returned an error.{RESET}") - } - - // Create a new Version title in RELEASES.md. - cl::create_new_version_in_releases_md(&release_name).unwrap(); - - println!( - " - {YELLOW}New GitHub release created: {release_name}.{RESET} -" - ); - - // region: upload asset only for executables, not for libraries - - let release_id = json_value.get("id").unwrap().as_i64().unwrap().to_string(); - println!( - " - {YELLOW}Now uploading release asset. This can take some time if the files are big. Wait...{RESET} - " - ); - // compress files tar.gz - let tar_name = format!("{repo_name}-{tag_name_version}-x86_64-unknown-linux-gnu.tar.gz"); - - cl::ShellCommandLimitedDoubleQuotesSanitizer::new(r#"tar -zcvf "{tar_name_sanitized_for_double_quote}" "target/release/{repo_name_sanitized_for_double_quote}" "#).unwrap_or_else(|e| panic!("{e}")) - .arg("{tar_name_sanitized_for_double_quote}", &tar_name).unwrap_or_else(|e| panic!("{e}")) - .arg("{repo_name_sanitized_for_double_quote}", &repo_name).unwrap_or_else(|e| panic!("{e}")) - .run().unwrap_or_else(|e| panic!("{e}")); - - // upload asset - cgl::github_api_upload_asset_to_release(&github_client, &github_owner, &repo_name, &release_id, &tar_name); - - cl::ShellCommandLimitedDoubleQuotesSanitizer::new(r#"rm "{tar_name_sanitized_for_double_quote}" "#).unwrap_or_else(|e| panic!("{e}")) - .arg("{tar_name_sanitized_for_double_quote}", &tar_name).unwrap_or_else(|e| panic!("{e}")) - .run().unwrap_or_else(|e| panic!("{e}")); - - println!( - r#" - {YELLOW}Asset uploaded. Open and edit the description on GitHub Releases in the browser.{RESET} - "# - ); - - // endregion: upload asset only for executables, not for libraries - - println!( - r#" -{GREEN}https://github.com/{github_owner}/{repo_name}/releases{RESET} - "# - ); -} -// endregion: tasks diff --git a/template_new_cli/automation_tasks_rs/src/secrets_always_local_mod.rs b/template_new_cli/automation_tasks_rs/src/secrets_always_local_mod.rs deleted file mode 100644 index 73a8cca3..00000000 --- a/template_new_cli/automation_tasks_rs/src/secrets_always_local_mod.rs +++ /dev/null @@ -1,763 +0,0 @@ -// secrets_always_local_mod.rs - -/// Secrets like GitHub API secret_token, crates.io secret token, docker hub secret_token, SSH private key passphrase and similar -/// must never go out of this crate. Never pass any secret to an external crate library as much as possible. -/// The user has the source code under his fingers in this crate. So he knows nobody will mess with this code -/// once he inspected and reviewed it. -/// All the modules are in one file to avoid clutter in the automation_tasks_rs folder. -/// The simple program flow of functions that need secrets is butchered to avoid secrets leaving this crate. -/// Now it looks like a mess, but the goal is achieved. The secrets never leave this crate. - -// region: Public API constants -// ANSI colors for Linux terminal -// https://github.com/shiena/ansicolor/blob/master/README.md -/// ANSI color -pub const RED: &str = "\x1b[31m"; -/// ANSI color -pub const GREEN: &str = "\x1b[32m"; -/// ANSI color -pub const YELLOW: &str = "\x1b[33m"; -/// ANSI color -pub const BLUE: &str = "\x1b[34m"; -/// ANSI color -pub const RESET: &str = "\x1b[0m"; -// endregion: Public API constants - -pub use cargo_auto_encrypt_secret_lib::EncryptedString; -pub use secrecy::ExposeSecret; - -pub(crate) mod decrypt_mod { - - use crate::secrets_always_local_mod::*; - - /// The secrets must not leave this crate. - /// They are never going into an external library crate. - /// This crate is "user code" and is easy to review and inspect. - pub(crate) struct Decryptor<'a> { - secret_string: secrecy::SecretString, - secret_passcode_bytes: &'a secrecy::SecretVec, - } - - impl<'a> Decryptor<'a> { - pub(crate) fn new_for_decrypt(secret_passcode_bytes: &'a secrecy::SecretVec) -> Self { - Decryptor { - secret_string: secrecy::SecretString::new("".to_string()), - secret_passcode_bytes, - } - } - pub(crate) fn return_secret_string(&self) -> &secrecy::SecretString { - &self.secret_string - } - - /// Decrypts encrypted_string with secret_passcode_bytes - /// - /// secret_passcode_bytes must be 32 bytes or more - /// Returns the secret_string - pub(crate) fn decrypt_symmetric(&mut self, encrypted_string: &cargo_auto_encrypt_secret_lib::EncryptedString) { - let encrypted_bytes = ::decode_vec(&encrypted_string.0).unwrap(); - //only first 32 bytes - let mut secret_passcode_32bytes = [0u8; 32]; - secret_passcode_32bytes.copy_from_slice(&self.secret_passcode_bytes.expose_secret()[0..32]); - - let cipher = ::new(&secret_passcode_32bytes.into()); - // nonce is salt - let nonce = rsa::sha2::digest::generic_array::GenericArray::from_slice(&encrypted_bytes[..12]); - let cipher_text = &encrypted_bytes[12..]; - - let Ok(decrypted_bytes) = aes_gcm::aead::Aead::decrypt(&cipher, nonce, cipher_text) else { - panic!("{RED}Error: Decryption failed. {RESET}"); - }; - let decrypted_string = String::from_utf8(decrypted_bytes).unwrap(); - self.secret_string = secrecy::SecretString::new(decrypted_string) - } - } -} - -pub(crate) mod encrypt_mod { - use crate::secrets_always_local_mod::*; - - /// The secrets must not leave this crate. - /// They are never going into an external library crate. - /// This crate is "user code" and is easy to review and inspect. - pub(crate) struct Encryptor<'a> { - secret_string: secrecy::SecretString, - secret_passcode_bytes: &'a secrecy::SecretVec, - } - - impl<'a> Encryptor<'a> { - pub(crate) fn new_for_encrypt(secret_string: secrecy::SecretString, secret_passcode_bytes: &'a secrecy::SecretVec) -> Self { - Encryptor { secret_string, secret_passcode_bytes } - } - - /// Encrypts secret_string with secret_passcode_bytes - /// - /// secret_passcode_bytes must be 32 bytes or more - /// returns the encrypted_string - pub(crate) fn encrypt_symmetric(&self) -> Option { - //only first 32 bytes - let mut secret_passcode_32bytes = [0u8; 32]; - secret_passcode_32bytes.copy_from_slice(&self.secret_passcode_bytes.expose_secret()[0..32]); - - let cipher = ::new(&secret_passcode_32bytes.into()); - // nonce is salt - let nonce = ::generate_nonce(&mut aes_gcm::aead::OsRng); - - let Ok(cipher_text) = aes_gcm::aead::Aead::encrypt(&cipher, &nonce, self.secret_string.expose_secret().as_bytes()) else { - panic!("{RED}Error: Encryption failed. {RESET}"); - }; - - let mut encrypted_bytes = nonce.to_vec(); - encrypted_bytes.extend_from_slice(&cipher_text); - let encrypted_string = ::encode_string(&encrypted_bytes); - Some(cargo_auto_encrypt_secret_lib::EncryptedString(encrypted_string)) - } - } -} - -pub(crate) mod secrecy_mod { - - //! The crate secrecy is probably great. - //! But I want to encrypt the content, so I will make a wrapper. - //! The secrets must always be moved to secrecy types as soon as possible. - - use crate::secrets_always_local_mod::*; - - pub struct SecretEncryptedString { - encrypted_string: EncryptedString, - } - - impl SecretEncryptedString { - pub fn new_with_secret_string(secret_string: secrecy::SecretString, session_passcode: &secrecy::SecretVec) -> Self { - let encryptor = super::encrypt_mod::Encryptor::new_for_encrypt(secret_string, &session_passcode); - let encrypted_string = encryptor.encrypt_symmetric().unwrap(); - - SecretEncryptedString { encrypted_string } - } - - pub fn new_with_string(secret_string: String, session_passcode: &secrecy::SecretVec) -> Self { - let secret_string = secrecy::SecretString::new(secret_string); - Self::new_with_secret_string(secret_string, session_passcode) - } - - pub fn expose_decrypted_secret(&self, session_passcode: &secrecy::SecretVec) -> secrecy::SecretString { - let mut decryptor = super::decrypt_mod::Decryptor::new_for_decrypt(&session_passcode); - decryptor.decrypt_symmetric(&self.encrypted_string); - decryptor.return_secret_string().clone() - } - } -} - -pub(crate) mod ssh_mod { - - use crate::secrets_always_local_mod::*; - - pub struct SshContext { - signed_passcode_is_a_secret: secrecy::SecretVec, - decrypted_string: secrecy::SecretString, - } - - impl SshContext { - pub fn new() -> Self { - SshContext { - signed_passcode_is_a_secret: secrecy::SecretVec::new(vec![]), - decrypted_string: secrecy::SecretString::new("".to_string()), - } - } - pub fn get_decrypted_string(&self) -> secrecy::SecretString { - self.decrypted_string.clone() - } - } - - impl cargo_auto_encrypt_secret_lib::SshContextTrait for SshContext { - /// decrypt from file data and write the decrypted secret in private field for later use in this crate, not in external library crates - fn decrypt_from_file_data(&mut self, encrypted_string: &cargo_auto_encrypt_secret_lib::EncryptedString) { - let mut decryptor = decrypt_mod::Decryptor::new_for_decrypt(&self.signed_passcode_is_a_secret); - decryptor.decrypt_symmetric(encrypted_string); - self.decrypted_string = decryptor.return_secret_string().clone(); - } - - /// get secret_token and encrypt - fn get_secret_token_and_encrypt(&self) -> cargo_auto_encrypt_secret_lib::EncryptedString { - /// Internal function used only for test configuration - /// - /// It is not interactive, but reads from a env var. - #[cfg(test)] - fn get_secret_token() -> secrecy::SecretString { - secrecy::SecretString::new(std::env::var("TEST_TOKEN").unwrap()) - } - /// Internal function get_passphrase interactively ask user to type the passphrase - /// - /// This is used for normal code execution. - #[cfg(not(test))] - fn get_secret_token() -> secrecy::SecretString { - eprintln!(" "); - eprintln!(" {BLUE}Enter the secret_token to encrypt:{RESET}"); - secrecy::SecretString::new( - inquire::Password::new("") - .without_confirmation() - .with_display_mode(inquire::PasswordDisplayMode::Masked) - .prompt() - .unwrap(), - ) - } - let secret_token = get_secret_token(); - // use this signed as password for symmetric encryption - let encryptor = encrypt_mod::Encryptor::new_for_encrypt(secret_token, &self.signed_passcode_is_a_secret); - - let encrypted_token = encryptor.encrypt_symmetric().unwrap(); - // return - encrypted_token - } - - /// Sign with ssh-agent or with identity_file - /// - /// get passphrase interactively - /// returns secret_password_bytes:Vec u8 - fn sign_with_ssh_agent_or_identity_file(&mut self, identity_private_file_path: &camino::Utf8Path, seed_bytes_not_a_secret: &[u8; 32]) { - /// Internal function used only for test configuration - /// - /// It is not interactive, but reads from a env var. - #[cfg(test)] - fn get_passphrase() -> secrecy::SecretString { - secrecy::SecretString::new(std::env::var("TEST_PASSPHRASE").unwrap()) - } - /// Internal function get_passphrase interactively ask user to type the passphrase - /// - /// This is used for normal code execution. - #[cfg(not(test))] - fn get_passphrase() -> secrecy::SecretString { - eprintln!(" "); - eprintln!(" {BLUE}Enter the passphrase for the SSH private key:{RESET}"); - secrecy::SecretString::new( - inquire::Password::new("") - .without_confirmation() - .with_display_mode(inquire::PasswordDisplayMode::Masked) - .prompt() - .unwrap(), - ) - } - - let identity_private_file_path_expanded = expand_path_check_private_key_exists(identity_private_file_path); - - let fingerprint_from_file = cargo_auto_encrypt_secret_lib::get_fingerprint_from_file(&identity_private_file_path_expanded); - - let mut ssh_agent_client = cargo_auto_encrypt_secret_lib::crate_ssh_agent_client(); - match cargo_auto_encrypt_secret_lib::ssh_add_list_contains_fingerprint(&mut ssh_agent_client, &fingerprint_from_file) { - Some(public_key) => { - // sign with public key from ssh-agent - let signature_is_the_new_secret_password = ssh_agent_client.sign(&public_key, seed_bytes_not_a_secret).unwrap(); - // only the data part of the signature goes into as_bytes. - self.signed_passcode_is_a_secret = secrecy::SecretVec::new(signature_is_the_new_secret_password.as_bytes().to_owned()); - } - None => { - // ask user to think about adding with ssh-add - eprintln!(" {YELLOW}SSH key for encrypted secret_token is not found in the ssh-agent.{RESET}"); - eprintln!(" {YELLOW}Without ssh-agent, you will have to type the private key passphrase every time. This is more secure, but inconvenient.{RESET}"); - eprintln!(" {YELLOW}You can manually add the SSH identity to ssh-agent for 1 hour:{RESET}"); - eprintln!(" {YELLOW}WARNING: using ssh-agent is less secure, because there is no need for user interaction.{RESET}"); - eprintln!("{GREEN}ssh-add -t 1h {identity_private_file_path_expanded}{RESET}"); - - // just for test purpose I will use env var to read this passphrase. Don't use it in production. - - let passphrase_is_a_secret = get_passphrase(); - let private_key = ssh_key::PrivateKey::read_openssh_file(identity_private_file_path_expanded.as_std_path()).unwrap(); - let mut private_key = private_key.decrypt(passphrase_is_a_secret.expose_secret()).unwrap(); - - // FYI: this type of signature is compatible with ssh-agent because it does not involve namespace - let signature_is_the_new_secret_password = rsa::signature::SignerMut::try_sign(&mut private_key, seed_bytes_not_a_secret).unwrap(); - - // only the data part of the signature goes into as_bytes. - self.signed_passcode_is_a_secret = secrecy::SecretVec::new(signature_is_the_new_secret_password.as_bytes().to_owned()); - } - } - } - } - - /// Expand path and check if identity file exists - /// - /// Inform the user how to generate identity file. - pub fn expand_path_check_private_key_exists(identity_private_file_path: &camino::Utf8Path) -> camino::Utf8PathBuf { - let identity_private_file_path_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(identity_private_file_path); - if !camino::Utf8Path::new(&identity_private_file_path_expanded).exists() { - eprintln!("{RED}Identity file {identity_private_file_path_expanded} that contains the SSH private key does not exist! {RESET}"); - eprintln!(" {YELLOW}Create the SSH key manually in bash with this command:{RESET}"); - if identity_private_file_path_expanded.as_str().contains("github_api") { - eprintln!(r#"{GREEN}ssh-keygen -t ed25519 -f "{identity_private_file_path_expanded}" -C "github api secret_token"{RESET}"#); - } else if identity_private_file_path_expanded.as_str().contains("crates_io") { - eprintln!(r#"{GREEN}ssh-keygen -t ed25519 -f "{identity_private_file_path_expanded}" -C "crates io secret_token"{RESET}"#); - } else if identity_private_file_path_expanded.as_str().contains("docker_hub") { - eprintln!(r#"{GREEN}ssh-keygen -t ed25519 -f "{identity_private_file_path_expanded}" -C "docker hub secret_token"{RESET}"#); - } - eprintln!(" "); - panic!("{RED}Error: File {identity_private_file_path_expanded} does not exist! {RESET}"); - } - identity_private_file_path_expanded - } -} - -pub(crate) mod github_mod { - - //! Every API call needs the GitHub API secret_token. This is a secret important just like a password. - //! I don't want to pass this secret to an "obscure" library crate that is difficult to review. - //! This secret will stay here in this codebase that every developer can easily inspect. - //! Instead of the secret_token, I will pass the struct GitHubClient with the trait SendToGitHubApi. - //! This way, the secret_token will be encapsulated. - - use crate::secrets_always_local_mod::*; - use cargo_auto_github_lib as cgl; - use reqwest::Client; - - /// Struct GitHubClient contains only private fields - /// This fields are accessible only to methods in implementation of traits. - pub struct GitHubClient { - /// Passcode for encrypt the secret_token to encrypted_token in memory. - /// So that the secret is in memory as little as possible as plain text. - /// For every session (program start) a new random passcode is created. - session_passcode: secrecy::SecretVec, - - /// private field is set only once in the new() constructor - encrypted_token: super::secrecy_mod::SecretEncryptedString, - } - - impl GitHubClient { - /// Create new GitHub client - /// - /// Interactively ask the user to input the GitHub secret_token. - pub fn new_interactive_input_secret_token() -> Self { - let mut github_client = Self::new_wo_secret_token(); - - println!("{BLUE}Enter the GitHub API secret_token:{RESET}"); - github_client.encrypted_token = - super::secrecy_mod::SecretEncryptedString::new_with_string(inquire::Password::new("").without_confirmation().prompt().unwrap(), &github_client.session_passcode); - - // return - github_client - } - - /// Create new GitHub client without secret_token - fn new_wo_secret_token() -> Self { - /// Internal function Generate a random password - fn random_byte_passcode() -> [u8; 32] { - let mut password = [0_u8; 32]; - use aes_gcm::aead::rand_core::RngCore; - aes_gcm::aead::OsRng.fill_bytes(&mut password); - password - } - - let session_passcode = secrecy::SecretVec::new(random_byte_passcode().to_vec()); - let encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_string("".to_string(), &session_passcode); - - GitHubClient { session_passcode, encrypted_token } - } - - /// Use the stored API secret_token - /// - /// If the secret_token not exists ask user to interactively input the secret_token. - /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the secret_token. - /// it is then possible also to have the ssh key in ssh-agent and write the passphrase only once. - /// But this great user experience comes with security concerns. The secret_token is accessible if the attacker is very dedicated. - pub fn new_with_stored_secret_token() -> Self { - /// Internal function for DRY Don't Repeat Yourself - fn read_secret_token_and_decrypt_return_github_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> GitHubClient { - // read the secret_token and decrypt - cargo_auto_encrypt_secret_lib::decrypt_with_ssh_interactive_from_file(&mut ssh_context, encrypted_string_file_path); - let secret_token = ssh_context.get_decrypted_string(); - let mut github_client = GitHubClient::new_wo_secret_token(); - github_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(secret_token, &github_client.session_passcode); - github_client - } - - let encrypted_string_file_path = camino::Utf8Path::new("~/.ssh/github_api_secret_token_encrypted.txt"); - let encrypted_string_file_path_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(encrypted_string_file_path); - - let identity_private_file_path = camino::Utf8Path::new("~/.ssh/github_api_secret_token_ssh_1"); - let _identity_private_file_path_expanded = crate::secrets_always_local_mod::ssh_mod::expand_path_check_private_key_exists(identity_private_file_path); - - if !encrypted_string_file_path_expanded.exists() { - // ask interactive - println!(" {BLUE}Do you want to store the GitHub API secret_token encrypted with an SSH key? (y/n){RESET}"); - let answer = inquire::Text::new("").prompt().unwrap(); - if answer.to_lowercase() != "y" { - // enter the secret_token manually, not storing - return Self::new_interactive_input_secret_token(); - } else { - // get the passphrase and secret_token interactively - let mut ssh_context = super::ssh_mod::SshContext::new(); - // encrypt and save the encrypted secret_token - cargo_auto_encrypt_secret_lib::encrypt_with_ssh_interactive_save_file(&mut ssh_context, identity_private_file_path, encrypted_string_file_path); - // read the secret_token and decrypt, return GitHubClient - read_secret_token_and_decrypt_return_github_client(ssh_context, encrypted_string_file_path) - } - } else { - // file exists - let ssh_context = super::ssh_mod::SshContext::new(); - // read the secret_token and decrypt, return GitHubClient - read_secret_token_and_decrypt_return_github_client(ssh_context, encrypted_string_file_path) - } - } - - /// decrypts the secret_token in memory - #[allow(dead_code)] - pub fn decrypt_secret_token_in_memory(&self) -> secrecy::SecretString { - self.encrypted_token.expose_decrypted_secret(&self.session_passcode) - } - } - - /// trait from the crate library, so the 2 crates can share a function - impl cgl::SendToGitHubApi for GitHubClient { - /// Send GitHub API request - /// - /// This function encapsulates the secret API secret_token. - /// The RequestBuilder is created somewhere in the library crate. - /// The client can be passed to the library. It will not reveal the secret_token. - fn send_to_github_api(&self, req: reqwest::blocking::RequestBuilder) -> serde_json::Value { - // I must build the request to be able then to inspect it. - let req = req.bearer_auth(self.decrypt_secret_token_in_memory().expose_secret()).build().unwrap(); - - // region: Assert the correct url and https - // It is important that the request coming from a external crate/library - // is only sent always and only to GitHub API and not some other malicious url, - // because the request contains the secret GitHub API secret_token. - // And it must always use https - let host_str = req.url().host_str().unwrap(); - assert!(host_str == "api.github.com", "{RED}Error: Url is not correct: {host_str}. It must be always api.github.com.{RESET}"); - let scheme = req.url().scheme(); - assert!(scheme == "https", "{RED}Error: Scheme is not correct: {scheme}. It must be always https.{RESET}"); - // endregion: Assert the correct url and https - - let reqwest_client = reqwest::blocking::Client::new(); - let response_text = reqwest_client.execute(req).unwrap().text().unwrap(); - - let json_value: serde_json::Value = serde_json::from_str(&response_text).unwrap(); - - // panic if "message": String("Bad credentials"), - if let Some(m) = json_value.get("message") { - if m == "Bad credentials" { - panic!("{RED}Error: Bad credentials for GitHub API. {RESET}"); - } - } - - // return - json_value - } - - /// Upload to GitHub - /// - /// This function encapsulates the secret API secret_token. - /// The RequestBuilder is created somewhere in the library crate. - /// The client can be passed to the library. It will not reveal the secret_token. - /// This is basically an async fn, but use of `async fn` in public traits is discouraged... - async fn upload_to_github(&self, req: reqwest::RequestBuilder) -> serde_json::Value { - // I must build the request to be able then to inspect it. - let req = req.bearer_auth(self.decrypt_secret_token_in_memory().expose_secret()).build().unwrap(); - - // region: Assert the correct url and https - // It is important that the request coming from a external crate/library - // is only sent always and only to GitHub uploads and not some other malicious url, - // because the request contains the secret GitHub API secret_token. - // And it must always use https - let host_str = req.url().host_str().unwrap(); - assert!(host_str == "uploads.github.com", "{RED}Error: Url is not correct: {host_str}. It must be always api.github.com.{RESET}"); - let scheme = req.url().scheme(); - assert!(scheme == "https", "{RED}Error: Scheme is not correct: {scheme}. It must be always https.{RESET}"); - // endregion: Assert the correct url and https - - let reqwest_client = Client::new(); - let response_text = reqwest_client.execute(req).await.unwrap().text().await.unwrap(); - - let json_value: serde_json::Value = serde_json::from_str(&response_text).unwrap(); - - // panic if "message": String("Bad credentials"), - if let Some(m) = json_value.get("message") { - if m == "Bad credentials" { - panic!("{RED}Error: Bad credentials for GitHub API. {RESET}"); - } - } - - // return - json_value - } - } -} - -pub(crate) mod crates_io_mod { - - //! Publish to crates.io needs the crates.io secret_token. This is a secret important just like a password. - //! I don't want to pass this secret to an "obscure" library crate that is difficult to review. - //! This secret will stay here in this codebase that every developer can easily inspect. - //! Instead of the secret_token, I will pass the struct CratesIoClient with the trait SendToCratesIo. - //! This way, the secret_token will be encapsulated. - - use crate::secrets_always_local_mod::*; - use cargo_auto_lib::ShellCommandLimitedDoubleQuotesSanitizer; - use cargo_auto_lib::ShellCommandLimitedDoubleQuotesSanitizerTrait; - - /// Struct CratesIoClient contains only private fields - /// This fields are accessible only to methods in implementation of traits. - pub struct CratesIoClient { - /// Passcode for encrypt the secret_token to encrypted_token in memory. - /// So that the secret is in memory as little as possible as plain text. - /// For every session (program start) a new random passcode is created. - session_passcode: secrecy::SecretVec, - - /// private field is set only once in the new() constructor - encrypted_token: super::secrecy_mod::SecretEncryptedString, - } - - impl CratesIoClient { - /// Create new CratesIo client - /// - /// Interactively ask the user to input the crates.io secret_token. - #[allow(dead_code)] - pub fn new_interactive_input_secret_token() -> Self { - let mut crates_io_client = Self::new_wo_secret_token(); - - println!("{BLUE}Enter the crates.io secret_token:{RESET}"); - crates_io_client.encrypted_token = - super::secrecy_mod::SecretEncryptedString::new_with_string(inquire::Password::new("").without_confirmation().prompt().unwrap(), &crates_io_client.session_passcode); - - // return - crates_io_client - } - - /// Create new CratesIo client without secret_token - #[allow(dead_code)] - fn new_wo_secret_token() -> Self { - /// Internal function Generate a random password - fn random_byte_passcode() -> [u8; 32] { - let mut password = [0_u8; 32]; - use aes_gcm::aead::rand_core::RngCore; - aes_gcm::aead::OsRng.fill_bytes(&mut password); - password - } - - let session_passcode = secrecy::SecretVec::new(random_byte_passcode().to_vec()); - let encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_string("".to_string(), &session_passcode); - - CratesIoClient { session_passcode, encrypted_token } - } - - /// Use the stored crates.io secret_token - /// - /// If the secret_token not exists ask user to interactively input the secret_token. - /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the secret_token. - /// It is then possible also to have the ssh key in ssh-agent and write the passphrase only once. - /// But this great user experience comes with security concerns. The secret_token is accessible if the attacker is very dedicated. - #[allow(dead_code)] - pub fn new_with_stored_secret_token() -> Self { - /// Internal function for DRY Don't Repeat Yourself - fn read_secret_token_and_decrypt_return_crates_io_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> CratesIoClient { - cargo_auto_encrypt_secret_lib::decrypt_with_ssh_interactive_from_file(&mut ssh_context, encrypted_string_file_path); - let secret_token = ssh_context.get_decrypted_string(); - let mut crates_io_client = CratesIoClient::new_wo_secret_token(); - crates_io_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(secret_token, &crates_io_client.session_passcode); - crates_io_client - } - - // check if the plain-text file from `podman login` exists and warn the user because it is a security vulnerability. - let file_auth = "~/.cargo/credentials.toml"; - let file_auth = camino::Utf8Path::new(file_auth); - // TODO: check for env variable also? - let file_auth_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(file_auth); - let file_auth_expanded = camino::Utf8Path::new(&file_auth_expanded); - if file_auth_expanded.exists() { - eprintln!("{RED}Security vulnerability: Found the cargo file with plain-text secret_token: {file_auth_expanded}. It would be better to inspect and remove it. {RESET}") - } - - let encrypted_string_file_path = camino::Utf8Path::new("~/.ssh/crates_io_secret_token_encrypted.txt"); - let encrypted_string_file_path_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(encrypted_string_file_path); - - let identity_private_file_path = camino::Utf8Path::new("~/.ssh/crates_io_secret_token_ssh_1"); - let _identity_private_file_path_expanded = crate::secrets_always_local_mod::ssh_mod::expand_path_check_private_key_exists(identity_private_file_path); - - if !encrypted_string_file_path_expanded.exists() { - // ask interactive - println!(" {BLUE}Do you want to store the crates.io secret_token encrypted with an SSH key? (y/n){RESET}"); - let answer = inquire::Text::new("").prompt().unwrap(); - if answer.to_lowercase() != "y" { - // enter the secret_token manually, not storing - return Self::new_interactive_input_secret_token(); - } else { - // get the passphrase and secret_token interactively - let mut ssh_context = super::ssh_mod::SshContext::new(); - // encrypt and save the encrypted secret_token - cargo_auto_encrypt_secret_lib::encrypt_with_ssh_interactive_save_file(&mut ssh_context, identity_private_file_path, encrypted_string_file_path); - // read the secret_token and decrypt, return CratesIoClient - read_secret_token_and_decrypt_return_crates_io_client(ssh_context, encrypted_string_file_path) - } - } else { - // file exists - let ssh_context = super::ssh_mod::SshContext::new(); - // read the secret_token and decrypt, return CratesIoClient - read_secret_token_and_decrypt_return_crates_io_client(ssh_context, encrypted_string_file_path) - } - } - - /// decrypts the secret_token in memory - #[allow(dead_code)] - pub fn decrypt_secret_token_in_memory(&self) -> secrecy::SecretString { - self.encrypted_token.expose_decrypted_secret(&self.session_passcode) - } - - /// Publish to crates.io - /// - /// This function encapsulates the secret crates.io secret_token. - /// The client can be passed to the library. It will not reveal the secret_token. - #[allow(dead_code)] - pub fn publish_to_crates_io(&self) { - // the secret_token is redacted when print on screen - ShellCommandLimitedDoubleQuotesSanitizer::new(r#"cargo publish --token "{secret_token}" "#) - .unwrap_or_else(|e| panic!("{e}")) - .arg_secret("{secret_token}", &self.decrypt_secret_token_in_memory()) - .unwrap_or_else(|e| panic!("{e}")) - .run() - .unwrap_or_else(|e| panic!("{e}")); - } - } -} - -pub(crate) mod docker_hub_mod { - - //! Push to docker-hub needs the docker hub secret_token. This is a secret important just like a password. - //! I don't want to pass this secret to an "obscure" library crate that is difficult to review. - //! This secret will stay here in this codebase that every developer can easily inspect. - //! Instead of the secret_token, I will pass the struct DockerHubClient with the trait SendToDockerHub. - //! This way, the secret_token will be encapsulated. - - use crate::secrets_always_local_mod::*; - use cargo_auto_lib::ShellCommandLimitedDoubleQuotesSanitizer; - use cargo_auto_lib::ShellCommandLimitedDoubleQuotesSanitizerTrait; - - /// Struct DockerHubClient contains only private fields - /// This fields are accessible only to methods in implementation of traits. - pub struct DockerHubClient { - /// Passcode for encrypt the secret_token to encrypted_token in memory. - /// So that the secret is in memory as little as possible as plain text. - /// For every session (program start) a new random passcode is created. - session_passcode: secrecy::SecretVec, - - /// private field is set only once in the new() constructor - encrypted_token: super::secrecy_mod::SecretEncryptedString, - } - - impl DockerHubClient { - /// Create new DockerHub client - /// - /// Interactively ask the user to input the docker hub secret_token. - #[allow(dead_code)] - pub fn new_interactive_input_secret_token() -> Self { - let mut docker_hub_client = Self::new_wo_secret_token(); - - println!("{BLUE}Enter the docker hub secret_token:{RESET}"); - docker_hub_client.encrypted_token = - super::secrecy_mod::SecretEncryptedString::new_with_string(inquire::Password::new("").without_confirmation().prompt().unwrap(), &docker_hub_client.session_passcode); - - // return - docker_hub_client - } - - /// Create new DockerHub client without secret_token - #[allow(dead_code)] - fn new_wo_secret_token() -> Self { - /// Internal function Generate a random password - fn random_byte_passcode() -> [u8; 32] { - let mut password = [0_u8; 32]; - use aes_gcm::aead::rand_core::RngCore; - aes_gcm::aead::OsRng.fill_bytes(&mut password); - password - } - - let session_passcode = secrecy::SecretVec::new(random_byte_passcode().to_vec()); - let encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_string("".to_string(), &session_passcode); - - DockerHubClient { session_passcode, encrypted_token } - } - - /// Use the stored docker hub secret_token - /// - /// If the secret_token not exists ask user to interactively input the secret_token. - /// To decrypt it, use the SSH passphrase. That is much easier to type than typing the secret_token. - /// It is then possible also to have the ssh key in ssh-agent and write the passphrase only once. - /// But this great user experience comes with security concerns. The secret_token is accessible if the attacker is very dedicated. - #[allow(dead_code)] - pub fn new_with_stored_secret_token(user_name: &str, registry: &str) -> Self { - /// Internal function for DRY Don't Repeat Yourself - fn read_secret_token_and_decrypt_return_docker_hub_client(mut ssh_context: super::ssh_mod::SshContext, encrypted_string_file_path: &camino::Utf8Path) -> DockerHubClient { - cargo_auto_encrypt_secret_lib::decrypt_with_ssh_interactive_from_file(&mut ssh_context, encrypted_string_file_path); - let secret_token = ssh_context.get_decrypted_string(); - let mut docker_hub_client = DockerHubClient::new_wo_secret_token(); - docker_hub_client.encrypted_token = super::secrecy_mod::SecretEncryptedString::new_with_secret_string(secret_token, &docker_hub_client.session_passcode); - docker_hub_client - } - - // check if the plain-text file from `podman login` exists and warn the user because it is a security vulnerability. - let file_auth = "${XDG_RUNTIME_DIR}/containers/auth.json"; - // TODO: check for env variable also? - if let Some(xdg_runtime_dir) = std::env::var_os("XDG_RUNTIME_DIR"){ - let xdg_runtime_dir=xdg_runtime_dir.to_string_lossy().to_string(); - let file_auth_expanded = file_auth.replace("${XDG_RUNTIME_DIR}", &xdg_runtime_dir); - let file_auth_expanded = camino::Utf8Path::new(&file_auth_expanded); - if file_auth_expanded.exists() { - eprintln!("{RED}Security vulnerability: Found the docker hub file with plain-text secret_token: {file_auth_expanded}. It would be better to inspect and remove it. {RESET}") - } - } - - // registry: docker.io -> replace dot into "--"" - // username: bestiadev - let registry_escaped = registry.replace(".", "--"); - let encrypted_string_file_path = format!("~/.ssh/docker_hub_{registry_escaped}_{user_name}.txt"); - let encrypted_string_file_path = camino::Utf8Path::new(&encrypted_string_file_path); - let encrypted_string_file_path_expanded = cargo_auto_encrypt_secret_lib::file_path_home_expand(encrypted_string_file_path); - - let identity_private_file_path = camino::Utf8Path::new("~/.ssh/docker_hub_secret_token_ssh_1"); - let _identity_private_file_path_expanded = crate::secrets_always_local_mod::ssh_mod::expand_path_check_private_key_exists(identity_private_file_path); - - if !encrypted_string_file_path_expanded.exists() { - // ask interactive - println!(" {BLUE}Do you want to store the docker hub secret_token encrypted with an SSH key? (y/n){RESET}"); - let answer = inquire::Text::new("").prompt().unwrap(); - if answer.to_lowercase() != "y" { - // enter the secret_token manually, not storing - return Self::new_interactive_input_secret_token(); - } else { - // get the passphrase and secret_token interactively - let mut ssh_context = super::ssh_mod::SshContext::new(); - // encrypt and save the encrypted secret_token - cargo_auto_encrypt_secret_lib::encrypt_with_ssh_interactive_save_file(&mut ssh_context, identity_private_file_path, encrypted_string_file_path); - // read the secret_token and decrypt, return DockerHubClient - read_secret_token_and_decrypt_return_docker_hub_client(ssh_context, encrypted_string_file_path) - } - } else { - // file exists - let ssh_context = super::ssh_mod::SshContext::new(); - // read the secret_token and decrypt, return DockerHubClient - read_secret_token_and_decrypt_return_docker_hub_client(ssh_context, encrypted_string_file_path) - } - } - - /// decrypts the secret_token in memory - #[allow(dead_code)] - pub fn decrypt_secret_token_in_memory(&self) -> secrecy::SecretString { - self.encrypted_token.expose_decrypted_secret(&self.session_passcode) - } - - /// Push to docker hub - /// - /// This function encapsulates the secret docker hub secret_token. - /// The client can be passed to the library. It will not reveal the secret_token. - #[allow(dead_code)] - pub fn push_to_docker_hub(&self, image_url: &str, user_name: &str) { - // the secret_token can be used in place of the password in --cred - ShellCommandLimitedDoubleQuotesSanitizer::new(r#"podman push --creds "{user_name}:{secret_token}" "{image_url}" "#) - .unwrap_or_else(|e| panic!("{e}")) - .arg("{user_name}", user_name) - .unwrap_or_else(|e| panic!("{e}")) - .arg_secret("{secret_token}", &self.decrypt_secret_token_in_memory()) - .unwrap_or_else(|e| panic!("{e}")) - .arg("{image_url}", image_url) - .unwrap_or_else(|e| panic!("{e}")) - .run() - .unwrap_or_else(|e| panic!("{e}")); - } - } -} diff --git a/template_new_cli/examples/example_1.rs b/template_new_cli/examples/example_1.rs deleted file mode 100644 index c6f09f63..00000000 --- a/template_new_cli/examples/example_1.rs +++ /dev/null @@ -1,19 +0,0 @@ -// examples/example_1.rs - -//! A simple example how to use the `lib.rs` -//! You can run it with `cargo run --example example_1` - -use cargo_auto_template_new_cli_lib::*; - -/// example how to use format_hello_phrase() and format_upper_hello_phrase() -fn main() { - let greet_name = "world"; - let phrase = format_hello_phrase(greet_name); - println!("{}", phrase); - - // possible error must be processed - match format_upper_hello_phrase(greet_name) { - Ok(phrase) => println!("{}", phrase), - Err(err) => log::error!("Error: {}", err), - } -} diff --git a/template_new_cli/rustfmt.toml b/template_new_cli/rustfmt.toml deleted file mode 100644 index ffc467de..00000000 --- a/template_new_cli/rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -max_width = 200 diff --git a/template_new_cli/src/bin/cargo_auto_template_new_cli/main.rs b/template_new_cli/src/bin/cargo_auto_template_new_cli/main.rs deleted file mode 100644 index d046f0b9..00000000 --- a/template_new_cli/src/bin/cargo_auto_template_new_cli/main.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! src/bin/cargo_auto_template_new_cli/main.rs - -// This `main.rs` is the code for the CLI application. -// The build of this project will create the CLI application. -// The `main.rs` has all the stdin and stdout. -// The `lib.rs` must be in/out agnostic. That is the responsibility of the `main.rs` -// This `lib.rs` can be used as dependency crate for other projects. - -// The `main.rs` uses the `anyhow` error library. -// The `lib.rs` uses the `thiserror` library. - -// Linux terminal colors -use cargo_auto_template_new_cli_lib::{GREEN, RED, RESET, YELLOW}; - -/// entry point into the bin-executable -fn main() { - // logging is essential for every project - pretty_env_logger::init(); - - // super simple argument parsing. There are crates that can parse more complex arguments. - match std::env::args().nth(1).as_deref() { - None | Some("--help") | Some("-h") => print_help(), - Some("print") => match std::env::args().nth(2).as_deref() { - // second argument - Some(greet_name) => { - print_greet_name(greet_name); - } - None => println!("{RED}Error: Missing arguments `greet_name`.{RESET}"), - }, - Some("upper") => match std::env::args().nth(2).as_deref() { - // second argument - Some(greet_name) => { - // this can return an error. Here is the last place I can deal with the error. - match upper_greet_name(greet_name) { - // do nothing - Ok(()) => (), - // log error from anyhow - Err(err) => println!("{RED}Error: {err}{RESET}"), - } - } - None => println!("{RED}Error: Missing arguments `greet_name`.{RESET}"), - }, - _ => println!("{RED}Error: Unrecognized arguments. Try `cargo_auto_template_new_cli --help`{RESET}"), - } -} - -/// print help -fn print_help() { - println!( - r#" - {YELLOW}Welcome to cargo_auto_template_new_cli ! - This is a simple yet complete template for a CLI program written in Rust.{RESET} - -{GREEN}cargo_auto_template_new_cli --help{RESET} -{GREEN}cargo_auto_template_new_cli print world{RESET} -{GREEN}cargo_auto_template_new_cli upper world{RESET} - - {YELLOW}This command should return an error:{RESET} -{GREEN}cargo_auto_template_new_cli upper WORLD{RESET} - - {YELLOW}© 2024 bestia.dev MIT License github.com/automation-tasks-rs/cargo-auto{RESET} -"# - ); -} - -/// print my name -fn print_greet_name(greet_name: &str) { - // call the function from the `lib.rs` - println!("{}", cargo_auto_template_new_cli_lib::format_hello_phrase(greet_name)); -} - -/// print my name upper, can return error -fn upper_greet_name(greet_name: &str) -> anyhow::Result<()> { - // the function from `lib.rs`, can return error - // use the ? syntax to bubble the error up one level or continue (early return) - let upper = cargo_auto_template_new_cli_lib::format_upper_hello_phrase(greet_name)?; - println!("{}", upper); - // return - Ok(()) -} diff --git a/template_new_cli/src/hello_mod.rs b/template_new_cli/src/hello_mod.rs deleted file mode 100644 index 151787a1..00000000 --- a/template_new_cli/src/hello_mod.rs +++ /dev/null @@ -1,50 +0,0 @@ -// cargo_auto_template_new_cli/src/hello_mod.rs - -//! All the real code is inside modules in separate files. -//! -//! This doc-comments will be compiled into the `docs`. - -/// format the hello phrase -pub fn format_hello_phrase(greet_name: &str) -> String { - log::info!("start format_hello_phrase()"); - // return - format!("Hello {}!", greet_name) -} - -/// format the hello phrase with uppercase name -/// if it is already uppercase, return error with thiserror -pub fn format_upper_hello_phrase(greet_name: &str) -> Result { - log::info!("start format_upper_hello_phrase()"); - // shadowing the same variable name: - let upper_greet_name = make_uppercase(greet_name); - if upper_greet_name == greet_name { - return Err(crate::LibraryError::Uppercase(greet_name.to_string())); - } - - // return - Ok(format!("Hello {}!", &upper_greet_name)) -} - -/// return uppercase -pub fn make_uppercase(greet_name: &str) -> String { - // return - greet_name.to_uppercase() -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - pub fn test_format_upper_hello_phrase() { - assert_eq!(format_upper_hello_phrase("abcd").expect("error"), "Hello ABCD!"); - assert!(format_upper_hello_phrase("ABCD").is_err()); - } - - #[test] - pub fn test_make_uppercase() { - assert_eq!(make_uppercase("abcd"), "ABCD"); - assert_eq!(make_uppercase("1234abcd"), "1234ABCD"); - assert_eq!(make_uppercase("čšž"), "ČŠŽ"); - } -} diff --git a/template_new_cli/src/lib.rs b/template_new_cli/src/lib.rs deleted file mode 100644 index b9d9be54..00000000 --- a/template_new_cli/src/lib.rs +++ /dev/null @@ -1,42 +0,0 @@ -// cargo_auto_template_new_cli/src/lib.rs - -// The `main.rs` has all the stdin and stdout. -// The `lib.rs` must be in/out agnostic. That is the responsibility of the `main.rs` -// The `lib.rs` does not have any real code. All the code is in modules in separate files. -// The `lib.rs` has just the list of modules, it publishes module's functions or class for the caller -// and it has some global stuff like the Error enum. - -// region: auto_md_to_doc_comments include README.md A //! - -// endregion: auto_md_to_doc_comments include README.md A //! - -// access to modules -mod hello_mod; - -// `pub use` allows the caller of the lib to access modules functions, structs or all(*) -pub use hello_mod::format_hello_phrase; -pub use hello_mod::format_upper_hello_phrase; - -// The `main.rs` uses the `anyhow` error library. -// The `lib.rs` uses the `thiserror` library. -use thiserror::Error; - -/// all possible library errors for `thiserror` -#[derive(Error, Debug)] -pub enum LibraryError { - #[error("Name `{0}` is already uppercase.")] - Uppercase(String), - #[error("Unknown error.")] - Unknown, -} - -// ANSI colors for Linux terminal -// https://github.com/shiena/ansicolor/blob/master/README.md -#[allow(dead_code)] -pub const RED: &str = "\x1b[31m"; -#[allow(dead_code)] -pub const YELLOW: &str = "\x1b[33m"; -#[allow(dead_code)] -pub const GREEN: &str = "\x1b[32m"; -#[allow(dead_code)] -pub const RESET: &str = "\x1b[0m"; diff --git a/template_new_cli/tests/integration_test.rs b/template_new_cli/tests/integration_test.rs deleted file mode 100644 index 60fad812..00000000 --- a/template_new_cli/tests/integration_test.rs +++ /dev/null @@ -1,14 +0,0 @@ -// tests/integration_test.rs - -use cargo_auto_template_new_cli_lib::*; - -#[test] -fn integration_test_01() { - assert_eq!(format_hello_phrase("abcd"), "Hello abcd!"); - assert_eq!(format_upper_hello_phrase("abcd").expect("error"), "Hello ABCD!"); -} - -#[test] -fn integration_test_02_error_check() { - assert!(format_upper_hello_phrase("ABCD").is_err()); -} diff --git a/template_new_pwa_wasm/web_server_folder/pwa_short_name/service_worker.js b/template_new_pwa_wasm/web_server_folder/pwa_short_name/service_worker.js index 091baacd..94166daa 100644 --- a/template_new_pwa_wasm/web_server_folder/pwa_short_name/service_worker.js +++ b/template_new_pwa_wasm/web_server_folder/pwa_short_name/service_worker.js @@ -7,7 +7,7 @@ // but the new service worker will not be activated until all // tabs with this webapp are closed. -const CACHE_NAME = '2024.422.214'; +const CACHE_NAME = '2024.501.55'; self.addEventListener('install', event => { console.log('event install ', CACHE_NAME);