From 19e60217ea3788be546bdb6cb3102491fc45a58b Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 14 Apr 2026 14:04:44 +0200 Subject: [PATCH 01/20] 127.0.0.1 now works for federation --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 839e91c..9234589 100644 --- a/README.md +++ b/README.md @@ -274,8 +274,7 @@ proxy to allow using a wildcard domain. ### Setup - Set a DNS record for `*.haze.exmaple.com` and `haze.example.com` pointing to - your development machine. Pointing it to `127.0.0.1` will also work, but comes - with limitations like federation not being supported. + your development machine. - Set the `proxy` configuration with your domain and desired listen endpoint. - Set up a service to run `haze proxy` in the background as your own user. A systemd user service is recommended (see [haze.service](./haze.service) for an From 948d01600ebe7881db61f4bacd356ec8ff820ece Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 14 Apr 2026 14:52:41 +0200 Subject: [PATCH 02/20] readme typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9234589..d284ea7 100644 --- a/README.md +++ b/README.md @@ -295,7 +295,7 @@ will automatically point to the last created instance. Additionally, the proxy allows access to the server containers trough either `-.haze.example.com` for a specific instance, or -`.haze.example.com` for the last created instead. For example +`.haze.example.com` for the last created instance. For example `rolling-bees-mail.haze.example.com` will give access to the smtp4dev web interface of the `rolling-bees` instance. From 53e30a94aa16e41f406f743c0f160ea50484a2be Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 15 Apr 2026 23:28:11 +0200 Subject: [PATCH 03/20] fix generated urls in integration tests --- nix/image/scripts/integration | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nix/image/scripts/integration b/nix/image/scripts/integration index 8477132..32a9708 100755 --- a/nix/image/scripts/integration +++ b/nix/image/scripts/integration @@ -13,6 +13,10 @@ def main [feature: path, ...rest] { $feature = $feature | str replace "build/integration/" "" } + # don't use the proxy urls for generated urls + occ config:system:delete overwritehost + occ config:system:delete overwriteprotocol + cd $workdir bash run.sh $feature ...$rest } From 9de626a9058be9be25e65342ee0d021465558dad Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 16 Apr 2026 16:30:04 +0200 Subject: [PATCH 04/20] typo --- src/sources.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sources.rs b/src/sources.rs index b99c54a..ccfe564 100644 --- a/src/sources.rs +++ b/src/sources.rs @@ -122,7 +122,7 @@ pub async fn download_nc(config: &HazeConfig, version: &str) -> Result Date: Fri, 17 Apr 2026 22:22:24 +0200 Subject: [PATCH 05/20] autosetup for ldap fixes #19 --- src/service/ldap.rs | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/src/service/ldap.rs b/src/service/ldap.rs index aa082b0..22b7383 100644 --- a/src/service/ldap.rs +++ b/src/service/ldap.rs @@ -86,6 +86,47 @@ impl ServiceTrait for Ldap { ) -> Result { self.is_running(docker, cloud_id).await } + + async fn post_setup( + &self, + _docker: &Docker, + _cloud_id: &str, + _config: &HazeConfig, + ) -> Result> { + Ok(vec![ + "occ ldap:create-empty-config".into(), + "occ ldap:set-config s01 ldapHost 'ldap://ldap'".into(), + "occ ldap:set-config s01 ldapPort '389'".into(), + "occ ldap:set-config s01 ldapAgentName 'cn=admin,dc=example,dc=org'".into(), + "occ ldap:set-config s01 ldapAgentPassword 'haze'".into(), + "occ ldap:set-config s01 ldapBase 'dc=example,dc=org'".into(), + "occ ldap:set-config s01 ldapBaseUsers 'dc=example,dc=org'".into(), + "occ ldap:set-config s01 ldapBaseGroups 'dc=example,dc=org'".into(), + "occ ldap:set-config s01 ldapLoginFilter '(&(&(objectclass=inetOrgPerson))(uid=%uid))'" + .into(), + "occ ldap:set-config s01 ldapUserFilter '((objectclass=inetOrgPerson))'".into(), + "occ ldap:set-config s01 ldapUserFilterMode '0'".into(), + "occ ldap:set-config s01 ldapUserDisplayName 'sn'".into(), + "occ ldap:set-config s01 ldapUserFilterObjectclass 'inetOrgPerson'".into(), + "occ ldap:set-config s01 ldapGroupFilter '(&(|(objectclass=posixGroup)))'".into(), + "occ ldap:set-config s01 ldapGroupFilterObjectclass 'posixGroup'".into(), + "occ ldap:set-config s01 ldapEmailAttribute 'email'".into(), + "occ ldap:set-config s01 ldapUuidUserAttribute 'email'".into(), + "occ ldap:set-config s01 ldapUuidUserAttribute 'auto'".into(), + "occ ldap:set-config s01 ldapUuidGroupAttribute 'auto'".into(), + "occ ldap:set-config s01 ldapLoginFilterUsername '1'".into(), + "occ ldap:set-config s01 ldapConfigurationActive '1'".into(), + ]) + } + + async fn start_message( + &self, + _docker: &Docker, + _cloud_id: &str, + _proxy: &ProxyConfig, + ) -> Result> { + Ok(Some(format!("\nLdap users provisioned:\n\t'cn=admin,dc=example,dc=org' and password 'haze'\n\t'cn=ldaptest,dc=example,dc=org' and password 'test'\n\nldaptest is available for login\n"))) + } } #[derive(Debug, Clone, Eq, PartialEq)] @@ -185,8 +226,6 @@ impl ServiceTrait for LdapAdmin { return Err(Report::msg("ldap admin not started")); }; let addr = proxy.addr(&id, IpAddr::from_str(&ip).unwrap()); - Ok(Some(format!( - "Ldap admin running at: {addr} with 'cn=admin,dc=example,dc=org' and password 'haze'" - ))) + Ok(Some(format!("Ldap admin running at: {addr}"))) } } From b4a77997ab31836cdf1eb47298e0ba10afacc415 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 28 Apr 2026 18:54:44 +0200 Subject: [PATCH 06/20] fpm status page --- nix/image/configs/nginx.conf | 11 +++++++++++ nix/image/configs/php-fpm.conf | 1 + 2 files changed, 12 insertions(+) diff --git a/nix/image/configs/nginx.conf b/nix/image/configs/nginx.conf index b25ab52..15a57c3 100644 --- a/nix/image/configs/nginx.conf +++ b/nix/image/configs/nginx.conf @@ -109,5 +109,16 @@ http { expires 7d; # Cache-Control policy borrowed from `.htaccess` access_log off; # Optional: Don't log access to assets } + + location /fpm-status { + include /conf/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_pass php-handler; + fastcgi_read_timeout 3600; + proxy_request_buffering off; + fastcgi_request_buffering off; + fastcgi_buffering off; + } } } diff --git a/nix/image/configs/php-fpm.conf b/nix/image/configs/php-fpm.conf index e6292fb..c7751a4 100644 --- a/nix/image/configs/php-fpm.conf +++ b/nix/image/configs/php-fpm.conf @@ -21,6 +21,7 @@ pm.max_children = 5 pm.start_servers = 2 pm.min_spare_servers = 1 pm.max_spare_servers = 3 +pm.status_path = /fpm-status clear_env = no From ea3f89bb040f2f2fc8d089768eb450f071dff0c8 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 28 Apr 2026 18:55:08 +0200 Subject: [PATCH 07/20] fpm logs --- nix/image/configs/php-fpm.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/image/configs/php-fpm.conf b/nix/image/configs/php-fpm.conf index c7751a4..89379d8 100644 --- a/nix/image/configs/php-fpm.conf +++ b/nix/image/configs/php-fpm.conf @@ -1,11 +1,11 @@ [global] -error_log = /proc/self/fd/2 +error_log = /var/log/php-fpm-error.log daemonize = no [www] -access.log = /proc/self/fd/2 +access.log =/var/log/php-fpm-access.log user = haze group = haze From 512b669a7c02d4d5798b8579731fd7009d68d86c Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 30 Apr 2026 19:08:10 +0200 Subject: [PATCH 08/20] sqlite table mode --- nix/image/bootstrap | 1 + nix/image/configs/home/.sqliterc | 1 + 2 files changed, 2 insertions(+) create mode 100644 nix/image/configs/home/.sqliterc diff --git a/nix/image/bootstrap b/nix/image/bootstrap index d16e605..2c9dcff 100755 --- a/nix/image/bootstrap +++ b/nix/image/bootstrap @@ -42,6 +42,7 @@ if ((getent group $HAZE_GID | length) > 0) { useradd -u $HAZE_UID -g $HAZE_GID haze } chown -R $"haze:($HAZE_GID)" /home/haze +ls -af /etc/home | each {|file| ln -s $file.name $"/home/haze/($file.name | path basename)" } if ("/var/run/docker.sock" | path exists) { let dockerGid = stat --format "%g" /var/run/docker.sock diff --git a/nix/image/configs/home/.sqliterc b/nix/image/configs/home/.sqliterc new file mode 100644 index 0000000..0d5fe04 --- /dev/null +++ b/nix/image/configs/home/.sqliterc @@ -0,0 +1 @@ +.mode table From 9e080f0d544da3950337baf966068cc08246c0e1 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 7 May 2026 18:40:17 +0200 Subject: [PATCH 09/20] fix using '--' flags in test and integration commands --- nix/image/scripts/integration | 2 +- nix/image/scripts/tests | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/image/scripts/integration b/nix/image/scripts/integration index 32a9708..d8187d9 100755 --- a/nix/image/scripts/integration +++ b/nix/image/scripts/integration @@ -1,6 +1,6 @@ #!/bin/nu -def main [feature: path, ...rest] { +def --wrapped main [feature: path, ...rest] { mut feature = $feature; mut workdir = $"($env.WEBROOT)/build/integration" if ($feature | str starts-with "apps/") { diff --git a/nix/image/scripts/tests b/nix/image/scripts/tests index 524172c..868f225 100755 --- a/nix/image/scripts/tests +++ b/nix/image/scripts/tests @@ -1,6 +1,6 @@ #!/bin/nu -def main [...rest] { +def --wrapped main [...rest] { cd $env.WEBROOT XDEBUG_SESSION=haze phpunit --configuration $"($env.WEBROOT)/tests/phpunit-autotest.xml" ...$rest From 373ce0f3fd63cd782efd881d0c1e18b12eb1baec Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 7 May 2026 18:58:54 +0200 Subject: [PATCH 10/20] add php-imagick fixes #22 --- nix/image/php-ext.nix | 1 + nix/image/php.nix | 38 +++++++++++++++++--------------------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/nix/image/php-ext.nix b/nix/image/php-ext.nix index 2715cf2..69d23e1 100644 --- a/nix/image/php-ext.nix +++ b/nix/image/php-ext.nix @@ -35,6 +35,7 @@ in gmp apcu ffi + imagick ] ++ optionals (!debug) [ smbclient # this breaks the build for no apparent reason diff --git a/nix/image/php.nix b/nix/image/php.nix index 4922655..8ef3d31 100644 --- a/nix/image/php.nix +++ b/nix/image/php.nix @@ -2,27 +2,23 @@ lib, php, debug ? false, -}: let - inherit (builtins) compareVersions; - inherit (lib) optionals; - withBlackfire = !debug && ((compareVersions php.version "8.1.0") == 1); -in - php.buildEnv { - extensions = import ./php-ext.nix {inherit lib php debug;}; - extraConfig = '' - xdebug.mode=debug,trace,profile - xdebug.start_with_request=trigger - xdebug.discover_client_host=false - xdebug.client_host=hazehost - xdebug.log_level=0 - xdebug.output_dir=/tmp/xdebug +}: +php.buildEnv { + extensions = import ./php-ext.nix {inherit lib php debug;}; + extraConfig = '' + xdebug.mode=debug,trace,profile + xdebug.start_with_request=trigger + xdebug.discover_client_host=false + xdebug.client_host=hazehost + xdebug.log_level=0 + xdebug.output_dir=/tmp/xdebug - memory_limit=512M + memory_limit=512M - post_max_size 10G - upload_max_filesize 10G + post_max_size 10G + upload_max_filesize 10G - apc.enable_cli=1 - opcache.enable_cli=1 - ''; - } + apc.enable_cli=1 + opcache.enable_cli=1 + ''; +} From 8771e7dc5f3f02b152c403575d3152722f701a67 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 8 May 2026 18:57:19 +0200 Subject: [PATCH 11/20] switch to multiprogress for git pull --- src/git.rs | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/git.rs b/src/git.rs index 3a797c3..c20d450 100644 --- a/src/git.rs +++ b/src/git.rs @@ -2,11 +2,13 @@ use crate::config::HazeConfig; use crate::Result; use git2::build::CheckoutBuilder; use git2::{Branch, BranchType, Repository, RepositoryState}; +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use miette::{Context, IntoDiagnostic}; use std::fs::read_dir; use std::iter::once; use std::path::PathBuf; use std::process::Command; +use std::time::Duration; fn find_app_repos(config: &HazeConfig) -> Result> { let apps_dirs = once(config.sources_root.as_path().join("apps")) @@ -83,6 +85,9 @@ const GIT_BINARY: &str = match option_env!("GIT_BINARY") { pub fn pull_all(config: &HazeConfig) -> Result<()> { let (max_app, max_branch) = longest_app_branch(config)?; + let progress = MultiProgress::new(); + let pull_style = ProgressStyle::with_template("{spinner:.green} {msg}").unwrap(); + for app_dir in find_app_repos(config)? { let app_name = app_dir.file_name().unwrap().to_string_lossy(); let repo = Repository::init(&app_dir) @@ -90,17 +95,26 @@ pub fn pull_all(config: &HazeConfig) -> Result<()> { .wrap_err_with(|| format!("Failed to open repository {}", app_dir.display()))?; let branch_name = current_branch_name(&repo).unwrap_or("unknown".into()); - print!( - "{app_name: Result<()> { .wrap_err_with(|| format!("Failed to run git pull for {}", app_dir.display()))?; if output.status.success() { - println!(" ✓"); + bar.set_message(msg(" ✓")); } else { - println!(" ❌"); - eprintln!("{}", String::from_utf8_lossy(&output.stderr)) + let err = String::from_utf8_lossy(&output.stderr); + let err_line = err.lines().next().unwrap(); + bar.set_message(msg(&format!(" ❌ {err_line}"))); } + bar.finish(); } Ok(()) } From b977cd9dfa900324e90ffb496b42ac4b5bb1b799 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 8 May 2026 19:09:31 +0200 Subject: [PATCH 12/20] parallelize git pull fixes #21 --- Cargo.lock | 46 +++++++++++++++++++++++++++ Cargo.toml | 1 + src/git.rs | 92 ++++++++++++++++++++++++++++++++---------------------- 3 files changed, 101 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 22fafea..90f1ad9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -506,6 +506,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crypto-common" version = "0.1.7" @@ -956,6 +981,7 @@ dependencies = [ "opener", "owo-colors", "petname", + "rayon", "reqwest", "serde", "serde_json", @@ -2002,6 +2028,26 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.10" diff --git a/Cargo.toml b/Cargo.toml index 9318714..947de7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ zip = "8.1.0" sha2 = "0.11.0-rc.5" base16ct = { version = "1.0.0", features = ["alloc"] } indicatif = "0.18.4" +rayon = "1.12.0" hyper-reverse-proxy = { version = "0.5.2-dev", git = "https://github.com/chpio/hyper-reverse-proxy", rev = "6934877eb74465204f605cc1c05ca5a9772db7c0" } hyper = "1.8.1" diff --git a/src/git.rs b/src/git.rs index c20d450..50ebb05 100644 --- a/src/git.rs +++ b/src/git.rs @@ -4,6 +4,8 @@ use git2::build::CheckoutBuilder; use git2::{Branch, BranchType, Repository, RepositoryState}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use miette::{Context, IntoDiagnostic}; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use rayon::ThreadPoolBuilder; use std::fs::read_dir; use std::iter::once; use std::path::PathBuf; @@ -88,49 +90,63 @@ pub fn pull_all(config: &HazeConfig) -> Result<()> { let progress = MultiProgress::new(); let pull_style = ProgressStyle::with_template("{spinner:.green} {msg}").unwrap(); - for app_dir in find_app_repos(config)? { - let app_name = app_dir.file_name().unwrap().to_string_lossy(); - let repo = Repository::init(&app_dir) - .into_diagnostic() - .wrap_err_with(|| format!("Failed to open repository {}", app_dir.display()))?; - let branch_name = current_branch_name(&repo).unwrap_or("unknown".into()); + let pool = ThreadPoolBuilder::new() + .num_threads(8) + .build() + .into_diagnostic()?; + let repos = find_app_repos(config)?.collect::>(); - let bar = ProgressBar::new_spinner().with_style(pull_style.clone()); - bar.enable_steady_tick(Duration::from_millis(100)); - let bar = progress.add(bar); + pool.install(|| { + repos.par_iter().for_each(|app_dir| { + let app_name = app_dir.file_name().unwrap().to_string_lossy(); + let Ok(repo) = Repository::init(&app_dir) else { + return; + }; + let branch_name = current_branch_name(&repo).unwrap_or("unknown".into()); - let msg = |state: &str| { - format!( - "{app_name: output, + Err(error) => { + bar.set_message(msg(&format!(" ⨯ {error}"))); + return; + } + }; + + if output.status.success() { + bar.set_message(msg(" ✓")); + } else { + let err = String::from_utf8_lossy(&output.stderr); + let err_line = err.lines().next().unwrap(); + bar.set_message(msg(&format!(" ⨯ {err_line}"))); + } bar.finish(); - continue; - } + }); + }); - bar.set_message(msg("")); - - let output = Command::new(GIT_BINARY) - .arg("pull") - .current_dir(&app_dir) - .output() - .into_diagnostic() - .wrap_err_with(|| format!("Failed to run git pull for {}", app_dir.display()))?; - - if output.status.success() { - bar.set_message(msg(" ✓")); - } else { - let err = String::from_utf8_lossy(&output.stderr); - let err_line = err.lines().next().unwrap(); - bar.set_message(msg(&format!(" ❌ {err_line}"))); - } - bar.finish(); - } Ok(()) } From ad999702aabb605c73c597fca95b992f14f1ac54 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 8 May 2026 20:24:17 +0200 Subject: [PATCH 13/20] improve websocket proxying --- Cargo.lock | 49 +++++++++++++++------------- Cargo.toml | 7 ++-- nix/package.nix | 2 +- src/proxy.rs | 72 ++++++++++++++++++++++++++++++++---------- src/service.rs | 6 ++++ src/service/webhook.rs | 71 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 164 insertions(+), 43 deletions(-) create mode 100644 src/service/webhook.rs diff --git a/Cargo.lock b/Cargo.lock index 90f1ad9..ccedb17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,6 +147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ "axum-core", + "axum-macros", "bytes", "form_urlencoded", "futures-util", @@ -192,6 +193,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -921,25 +933,6 @@ dependencies = [ "url", ] -[[package]] -name = "h2" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "hashbrown" version = "0.15.2" @@ -957,7 +950,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "haze" -version = "2.2.1" +version = "2.2.2" dependencies = [ "async-trait", "atty", @@ -991,6 +984,7 @@ dependencies = [ "tar", "termion", "tokio", + "tokio-stream", "toml", "tracing", "tracing-subscriber", @@ -1098,7 +1092,6 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2", "http", "http-body", "httparse", @@ -1129,8 +1122,9 @@ dependencies = [ [[package]] name = "hyper-reverse-proxy" version = "0.5.2-dev" -source = "git+https://github.com/chpio/hyper-reverse-proxy?rev=6934877eb74465204f605cc1c05ca5a9772db7c0#6934877eb74465204f605cc1c05ca5a9772db7c0" +source = "git+https://code.betamike.com/micropelago/hyper-reverse-proxy.git?rev=d5a6f799189360d9449ae47ab3cdde511f02cf39#d5a6f799189360d9449ae47ab3cdde511f02cf39" dependencies = [ + "http-body-util", "hyper", "hyper-util", "tokio", @@ -2741,6 +2735,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.13" diff --git a/Cargo.toml b/Cargo.toml index 947de7c..6da76a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "haze" -version = "2.2.1" +version = "2.2.2" authors = ["Robin Appelman "] edition = "2021" repository = "https://codeberg.org/icewind/haze" @@ -12,6 +12,7 @@ bollard = "0.20.1" maplit = "1.0.2" camino = { version = "1.2.2", features = ["serde1"] } tokio = { version = "1.49.0", features = ["fs", "macros", "signal", "rt-multi-thread"] } +tokio-stream = { version = "0.1.18", features = ["net"] } futures-util = "0.3.32" termion = "4.0.6" opener = "0.8.4" @@ -41,10 +42,10 @@ base16ct = { version = "1.0.0", features = ["alloc"] } indicatif = "0.18.4" rayon = "1.12.0" -hyper-reverse-proxy = { version = "0.5.2-dev", git = "https://github.com/chpio/hyper-reverse-proxy", rev = "6934877eb74465204f605cc1c05ca5a9772db7c0" } +hyper-reverse-proxy = { version = "0.5.2-dev", git = "https://code.betamike.com/micropelago/hyper-reverse-proxy.git", rev = "d5a6f799189360d9449ae47ab3cdde511f02cf39" } hyper = "1.8.1" hyper-util = "0.1.20" -axum = { version = "0.8.8", features = ["tokio"] } +axum = { version = "0.8.8", features = ["tokio", "macros"] } [profile.release] lto = true diff --git a/nix/package.nix b/nix/package.nix index fcd6489..34a4388 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -20,7 +20,7 @@ in cargoLock = { lockFile = ../Cargo.lock; outputHashes = { - "hyper-reverse-proxy-0.5.2-dev" = "sha256-+ebi4FVVkiOpf75e8K5oGkHJBYQjLNJhUPNj+78zd7Q="; + "hyper-reverse-proxy-0.5.2-dev" = "sha256-awmj5aLFTea+kj81cwmfP1HWlWezwEKfyQSUJWjtamk="; }; }; } diff --git a/src/proxy.rs b/src/proxy.rs index d5a3b0e..4784a18 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -5,26 +5,34 @@ use axum::http::header::HOST; use axum::http::HeaderValue; use axum::{ body::Body, - extract::{Request, State}, + extract::Request, response::{IntoResponse, Response}, - Router, }; use bollard::Docker; +use futures_util::StreamExt; +use hyper::body::Incoming; +use hyper::server::conn::http1; +use hyper::service::service_fn; use hyper::StatusCode; +use hyper_util::rt::TokioIo; use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor}; use miette::{miette, IntoDiagnostic}; use std::collections::HashMap; +use std::convert::Infallible; use std::fs::{create_dir_all, set_permissions}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; +use std::pin::pin; use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::Duration; +use tokio::io::{AsyncRead, AsyncWrite}; use tokio::net::UnixListener; use tokio::signal::ctrl_c; use tokio::spawn; use tokio::time::sleep; +use tokio_stream::wrappers::{TcpListenerStream, UnixListenerStream}; use tracing::{debug, error, info}; struct ActiveInstances { @@ -163,20 +171,26 @@ async fn serve(instances: ActiveInstances, listen: String, base_address: String) ctrl_c().await.ok(); }; - let app = Router::new().fallback(handler).with_state(AppState { + let state = AppState { instances: instances.clone(), base_address: base_address.clone(), proxy_client: Arc::new(proxy_client), - }); + }; if !listen.starts_with('/') { let addr: SocketAddr = listen.parse().into_diagnostic()?; let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); println!("listening on {}", listener.local_addr().unwrap()); - axum::serve(listener, app) - .with_graceful_shutdown(cancel) - .await - .unwrap(); + let mut connections = pin!(TcpListenerStream::new(listener).take_until(cancel)); + + while let Some(stream) = connections.next().await { + match stream { + Ok(stream) => handle_connection(state.clone(), stream), + Err(error) => { + error!(%error, "connection failed"); + } + } + } } else { let listen: PathBuf = listen.into(); if let Some(parent) = listen.parent() { @@ -187,18 +201,42 @@ async fn serve(instances: ActiveInstances, listen: String, base_address: String) } let _ = tokio::fs::remove_file(&listen).await; - let uds = UnixListener::bind(&listen).unwrap(); + let listener = UnixListener::bind(&listen).unwrap(); + println!("listening on {}", listen.display()); set_permissions(&listen, PermissionsExt::from_mode(0o666)).into_diagnostic()?; - axum::serve(uds, app) - .with_graceful_shutdown(cancel) - .await - .unwrap(); + let mut connections = pin!(UnixListenerStream::new(listener).take_until(cancel)); + + while let Some(stream) = connections.next().await { + match stream { + Ok(stream) => handle_connection(state.clone(), stream), + Err(error) => { + error!(%error, "connection failed"); + } + } + } } Ok(()) } +fn handle_connection( + state: AppState, + stream: I, +) { + let io = TokioIo::new(stream); + // Spawn a tokio task to serve multiple connections concurrently + tokio::task::spawn(async move { + if let Err(err) = http1::Builder::new() + .serve_connection(io, service_fn(move |req| handler(state.clone(), req))) + .with_upgrades() + .await + { + eprintln!("Error serving connection: {:?}", err); + } + }); +} + async fn get_remote( host: Option<&HeaderValue>, instances: &ActiveInstances, @@ -232,9 +270,9 @@ async fn get_remote( } } -type Client = hyper_util::client::legacy::Client; +type Client = hyper_util::client::legacy::Client; -async fn handler(State(state): State, mut req: Request) -> Result { +async fn handler(state: AppState, mut req: Request) -> Result { let host = req.headers().get(HOST).cloned(); let remote = match get_remote(host.as_ref(), &state.instances, &state.base_address).await { Ok(remote) => remote, @@ -259,13 +297,13 @@ async fn handler(State(state): State, mut req: Request) -> Result Ok(response.map(Body::new)), Err(error) => { - error!(%error, "error while proxying request"); + error!(?error, "error while proxying request"); Ok(StatusCode::BAD_REQUEST.into_response()) } } diff --git a/src/service.rs b/src/service.rs index 49bb636..490f617 100644 --- a/src/service.rs +++ b/src/service.rs @@ -14,6 +14,7 @@ mod sftp; mod redis; mod sharded; mod smb; +mod webhook; use crate::cloud::CloudOptions; use crate::config::{HazeConfig, Preset, ProxyConfig}; @@ -32,6 +33,7 @@ use crate::service::redis::Redis; use crate::service::sftp::Sftp; use crate::service::sharded::{Sharding, ShardingMigrate, ShardingMigrateUnset, SingleShard}; use crate::service::smb::Smb; +use crate::service::webhook::Webhook; use bollard::models::ContainerState; use bollard::Docker; use enum_dispatch::enum_dispatch; @@ -296,6 +298,8 @@ pub enum ServiceType { RedisTls, /// Use FrankenPHP instead of PHP-FPM FrankenPhp, + /// Webhook test listener + Webhook, } #[enum_dispatch] @@ -326,6 +330,7 @@ pub enum Service { Redis(Redis), RedisTls(RedisTls), FrankenPhp(FrankenPhp), + Webhook(Webhook), Preset(PresetService), } @@ -369,6 +374,7 @@ impl Service { ServiceType::Redis => Some(vec![Service::Redis(Redis)]), ServiceType::RedisTls => Some(vec![Service::RedisTls(RedisTls)]), ServiceType::FrankenPhp => Some(vec![Service::FrankenPhp(FrankenPhp)]), + ServiceType::Webhook => Some(vec![Service::Webhook(Webhook)]), } } else { presets diff --git a/src/service/webhook.rs b/src/service/webhook.rs new file mode 100644 index 0000000..3967483 --- /dev/null +++ b/src/service/webhook.rs @@ -0,0 +1,71 @@ +use crate::cloud::CloudOptions; +use crate::config::HazeConfig; +use crate::image::pull_image; +use crate::service::ServiceTrait; +use crate::Result; +use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig}; +use bollard::query_parameters::CreateContainerOptions; +use bollard::Docker; +use maplit::hashmap; +use miette::IntoDiagnostic; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Webhook; + +#[async_trait::async_trait] +impl ServiceTrait for Webhook { + fn name(&self) -> &str { + "webhook" + } + + async fn spawn( + &self, + docker: &Docker, + cloud_id: &str, + network: &str, + _config: &HazeConfig, + _options: &CloudOptions, + ) -> Result> { + let image = "ghcr.io/tarampampam/webhook-tester"; + pull_image(docker, image).await?; + let options = Some(CreateContainerOptions { + name: self.container_name(cloud_id), + ..CreateContainerOptions::default() + }); + let config = ContainerCreateBody { + image: Some(image.into()), + host_config: Some(HostConfig { + network_mode: Some(network.to_string()), + ..Default::default() + }), + labels: Some(hashmap! { + "haze-type".into() => self.name().into(), + "haze-cloud-id".into() => cloud_id.into(), + }), + networking_config: Some(NetworkingConfig { + endpoints_config: Some(hashmap! { + network.into() => EndpointSettings { + aliases: Some(vec![self.name().to_string()]), + ..Default::default() + } + }), + }), + ..Default::default() + }; + let id = docker + .create_container(options, config) + .await + .into_diagnostic()? + .id; + docker.start_container(&id, None).await.into_diagnostic()?; + Ok(vec![id]) + } + + fn container_name(&self, cloud_id: &str) -> Option { + Some(format!("{}-webhook", cloud_id)) + } + + fn proxy_port(&self) -> u16 { + 8080 + } +} From 39ba7a2a5309e18a583513e2efc7f688830bc75c Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 8 May 2026 20:33:48 +0200 Subject: [PATCH 14/20] mention webhook tester in readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d284ea7..da8fecc 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,8 @@ Additionally, you can use the following options when starting an instance: generation. - `mail`: start a [smtp4dev](https://github.com/rnwood/smtp4dev) server and configure it the mail server. +- `webhook` start a + [webhook tester](https://github.com/tarampampam/webhook-tester) - `redis`: start a separate container for redis. - `redis-tls`: connect to redis over TLS. - ``: by specifying the path to an app package this package From cd9740675f0c0de66b25c6d13303a286df9f0def Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 8 May 2026 22:23:40 +0200 Subject: [PATCH 15/20] clippy fixes --- src/git.rs | 4 ++-- src/service/ldap.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/git.rs b/src/git.rs index 50ebb05..fc31255 100644 --- a/src/git.rs +++ b/src/git.rs @@ -99,7 +99,7 @@ pub fn pull_all(config: &HazeConfig) -> Result<()> { pool.install(|| { repos.par_iter().for_each(|app_dir| { let app_name = app_dir.file_name().unwrap().to_string_lossy(); - let Ok(repo) = Repository::init(&app_dir) else { + let Ok(repo) = Repository::init(app_dir) else { return; }; let branch_name = current_branch_name(&repo).unwrap_or("unknown".into()); @@ -126,7 +126,7 @@ pub fn pull_all(config: &HazeConfig) -> Result<()> { let output = match Command::new(GIT_BINARY) .arg("pull") - .current_dir(&app_dir) + .current_dir(app_dir) .output() { Ok(output) => output, diff --git a/src/service/ldap.rs b/src/service/ldap.rs index 22b7383..bd41ba3 100644 --- a/src/service/ldap.rs +++ b/src/service/ldap.rs @@ -125,7 +125,7 @@ impl ServiceTrait for Ldap { _cloud_id: &str, _proxy: &ProxyConfig, ) -> Result> { - Ok(Some(format!("\nLdap users provisioned:\n\t'cn=admin,dc=example,dc=org' and password 'haze'\n\t'cn=ldaptest,dc=example,dc=org' and password 'test'\n\nldaptest is available for login\n"))) + Ok(Some("\nLdap users provisioned:\n\t'cn=admin,dc=example,dc=org' and password 'haze'\n\t'cn=ldaptest,dc=example,dc=org' and password 'test'\n\nldaptest is available for login\n".into())) } } From f99238121b0755e302c4620aaab65f90dd158a21 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Sat, 9 May 2026 16:45:52 +0200 Subject: [PATCH 16/20] nix cleanup --- nix/package.nix | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nix/package.nix b/nix/package.nix index 34a4388..c6166f0 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -1,6 +1,5 @@ { rustPlatform, - pkg-config, lib, git, }: let @@ -10,7 +9,7 @@ src = sourceByRegex ../. ["Cargo.*" "(src|certificates)(/.*)?"]; version = (fromTOML (readFile ../Cargo.toml)).package.version; in - rustPlatform.buildRustPackage rec { + rustPlatform.buildRustPackage { pname = "haze"; inherit src version; From 204fb676d67aa0f794e070662310a79ae6e7e429 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 26 May 2026 20:45:29 +0200 Subject: [PATCH 17/20] add sftp with key authentication service --- README.md | 2 + certificates/sftp/id_rsa | 38 +++++++++++ certificates/sftp/id_rsa.pub | 1 + src/main.rs | 7 +- src/service.rs | 28 ++++++-- src/service/clam.rs | 46 ++++++------- src/service/dav.rs | 12 ++-- src/service/imaginary.rs | 13 ++-- src/service/kaspersky.rs | 24 +++---- src/service/ldap.rs | 47 +++++++------ src/service/mail.rs | 16 ++--- src/service/objectstore.rs | 22 +++---- src/service/oc.rs | 2 +- src/service/office.rs | 20 +++++- src/service/onlyoffice.rs | 46 ++++++++++--- src/service/push.rs | 16 +++-- src/service/redis.rs | 8 ++- src/service/sftp.rs | 123 ++++++++++++++++++++++++++++++++--- src/service/smb.rs | 14 ++-- 19 files changed, 349 insertions(+), 136 deletions(-) create mode 100644 certificates/sftp/id_rsa create mode 100644 certificates/sftp/id_rsa.pub diff --git a/README.md b/README.md index da8fecc..026c190 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,8 @@ Additionally, you can use the following options when starting an instance: - `smb`: set up a samba server for external storage use. - `dav`: set up a WebDAV server for external storage use. - `sftp`: set up a SFTP server for external storage use. +- `sftp-key`: set up a SFTP server for external storage use with public key + authentication. - `kaspersky`: set up a kaspersky scan engine server in http mode. ( Requires [manually setting up the image](https://github.com/icewind1991/kaspersky-docker)) - `kaspersky-icap`: setup a kaspersky scan engine server in ICAP mode. diff --git a/certificates/sftp/id_rsa b/certificates/sftp/id_rsa new file mode 100644 index 0000000..c8dcc4d --- /dev/null +++ b/certificates/sftp/id_rsa @@ -0,0 +1,38 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn +NhAAAAAwEAAQAAAYEA323aWqH6YwRLbCBO94UKOkfnJ2m6Zsic0dMt3TmDnjLU0JzpOt7w +t5+mMZrEKQTpefozyUHo3z+HkmllLAGOupNy3A+jG2O955UUgw0dGfu6j6OOb66Du9jpqt +8BQ6gr3cEYASplPI7B889/cVpJ5l1HiBUgyR7Z16v15qCDtmpFVIECAdICEmPosfmZutt3 +YYl9xLay5WCmUztWS/amPcGs0DOEGrWeCtdxGKWT3TywdBKyQ0PbdYMamgDIT7JV1ZSzZP +aly4sB7E+dpS5AgBFVXmZ61151KN1TJ8gyoUjFhY7ctYEIpncZmyT4PYvyIvxRsbJtvERi +eNH8DoX5DwtqcxbgHK0OwYtdl4ydRXToYo3l+qIidf+g8ADVea/mbkfTPegdToo3LOuThX +OwExDlukpM8obFDpz1Yl1L6rRJAVNO1KmHWhn6to23jtYjBhczA2nkemQXQbVSjc/hItjQ +DIFNMOsLW33P+Y2k9LkpI0TL09ogOxOFZzGZp2tNAAAFgIgMIZ+IDCGfAAAAB3NzaC1yc2 +EAAAGBAN9t2lqh+mMES2wgTveFCjpH5ydpumbInNHTLd05g54y1NCc6Tre8LefpjGaxCkE +6Xn6M8lB6N8/h5JpZSwBjrqTctwPoxtjveeVFIMNHRn7uo+jjm+ug7vY6arfAUOoK93BGA +EqZTyOwfPPf3FaSeZdR4gVIMke2der9eagg7ZqRVSBAgHSAhJj6LH5mbrbd2GJfcS2suVg +plM7Vkv2pj3BrNAzhBq1ngrXcRilk908sHQSskND23WDGpoAyE+yVdWUs2T2pcuLAexPna +UuQIARVV5metdedSjdUyfIMqFIxYWO3LWBCKZ3GZsk+D2L8iL8UbGybbxEYnjR/A6F+Q8L +anMW4BytDsGLXZeMnUV06GKN5fqiInX/oPAA1Xmv5m5H0z3oHU6KNyzrk4VzsBMQ5bpKTP +KGxQ6c9WJdS+q0SQFTTtSph1oZ+raNt47WIwYXMwNp5HpkF0G1Uo3P4SLY0AyBTTDrC1t9 +z/mNpPS5KSNEy9PaIDsThWcxmadrTQAAAAMBAAEAAAGAWCkM/TEnztU9e3M+JX253OhNRe +h6lB75ffOxh7avgAc3oP8hKkkYu6PDnJQgbb0R8T7wGywmGp0DPhrXQGd27ZjLvBhxeBfB +sbTJ7LIKdxu0cAQN6nR2Z3M+NF2dLpiXgn80HRWg76W20yDffRcuzLamyIPptWI2e9rPAw +r4HczOAXuMErLOfXotsbg22BvL/dEWLr4WVdruli32LbArxXd73IVPTYi3TTjYV+zRrPzK +9WoBK/iFClfKcdT4NTY82llQesuUNu640lEJtT2G3Iba8UZnohyzm/S+UbeU65z8DKD5co +P7+QehxQSV+kj2BZnTi0WEwsD+GTznJYR5rvUsJCCAzoISsWrncSSgOQhF2XeW/T4ewvH+ +njLZViEhdG8R3kkdDjJG91OrSgrEqlk6Qhz1xEsv1rCOR69En7EJP3TNNrymPXPASrAnuE +HQkrVgGUfGqyD1sw1e6nBfNWisuw+g99CieIB8EI9WwpxQdKqWNU9Hjx+SAdC3NrPjAAAA +wQCo0hUGjSf6xhcgeaNa0gWSKEVuFhxR/FaCPTKadV7Md0APW4toeQZDujzDFlCZbQTZjC +0723B4lKugDzXfsOgvOTKp4vEjZOu0YGruS00LFWM7Sutdzx68b/ZMFALzITt/myKVMdpv +WpaO+3+PyEYIQH44QrSWw7cKLzNiZ8kt2drPkPktub4o2h5TdIBluEQLJDPMejy8IqQEU8 +aOyJOMvYxAbGAWY7Ck9DGlcJgaFdORROW8d8ZGrHQkyRl41JQAAADBAPeUMsrbI17wPP/s +Tsrkms5ws5yTz0xle2Wn6HwDSzQRSdn5abnIDYb3QSy0nRBvczef6ssH65dl50+2V4BV2L +MwHcmKD6/UoFsWwP/RMf1EoacPFiEAWJGxFbOthNX+rx5BpbUHNoQd8xby+88saDI0e8W6 +36HPBZrAZhQljkMa4OJqZDDCpOJvSndXwkZ789E96uprKopJZGwlLmfMtikQpNXT9R+I0b +SQCJj4yNakcdOE/7UifkOR02u+pgux3wAAAMEA5wdelKwGQ0EdkIF2TM844uLPszo3ZSH+ +Heff/Lbxs1Y+oL0NTJQicwMF0d9WEwBoTZJpuzsQEA1zkfmW0gi2womIRmiY0ZhpxbBuhO +6XePMIhUfQmWWjaUbAkrNB0eJkSTuUGzwxVkMXehrMuj4gYe8GMC8GgULbP0A8FjH01fKk +jFwgg4WAg6zUTpck12bh49NZRFyXIbXNk/jjxJtb0p//5TRTUQ6mR5IloaNTM23EiF6tle +Y6CAchnyhHO0BTAAAACWhhemVAaGF6ZQE= +-----END OPENSSH PRIVATE KEY----- diff --git a/certificates/sftp/id_rsa.pub b/certificates/sftp/id_rsa.pub new file mode 100644 index 0000000..81a5db6 --- /dev/null +++ b/certificates/sftp/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDfbdpaofpjBEtsIE73hQo6R+cnabpmyJzR0y3dOYOeMtTQnOk63vC3n6YxmsQpBOl5+jPJQejfP4eSaWUsAY66k3LcD6MbY73nlRSDDR0Z+7qPo45vroO72Omq3wFDqCvdwRgBKmU8jsHzz39xWknmXUeIFSDJHtnXq/XmoIO2akVUgQIB0gISY+ix+Zm623dhiX3EtrLlYKZTO1ZL9qY9wazQM4QatZ4K13EYpZPdPLB0ErJDQ9t1gxqaAMhPslXVlLNk9qXLiwHsT52lLkCAEVVeZnrXXnUo3VMnyDKhSMWFjty1gQimdxmbJPg9i/Ii/FGxsm28RGJ40fwOhfkPC2pzFuAcrQ7Bi12XjJ1FdOhijeX6oiJ1/6DwANV5r+ZuR9M96B1Oijcs65OFc7ATEOW6SkzyhsUOnPViXUvqtEkBU07UqYdaGfq2jbeO1iMGFzMDaeR6ZBdBtVKNz+Ei2NAMgU0w6wtbfc/5jaT0uSkjRMvT2iA7E4VnMZmna00= haze@haze diff --git a/src/main.rs b/src/main.rs index 2e4de9b..a58e922 100644 --- a/src/main.rs +++ b/src/main.rs @@ -633,12 +633,7 @@ async fn setup(docker: &Docker, options: CloudOptions, config: &HazeConfig) -> R for service in cloud.services() { for cmd in service.post_setup(docker, &cloud.id, config).await? { cloud - .exec( - docker, - shell_words::split(&cmd).into_diagnostic()?, - false, - Vec::::default(), - ) + .exec(docker, cmd, false, Vec::::default()) .await?; } } diff --git a/src/service.rs b/src/service.rs index 490f617..cb517cb 100644 --- a/src/service.rs +++ b/src/service.rs @@ -30,7 +30,7 @@ pub use crate::service::office::Office; pub use crate::service::onlyoffice::OnlyOffice; pub use crate::service::push::NotifyPush; use crate::service::redis::Redis; -use crate::service::sftp::Sftp; +use crate::service::sftp::{Sftp, SftpKey}; use crate::service::sharded::{Sharding, ShardingMigrate, ShardingMigrateUnset, SingleShard}; use crate::service::smb::Smb; use crate::service::webhook::Webhook; @@ -116,7 +116,7 @@ pub trait ServiceTrait { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(Vec::new()) } @@ -267,6 +267,8 @@ pub enum ServiceType { Dav, /// Sftp external storage Sftp, + /// Sftp external storage with public key authentication + SftpKey, /// ownCloud instance for migration Oc, /// Imaginary for preview generation @@ -318,6 +320,7 @@ pub enum Service { ShardingMigrate(ShardingMigrate), ShardingMigrateUnset(ShardingMigrateUnset), Sftp(Sftp), + SftpKey(SftpKey), Kaspersky(Kaspersky), KasperskyIcap(KasperskyIcap), Clam(Clam), @@ -361,6 +364,7 @@ impl Service { } ServiceType::Dav => Some(vec![Service::Dav(Dav)]), ServiceType::Sftp => Some(vec![Service::Sftp(Sftp)]), + ServiceType::SftpKey => Some(vec![Service::SftpKey(SftpKey)]), ServiceType::Oc => Some(vec![Service::Oc(Oc)]), ServiceType::Imaginary => Some(vec![Service::Imaginary(Imaginary)]), ServiceType::Kaspersky => Some(vec![Service::Kaspersky(Kaspersky)]), @@ -437,15 +441,29 @@ impl ServiceTrait for PresetService { _docker: &Docker, _cloud_id: &str, config: &HazeConfig, - ) -> Result> { + ) -> Result>> { let preset = get_preset(&config.preset, &self.0).ok_or_else(|| Report::msg("invalid preset"))?; let mut commands: Vec<_> = preset .apps .iter() - .map(|app| format!("occ app:enable {app} --force")) + .map(|app| { + vec![ + "occ".into(), + "app:enable".into(), + app.clone(), + "--force".into(), + ] + }) .collect(); - commands.extend_from_slice(&preset.commands); + for cmnd in &preset.commands { + commands.push(shell_words::split(cmnd).into_diagnostic()?); + } + Ok(commands) } } + +fn split_cmnd(s: &str) -> Vec { + s.split(' ').map(String::from).collect() +} diff --git a/src/service/clam.rs b/src/service/clam.rs index 74b790d..7221309 100644 --- a/src/service/clam.rs +++ b/src/service/clam.rs @@ -2,7 +2,7 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::exec::exec; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig}; use bollard::query_parameters::CreateContainerOptions; @@ -85,14 +85,13 @@ impl ServiceTrait for ClamIcap { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ config:app:set files_antivirus av_mode --value=icap".into(), - "occ config:app:set files_antivirus av_host --value=clamav-icap".into(), - "occ config:app:set files_antivirus av_port --value=1344".into(), - "occ config:app:set files_antivirus av_icap_request_service --value=avscan".into(), - "occ config:app:set files_antivirus av_icap_response_header --value=X-Infection-Found" - .into(), + split_cmnd("occ config:app:set files_antivirus av_mode --value=icap"), + split_cmnd("occ config:app:set files_antivirus av_host --value=clamav-icap"), + split_cmnd("occ config:app:set files_antivirus av_port --value=1344"), + split_cmnd("occ config:app:set files_antivirus av_icap_request_service --value=avscan"), + split_cmnd("occ config:app:set files_antivirus av_icap_response_header --value=X-Infection-Found"), ]) } } @@ -171,7 +170,7 @@ impl ServiceTrait for ClamIcapTls { docker: &Docker, cloud_id: &str, config: &HazeConfig, - ) -> Result> { + ) -> Result>> { let mut cert = Vec::new(); exec( docker, @@ -191,14 +190,13 @@ impl ServiceTrait for ClamIcapTls { .wrap_err("Failed to write icap certificate")?; Ok(vec![ - "occ config:app:set files_antivirus av_mode --value=icap".into(), - "occ config:app:set files_antivirus av_icap_tls --value=1".into(), - "occ config:app:set files_antivirus av_host --value=clamav-icap-tls".into(), - "occ config:app:set files_antivirus av_port --value=1345".into(), - "occ config:app:set files_antivirus av_icap_request_service --value=avscan".into(), - "occ config:app:set files_antivirus av_icap_response_header --value=X-Infection-Found" - .into(), - "occ security:certificates:import data/icap-cert.pem".into(), + split_cmnd("occ config:app:set files_antivirus av_mode --value=icap"), + split_cmnd("occ config:app:set files_antivirus av_icap_tls --value=1"), + split_cmnd("occ config:app:set files_antivirus av_host --value=clamav-icap-tls"), + split_cmnd("occ config:app:set files_antivirus av_port --value=1345"), + split_cmnd("occ config:app:set files_antivirus av_icap_request_service --value=avscan"), + split_cmnd("occ config:app:set files_antivirus av_icap_response_header --value=X-Infection-Found"), + split_cmnd("occ security:certificates:import data/icap-cert.pem"), ]) } } @@ -221,10 +219,10 @@ impl ServiceTrait for Clam { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ config:app:set files_antivirus av_mode --value=executable".into(), - "occ config:app:set files_antivirus av_path --value=/bin/clamscan".into(), + split_cmnd("occ config:app:set files_antivirus av_mode --value=executable"), + split_cmnd("occ config:app:set files_antivirus av_path --value=/bin/clamscan"), ]) } } @@ -294,10 +292,12 @@ impl ServiceTrait for ClamSocket { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ config:app:set files_antivirus av_mode --value=socket".into(), - "occ config:app:set files_antivirus av_socket --value=tcp://clamav-socket:3310".into(), + split_cmnd("occ config:app:set files_antivirus av_mode --value=socket"), + split_cmnd( + "occ config:app:set files_antivirus av_socket --value=tcp://clamav-socket:3310", + ), ]) } } diff --git a/src/service/dav.rs b/src/service/dav.rs index d328925..e0b20f3 100644 --- a/src/service/dav.rs +++ b/src/service/dav.rs @@ -1,7 +1,7 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::config::ContainerCreateBody; use bollard::models::{EndpointSettings, HostConfig, NetworkingConfig}; @@ -76,12 +76,12 @@ impl ServiceTrait for Dav { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ files_external:create dav dav password::password".into(), - "occ files_external:config 1 host dav".into(), - "occ files_external:config 1 user test".into(), - "occ files_external:config 1 password test".into(), + split_cmnd("occ files_external:create dav dav password::password"), + split_cmnd("occ files_external:config 1 host dav"), + split_cmnd("occ files_external:config 1 user test"), + split_cmnd("occ files_external:config 1 password test"), ]) } } diff --git a/src/service/imaginary.rs b/src/service/imaginary.rs index 693c173..4ec4d96 100644 --- a/src/service/imaginary.rs +++ b/src/service/imaginary.rs @@ -1,7 +1,7 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::config::NetworkingConfig; use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig}; @@ -71,11 +71,14 @@ impl ServiceTrait for Imaginary { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ config:system:set enabledPreviewProviders 0 --value='OC\\Preview\\Imaginary'" - .into(), - "occ config:system:set preview_imaginary_url --value='http://imaginary:9000'".into(), + split_cmnd( + "occ config:system:set enabledPreviewProviders 0 --value='OC\\Preview\\Imaginary'", + ), + split_cmnd( + "occ config:system:set preview_imaginary_url --value='http://imaginary:9000'", + ), ]) } } diff --git a/src/service/kaspersky.rs b/src/service/kaspersky.rs index bc4f08a..1c0a67f 100644 --- a/src/service/kaspersky.rs +++ b/src/service/kaspersky.rs @@ -2,7 +2,7 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::exec::exec; use crate::image::{image_exists, pull_image}; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig}; use bollard::query_parameters::CreateContainerOptions; @@ -101,11 +101,11 @@ impl ServiceTrait for Kaspersky { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ config:app:set files_antivirus av_mode --value=kaspersky".into(), - "occ config:app:set files_antivirus av_host --value=kaspersky".into(), - "occ config:app:set files_antivirus av_port --value=80".into(), + split_cmnd("occ config:app:set files_antivirus av_mode --value=kaspersky"), + split_cmnd("occ config:app:set files_antivirus av_host --value=kaspersky"), + split_cmnd("occ config:app:set files_antivirus av_port --value=80"), ]) } } @@ -187,13 +187,15 @@ impl ServiceTrait for KasperskyIcap { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ config:app:set files_antivirus av_mode --value=icap".into(), - "occ config:app:set files_antivirus av_host --value=kaspersky-icap".into(), - "occ config:app:set files_antivirus av_port --value=1344".into(), - "occ config:app:set files_antivirus av_icap_request_service --value=req".into(), - "occ config:app:set files_antivirus av_icap_response_header --value=X-Virus-ID".into(), + split_cmnd("occ config:app:set files_antivirus av_mode --value=icap"), + split_cmnd("occ config:app:set files_antivirus av_host --value=kaspersky-icap"), + split_cmnd("occ config:app:set files_antivirus av_port --value=1344"), + split_cmnd("occ config:app:set files_antivirus av_icap_request_service --value=req"), + split_cmnd( + "occ config:app:set files_antivirus av_icap_response_header --value=X-Virus-ID", + ), ]) } } diff --git a/src/service/ldap.rs b/src/service/ldap.rs index bd41ba3..0f22643 100644 --- a/src/service/ldap.rs +++ b/src/service/ldap.rs @@ -1,7 +1,7 @@ use crate::cloud::CloudOptions; use crate::config::{HazeConfig, ProxyConfig}; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::config::NetworkingConfig; use bollard::models::{ContainerCreateBody, ContainerState, EndpointSettings, HostConfig}; @@ -92,30 +92,29 @@ impl ServiceTrait for Ldap { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ ldap:create-empty-config".into(), - "occ ldap:set-config s01 ldapHost 'ldap://ldap'".into(), - "occ ldap:set-config s01 ldapPort '389'".into(), - "occ ldap:set-config s01 ldapAgentName 'cn=admin,dc=example,dc=org'".into(), - "occ ldap:set-config s01 ldapAgentPassword 'haze'".into(), - "occ ldap:set-config s01 ldapBase 'dc=example,dc=org'".into(), - "occ ldap:set-config s01 ldapBaseUsers 'dc=example,dc=org'".into(), - "occ ldap:set-config s01 ldapBaseGroups 'dc=example,dc=org'".into(), - "occ ldap:set-config s01 ldapLoginFilter '(&(&(objectclass=inetOrgPerson))(uid=%uid))'" - .into(), - "occ ldap:set-config s01 ldapUserFilter '((objectclass=inetOrgPerson))'".into(), - "occ ldap:set-config s01 ldapUserFilterMode '0'".into(), - "occ ldap:set-config s01 ldapUserDisplayName 'sn'".into(), - "occ ldap:set-config s01 ldapUserFilterObjectclass 'inetOrgPerson'".into(), - "occ ldap:set-config s01 ldapGroupFilter '(&(|(objectclass=posixGroup)))'".into(), - "occ ldap:set-config s01 ldapGroupFilterObjectclass 'posixGroup'".into(), - "occ ldap:set-config s01 ldapEmailAttribute 'email'".into(), - "occ ldap:set-config s01 ldapUuidUserAttribute 'email'".into(), - "occ ldap:set-config s01 ldapUuidUserAttribute 'auto'".into(), - "occ ldap:set-config s01 ldapUuidGroupAttribute 'auto'".into(), - "occ ldap:set-config s01 ldapLoginFilterUsername '1'".into(), - "occ ldap:set-config s01 ldapConfigurationActive '1'".into(), + split_cmnd("occ ldap:create-empty-config"), + split_cmnd("occ ldap:set-config s01 ldapHost 'ldap://ldap'"), + split_cmnd("occ ldap:set-config s01 ldapPort '389'"), + split_cmnd("occ ldap:set-config s01 ldapAgentName 'cn=admin,dc=example,dc=org'"), + split_cmnd("occ ldap:set-config s01 ldapAgentPassword 'haze'"), + split_cmnd("occ ldap:set-config s01 ldapBase 'dc=example,dc=org'"), + split_cmnd("occ ldap:set-config s01 ldapBaseUsers 'dc=example,dc=org'"), + split_cmnd("occ ldap:set-config s01 ldapBaseGroups 'dc=example,dc=org'"), + split_cmnd("occ ldap:set-config s01 ldapLoginFilter '(&(&(objectclass=inetOrgPerson))(uid=%uid))'"), + split_cmnd("occ ldap:set-config s01 ldapUserFilter '((objectclass=inetOrgPerson))'"), + split_cmnd("occ ldap:set-config s01 ldapUserFilterMode '0'"), + split_cmnd("occ ldap:set-config s01 ldapUserDisplayName 'sn'"), + split_cmnd("occ ldap:set-config s01 ldapUserFilterObjectclass 'inetOrgPerson'"), + split_cmnd("occ ldap:set-config s01 ldapGroupFilter '(&(|(objectclass=posixGroup)))'"), + split_cmnd("occ ldap:set-config s01 ldapGroupFilterObjectclass 'posixGroup'"), + split_cmnd("occ ldap:set-config s01 ldapEmailAttribute 'email'"), + split_cmnd("occ ldap:set-config s01 ldapUuidUserAttribute 'email'"), + split_cmnd("occ ldap:set-config s01 ldapUuidUserAttribute 'auto'"), + split_cmnd("occ ldap:set-config s01 ldapUuidGroupAttribute 'auto'"), + split_cmnd("occ ldap:set-config s01 ldapLoginFilterUsername '1'"), + split_cmnd("occ ldap:set-config s01 ldapConfigurationActive '1'"), ]) } diff --git a/src/service/mail.rs b/src/service/mail.rs index c52fdac..ad40471 100644 --- a/src/service/mail.rs +++ b/src/service/mail.rs @@ -1,7 +1,7 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig}; use bollard::query_parameters::CreateContainerOptions; @@ -70,14 +70,14 @@ impl ServiceTrait for Mail { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ config:system:set mail_smtpmode --value smtp".into(), - "occ config:system:set mail_sendmailmode --value smtp".into(), - "occ config:system:set mail_domain --value haze".into(), - "occ config:system:set mail_smtphost --value mail".into(), - "occ config:system:set mail_smtpport --value 25".into(), - "occ user:setting admin settings email admin@haze".into(), + split_cmnd("occ config:system:set mail_smtpmode --value smtp"), + split_cmnd("occ config:system:set mail_sendmailmode --value smtp"), + split_cmnd("occ config:system:set mail_domain --value haze"), + split_cmnd("occ config:system:set mail_smtphost --value mail"), + split_cmnd("occ config:system:set mail_smtpport --value 25"), + split_cmnd("occ user:setting admin settings email admin@haze"), ]) } } diff --git a/src/service/objectstore.rs b/src/service/objectstore.rs index c0767e0..20e586c 100644 --- a/src/service/objectstore.rs +++ b/src/service/objectstore.rs @@ -2,7 +2,7 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::exec::exec; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::models::{ ContainerCreateBody, ContainerState, EndpointSettings, HostConfig, NetworkingConfig, @@ -247,18 +247,18 @@ impl ServiceTrait for ObjectStore { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { match self { ObjectStore::S3 => Ok(vec![ - "occ files_external:create s3 amazons3 amazons3::accesskey".into(), - "occ files_external:config 1 bucket ext".into(), - "occ files_external:config 1 hostname s3".into(), - "occ files_external:config 1 port 9000".into(), - "occ files_external:config 1 use_ssl false".into(), - "occ files_external:config 1 use_path_style true".into(), - "occ files_external:config 1 key minio".into(), - "occ files_external:config 1 secret minio123".into(), - "mc alias set s3 http://s3:9000 minio minio123".into(), + split_cmnd("occ files_external:create s3 amazons3 amazons3::accesskey"), + split_cmnd("occ files_external:config 1 bucket ext"), + split_cmnd("occ files_external:config 1 hostname s3"), + split_cmnd("occ files_external:config 1 port 9000"), + split_cmnd("occ files_external:config 1 use_ssl false"), + split_cmnd("occ files_external:config 1 use_path_style true"), + split_cmnd("occ files_external:config 1 key minio"), + split_cmnd("occ files_external:config 1 secret minio123"), + split_cmnd("mc alias set s3 http://s3:9000 minio minio123"), ]), // ObjectStore::S3s => Ok(vec![ // "occ files_external:create s3 amazons3 amazons3::accesskey".into(), diff --git a/src/service/oc.rs b/src/service/oc.rs index 2eb3a22..296b2a5 100644 --- a/src/service/oc.rs +++ b/src/service/oc.rs @@ -83,7 +83,7 @@ impl ServiceTrait for Oc { docker: &Docker, cloud_id: &str, config: &HazeConfig, - ) -> Result> { + ) -> Result>> { if let Some(ip) = self.get_ips(docker, cloud_id).await?.next() { let container = self.container_name(cloud_id).unwrap(); let addr = config.proxy.addr(&container, ip); diff --git a/src/service/office.rs b/src/service/office.rs index 8aaf99a..e7db10d 100644 --- a/src/service/office.rs +++ b/src/service/office.rs @@ -119,7 +119,7 @@ impl ServiceTrait for Office { docker: &Docker, cloud_id: &str, config: &HazeConfig, - ) -> Result> { + ) -> Result>> { let container = &self.container_name(cloud_id).unwrap(); let info = docker .inspect_container(container, None) @@ -152,8 +152,22 @@ impl ServiceTrait for Office { .addr_with_port(container, ip, self.proxy_port()); Ok(vec![ - format!(r#"occ config:app:set richdocuments public_wopi_url --value="{public}""#), - r#"occ richdocuments:setup --wopi-url "http://office:9980" --callback-url "http://cloud""#.into(), + vec![ + "occ".into(), + "config:app:set".into(), + "richdocuments".into(), + "public_wopi_url".into(), + "--value".into(), + public, + ], + vec![ + "occ".into(), + "richdocuments:setup".into(), + "--wopi-url".into(), + "http://office:9980".into(), + "--callback-url".into(), + "http://cloud".into(), + ], ]) } diff --git a/src/service/onlyoffice.rs b/src/service/onlyoffice.rs index b7c8a14..fa12c2c 100644 --- a/src/service/onlyoffice.rs +++ b/src/service/onlyoffice.rs @@ -2,7 +2,7 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::exec::exec; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::models::{ ContainerCreateBody, ContainerState, EndpointSettings, HostConfig, NetworkingConfig, @@ -82,7 +82,7 @@ impl ServiceTrait for OnlyOffice { docker: &Docker, cloud_id: &str, config: &HazeConfig, - ) -> Result> { + ) -> Result>> { let info = docker .inspect_container(&self.container_name(cloud_id).unwrap(), None) .await @@ -137,16 +137,44 @@ impl ServiceTrait for OnlyOffice { ); Ok(vec![ - format!("occ config:app:set onlyoffice DocumentServerUrl --value {addr}/"), - format!("occ config:app:set onlyoffice jwt_secret --value {secret}"), - "occ onlyoffice:documentserver --check".into(), + vec![ + "occ".into(), + "config:app:set".into(), + "onlyoffice".into(), + "DocumentServerUrl".into(), + "--value".into(), + addr, + ], + vec![ + "occ".into(), + "config:app:set".into(), + "onlyoffice".into(), + "jwt_secret".into(), + "--value".into(), + secret.into(), + ], + split_cmnd("occ onlyoffice:documentserver --check"), ]) } else { Ok(vec![ - format!("occ config:app:set onlyoffice DocumentServerUrl --value https://{ip}/"), - "occ config:app:set onlyoffice verify_peer_off --value true".into(), - format!("occ config:app:set onlyoffice jwt_secret --value {secret}"), - "occ onlyoffice:documentserver --check".into(), + vec![ + "occ".into(), + "config:app:set".into(), + "onlyoffice".into(), + "DocumentServerUrl".into(), + "--value".into(), + format!("https://{ip}/"), + ], + split_cmnd("occ config:app:set onlyoffice verify_peer_off --value true"), + vec![ + "occ".into(), + "config:app:set".into(), + "onlyoffice".into(), + "jwt_secret".into(), + "--value".into(), + secret.into(), + ], + split_cmnd("occ onlyoffice:documentserver --check"), ]) } } diff --git a/src/service/push.rs b/src/service/push.rs index 22b3ac2..b0bbab2 100644 --- a/src/service/push.rs +++ b/src/service/push.rs @@ -87,7 +87,7 @@ impl ServiceTrait for NotifyPush { docker: &Docker, cloud_id: &str, config: &HazeConfig, - ) -> Result> { + ) -> Result>> { let mut ips: Vec<_> = self.get_ips(docker, cloud_id).await?.collect(); if let Ok(local_interfaces) = list_afinet_netifas() { ips.extend(local_interfaces.into_iter().map(|(_, ip)| ip)); @@ -97,10 +97,14 @@ impl ServiceTrait for NotifyPush { .iter() .enumerate() .map(|(i, ip)| { - format!( - "occ config:system:set trusted_proxies {} --value {ip}", - i + 1 - ) + vec![ + "occ".into(), + "config:system:set".into(), + "trusted_proxies".into(), + (i + 1).to_string(), + "--value".into(), + ip.to_string(), + ] }) .collect(); @@ -108,7 +112,7 @@ impl ServiceTrait for NotifyPush { config .proxy .addr_with_port(&self.container_name(cloud_id).unwrap(), ips[0], 7867); - commands.push(format!("occ notify_push:setup {}", addr)); + commands.push(vec!["occ".into(), "notify_push:setup".into(), addr]); Ok(commands) } diff --git a/src/service/redis.rs b/src/service/redis.rs index 29a22ee..db69667 100644 --- a/src/service/redis.rs +++ b/src/service/redis.rs @@ -1,7 +1,7 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig}; use bollard::query_parameters::CreateContainerOptions; @@ -70,7 +70,9 @@ impl ServiceTrait for Redis { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { - Ok(vec!["occ config:system:set redis host --value redis".into()]) + ) -> Result>> { + Ok(vec![split_cmnd( + "occ config:system:set redis host --value redis", + )]) } } diff --git a/src/service/sftp.rs b/src/service/sftp.rs index 80405e2..c414b16 100644 --- a/src/service/sftp.rs +++ b/src/service/sftp.rs @@ -1,13 +1,14 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig}; use bollard::query_parameters::CreateContainerOptions; use bollard::Docker; use maplit::hashmap; -use miette::IntoDiagnostic; +use miette::{Context, IntoDiagnostic}; +use std::fs::{create_dir_all, write}; #[derive(Debug, Clone, Eq, PartialEq)] pub struct Sftp; @@ -75,13 +76,119 @@ impl ServiceTrait for Sftp { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ files_external:create sftp sftp password::password".into(), - "occ files_external:config 1 host sftp".into(), - "occ files_external:config 1 user test".into(), - "occ files_external:config 1 root data".into(), - "occ files_external:config 1 password test".into(), + split_cmnd("occ files_external:create sftp sftp password::password"), + split_cmnd("occ files_external:config 1 host sftp"), + split_cmnd("occ files_external:config 1 user test"), + split_cmnd("occ files_external:config 1 root data"), + split_cmnd("occ files_external:config 1 password test"), + ]) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct SftpKey; + +#[async_trait::async_trait] +impl ServiceTrait for SftpKey { + fn name(&self) -> &str { + "sftp-key" + } + + async fn spawn( + &self, + docker: &Docker, + cloud_id: &str, + network: &str, + config: &HazeConfig, + _options: &CloudOptions, + ) -> Result> { + let image = "atmoz/sftp:alpine"; + pull_image(docker, image).await?; + let options = Some(CreateContainerOptions { + name: self.container_name(cloud_id), + ..CreateContainerOptions::default() + }); + let key_dir = config.work_dir.join("certificates/sftp"); + create_dir_all(&key_dir) + .into_diagnostic() + .wrap_err("Failed to create sftp certificate directory")?; + let private_path = key_dir.join("id_rsa"); + let public_path = key_dir.join("id_rsa.pub"); + let private_key = include_str!("../../certificates/sftp/id_rsa"); + let public_key = include_str!("../../certificates/sftp/id_rsa.pub"); + if !private_path.exists() { + write(&private_path, private_key) + .into_diagnostic() + .wrap_err("Failed to write sftp client certificate")?; + } + if !public_path.exists() { + write(&public_path, public_key) + .into_diagnostic() + .wrap_err("Failed to write sftp client key")?; + } + + let volumes = vec![format!("{public_path}:/home/test/.ssh/keys/id_rsa:ro")]; + + let config = ContainerCreateBody { + image: Some(image.into()), + host_config: Some(HostConfig { + network_mode: Some(network.to_string()), + binds: Some(volumes), + ..Default::default() + }), + labels: Some(hashmap! { + "haze-type".into() => self.name().into(), + "haze-cloud-id".into() => cloud_id.into(), + }), + networking_config: Some(NetworkingConfig { + endpoints_config: Some(hashmap! { + network.into() => EndpointSettings { + aliases: Some(vec![self.name().to_string()]), + ..Default::default() + } + }), + }), + cmd: Some(vec!["test::::data".into()]), + ..Default::default() + }; + let id = docker + .create_container(options, config) + .await + .into_diagnostic()? + .id; + docker.start_container(&id, None).await.into_diagnostic()?; + Ok(vec![id]) + } + + fn container_name(&self, cloud_id: &str) -> Option { + Some(format!("{}-sftp-key", cloud_id)) + } + + fn apps(&self) -> &'static [&'static str] { + &["files_external"] + } + + async fn post_setup( + &self, + _docker: &Docker, + _cloud_id: &str, + _config: &HazeConfig, + ) -> Result>> { + Ok(vec![ + split_cmnd("occ files_external:create sftp sftp publickey::rsa_private"), + split_cmnd("occ files_external:config 1 host sftp-key"), + split_cmnd("occ files_external:config 1 user test"), + split_cmnd("occ files_external:config 1 root data"), + vec![ + "occ".into(), + "files_external:config".into(), + "--value-from-file".into(), + "1".into(), + "private_key".into(), + "/certificates/sftp/id_rsa".into(), + ], ]) } } diff --git a/src/service/smb.rs b/src/service/smb.rs index a40b105..da8f1f6 100644 --- a/src/service/smb.rs +++ b/src/service/smb.rs @@ -1,7 +1,7 @@ use crate::cloud::CloudOptions; use crate::config::HazeConfig; use crate::image::pull_image; -use crate::service::ServiceTrait; +use crate::service::{split_cmnd, ServiceTrait}; use crate::Result; use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig}; use bollard::query_parameters::CreateContainerOptions; @@ -79,13 +79,13 @@ impl ServiceTrait for Smb { _docker: &Docker, _cloud_id: &str, _config: &HazeConfig, - ) -> Result> { + ) -> Result>> { Ok(vec![ - "occ files_external:create smb smb password::password".into(), - "occ files_external:config 1 host smb".into(), - "occ files_external:config 1 user test".into(), - "occ files_external:config 1 password test".into(), - "occ files_external:config 1 share test".into(), + split_cmnd("occ files_external:create smb smb password::password"), + split_cmnd("occ files_external:config 1 host smb"), + split_cmnd("occ files_external:config 1 user test"), + split_cmnd("occ files_external:config 1 password test"), + split_cmnd("occ files_external:config 1 share test"), ]) } } From 92fbc74a5b7bb5d4f309b22cdbe690705ffadb6a Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 28 May 2026 18:40:20 +0200 Subject: [PATCH 18/20] fix ldap --- src/service/ldap.rs | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/service/ldap.rs b/src/service/ldap.rs index 0f22643..4f317e6 100644 --- a/src/service/ldap.rs +++ b/src/service/ldap.rs @@ -95,26 +95,26 @@ impl ServiceTrait for Ldap { ) -> Result>> { Ok(vec![ split_cmnd("occ ldap:create-empty-config"), - split_cmnd("occ ldap:set-config s01 ldapHost 'ldap://ldap'"), - split_cmnd("occ ldap:set-config s01 ldapPort '389'"), - split_cmnd("occ ldap:set-config s01 ldapAgentName 'cn=admin,dc=example,dc=org'"), - split_cmnd("occ ldap:set-config s01 ldapAgentPassword 'haze'"), - split_cmnd("occ ldap:set-config s01 ldapBase 'dc=example,dc=org'"), - split_cmnd("occ ldap:set-config s01 ldapBaseUsers 'dc=example,dc=org'"), - split_cmnd("occ ldap:set-config s01 ldapBaseGroups 'dc=example,dc=org'"), - split_cmnd("occ ldap:set-config s01 ldapLoginFilter '(&(&(objectclass=inetOrgPerson))(uid=%uid))'"), - split_cmnd("occ ldap:set-config s01 ldapUserFilter '((objectclass=inetOrgPerson))'"), - split_cmnd("occ ldap:set-config s01 ldapUserFilterMode '0'"), - split_cmnd("occ ldap:set-config s01 ldapUserDisplayName 'sn'"), - split_cmnd("occ ldap:set-config s01 ldapUserFilterObjectclass 'inetOrgPerson'"), - split_cmnd("occ ldap:set-config s01 ldapGroupFilter '(&(|(objectclass=posixGroup)))'"), - split_cmnd("occ ldap:set-config s01 ldapGroupFilterObjectclass 'posixGroup'"), - split_cmnd("occ ldap:set-config s01 ldapEmailAttribute 'email'"), - split_cmnd("occ ldap:set-config s01 ldapUuidUserAttribute 'email'"), - split_cmnd("occ ldap:set-config s01 ldapUuidUserAttribute 'auto'"), - split_cmnd("occ ldap:set-config s01 ldapUuidGroupAttribute 'auto'"), - split_cmnd("occ ldap:set-config s01 ldapLoginFilterUsername '1'"), - split_cmnd("occ ldap:set-config s01 ldapConfigurationActive '1'"), + split_cmnd("occ ldap:set-config s01 ldapHost ldap://ldap"), + split_cmnd("occ ldap:set-config s01 ldapPort 389"), + split_cmnd("occ ldap:set-config s01 ldapAgentName cn=admin,dc=example,dc=org"), + split_cmnd("occ ldap:set-config s01 ldapAgentPassword haze"), + split_cmnd("occ ldap:set-config s01 ldapBase dc=example,dc=org"), + split_cmnd("occ ldap:set-config s01 ldapBaseUsers dc=example,dc=org"), + split_cmnd("occ ldap:set-config s01 ldapBaseGroups dc=example,dc=org"), + split_cmnd("occ ldap:set-config s01 ldapLoginFilter (&(&(objectclass=inetOrgPerson))(uid=%uid))"), + split_cmnd("occ ldap:set-config s01 ldapUserFilter ((objectclass=inetOrgPerson))"), + split_cmnd("occ ldap:set-config s01 ldapUserFilterMode 0"), + split_cmnd("occ ldap:set-config s01 ldapUserDisplayName sn"), + split_cmnd("occ ldap:set-config s01 ldapUserFilterObjectclass inetOrgPerson"), + split_cmnd("occ ldap:set-config s01 ldapGroupFilter (&(|(objectclass=posixGroup)))"), + split_cmnd("occ ldap:set-config s01 ldapGroupFilterObjectclass posixGroup"), + split_cmnd("occ ldap:set-config s01 ldapEmailAttribute email"), + split_cmnd("occ ldap:set-config s01 ldapUuidUserAttribute email"), + split_cmnd("occ ldap:set-config s01 ldapUuidUserAttribute auto"), + split_cmnd("occ ldap:set-config s01 ldapUuidGroupAttribute auto"), + split_cmnd("occ ldap:set-config s01 ldapLoginFilterUsername 1"), + split_cmnd("occ ldap:set-config s01 ldapConfigurationActive 1"), ]) } From d852f9db4fe4d7d7ddbe94a93b07f2771cd69e8a Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 28 May 2026 18:40:20 +0200 Subject: [PATCH 19/20] remove php-smbclient for now as it's broken --- nix/image/php-ext.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/image/php-ext.nix b/nix/image/php-ext.nix index 69d23e1..0a7e53e 100644 --- a/nix/image/php-ext.nix +++ b/nix/image/php-ext.nix @@ -38,7 +38,7 @@ in imagick ] ++ optionals (!debug) [ - smbclient # this breaks the build for no apparent reason + # smbclient # this breaks the build for no apparent reason ] ++ optionals withBlackfire [ blackfire From a3f2355deae7ebd87004fcb4a82588e1b35d7850 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 28 May 2026 18:40:20 +0200 Subject: [PATCH 20/20] create smb/sftp accounts for ldaptest --- src/service/sftp.rs | 5 ++++- src/service/smb.rs | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/service/sftp.rs b/src/service/sftp.rs index c414b16..1f47a40 100644 --- a/src/service/sftp.rs +++ b/src/service/sftp.rs @@ -51,7 +51,10 @@ impl ServiceTrait for Sftp { } }), }), - cmd: Some(vec!["test:test:::data".into()]), + cmd: Some(vec![ + "test:test:::data".into(), + "ldaptest:test:::data".into(), + ]), ..Default::default() }; let id = docker diff --git a/src/service/smb.rs b/src/service/smb.rs index da8f1f6..be1143b 100644 --- a/src/service/smb.rs +++ b/src/service/smb.rs @@ -40,8 +40,10 @@ impl ServiceTrait for Smb { }), env: Some(vec![ "ACCOUNT_test=test".into(), + "ACCOUNT_ldaptest=test".into(), "UID_test=1000".into(), "SAMBA_VOLUME_CONFIG_test=[test]; path=/tmp; valid users = test; guest ok = no; read only = no; browseable = yes".into(), + "SAMBA_VOLUME_CONFIG_ldaptest=[ldaptest]; path=/tmp; valid users = ldaptest; guest ok = no; read only = no; browseable = yes".into(), ]), labels: Some(hashmap! { "haze-type".into() => self.name().into(),