diff --git a/CHANGELOG.md b/CHANGELOG.md index d03244b..b8dc88f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ ## Unreleased +## [0.8.0-beta.2] - 2026-07-06 + +### Fixed + +- pishoo now loads identity access rules from the identity profile access-rule database. + +### Dependencies + +- Release manifests now target `h3x` v0.6.0-beta.2, `dhttp` + v0.5.0-beta.2, and `dshell` v0.6.0-beta.2. + +### Components + +- `gateway` v0.8.0-beta.2 +- `pishoo` v0.8.0-beta.2 +- `pishoo-common` v0.5.1-1 + ## [0.8.0-beta.1] - 2026-07-02 ### Fixed diff --git a/Cargo.toml b/Cargo.toml index 8d54e53..1877ae7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ tower = { version = "0.5", default-features = false, features = ["util"] } tower-service = "0.3" # --- QUIC Implementation --- -h3x = { version = "0.6.0-beta.1" } +h3x = { version = "0.6.0-beta.2" } # --- IPC / RPC --- remoc = { version = "0.18", default-features = false, features = [ @@ -60,7 +60,7 @@ remoc = { version = "0.18", default-features = false, features = [ ] } # --- DShell --- -dshell = { version = "0.6.0-beta.1" } +dshell = { version = "0.6.0-beta.2" } # --- Security & TLS --- getrandom = "0.4" @@ -89,7 +89,7 @@ form_urlencoded = "1" percent-encoding = "2" socket2 = "0.6" x509-parser = "0.18" -dhttp = { version = "0.5.0-beta.1" } +dhttp = { version = "0.5.0-beta.2" } # --- System Interaction --- libc = "0.2" diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index d73f5c4..c01217f 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gateway" -version = "0.8.0-beta.1" +version = "0.8.0-beta.2" edition = "2024" license = "Apache-2.0" diff --git a/pishoo/Cargo.toml b/pishoo/Cargo.toml index 2167156..69302b4 100644 --- a/pishoo/Cargo.toml +++ b/pishoo/Cargo.toml @@ -4,7 +4,7 @@ name = "pishoo" description = "modern, secure, QUIC-powered web/proxy engine" homepage = "https://www.dhttp.net" -version = "0.8.0-beta.1" +version = "0.8.0-beta.2" edition = "2024" license = "Apache-2.0" diff --git a/pishoo/src/service/source.rs b/pishoo/src/service/source.rs index 02e0039..8b55ae5 100644 --- a/pishoo/src/service/source.rs +++ b/pishoo/src/service/source.rs @@ -12,10 +12,10 @@ use gateway::{ }, reverse::router::RouterState, }; -use snafu::{OptionExt, Report, ResultExt, Snafu}; +use snafu::{OptionExt, ResultExt, Snafu}; use super::snapshot::ServerService; -use crate::config::load_identity_servers; +use crate::{config::load_identity_servers, worker::config::BuildConfigError}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct ListenRequestFingerprint { @@ -553,13 +553,47 @@ impl PrepareContext { } } +fn sqlite_access_rules_uri(path: &std::path::Path) -> String { + format!("sqlite://{}?mode=rw", path.display()) +} + +async fn load_identity_access_rules( + target_node: Option<&Arc>, + identity_profile: &IdentityProfile, + fallback: Arc, +) -> Result, BuildConfigError> { + if let Some(uri) = + target_node.and_then(|node| node.get::("access_rules").ok().flatten()) + { + let uri_str = uri.0.to_string(); + return crate::policy::load_policy_bundle(Some(&uri_str)) + .await + .map(|bundle| bundle.location_rules) + .map_err(|source| BuildConfigError::Policy { source }); + } + + let default_access_db = identity_profile.access_db_path(); + match std::fs::metadata(&default_access_db) { + Ok(_) => { + let uri = sqlite_access_rules_uri(&default_access_db); + crate::policy::load_policy_bundle(Some(&uri)) + .await + .map(|bundle| bundle.location_rules) + .map_err(|source| BuildConfigError::Policy { source }) + } + Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(fallback), + Err(source) => Err(BuildConfigError::InspectDefaultAccessRules { + path: default_access_db, + source, + }), + } +} + impl IdentityServiceSource { pub async fn prepare( &self, ctx: &PrepareContext, ) -> Result { - use crate::worker::config::BuildConfigError; - let identity = self .identity_profile .load_identity() @@ -638,27 +672,13 @@ impl IdentityServiceSource { .unwrap_or_default() .as_secs(); - // Worker access control: load rules from identity server.conf if - // access_rules is configured. Falls back to empty rules otherwise. - let access_rules = if let Some(uri) = target_node - .as_ref() - .and_then(|node| node.get::("access_rules").ok().flatten()) - { - let uri_str = uri.0.to_string(); - match crate::policy::load_policy_bundle(Some(&uri_str)).await { - Ok(bundle) => bundle.location_rules, - Err(error) => { - tracing::warn!( - error = %Report::from_error(&error), - uri = %uri_str, - "failed to load access rules from identity config" - ); - ctx.access_rules.clone() - } - } - } else { - ctx.access_rules.clone() - }; + let access_rules = load_identity_access_rules( + target_node.as_ref(), + &self.identity_profile, + ctx.access_rules.clone(), + ) + .await + .context(prepare_server_update_error::IdentityServiceSnafu)?; let server_node = target_node.clone().unwrap_or_else(|| { let registry = gateway::parse::default_registry(); @@ -831,6 +851,209 @@ mod tests { .expect("save identity"); } + async fn write_identity_server_conf(profile: &dhttp::home::identity::IdentityProfile) { + tokio::fs::write( + profile.server_conf_path(), + "server { listen all 0; location / { root .; } }", + ) + .await + .expect("write identity server config"); + } + + async fn write_identity_server_conf_with_access_rules( + profile: &dhttp::home::identity::IdentityProfile, + access_rules: &str, + ) { + tokio::fs::write( + profile.server_conf_path(), + format!( + "server {{ listen all 0; access_rules {access_rules}; location / {{ root .; }} }}" + ), + ) + .await + .expect("write identity server config"); + } + + async fn write_minimal_access_db(profile: &dhttp::home::identity::IdentityProfile) { + use sea_orm::{ConnectionTrait, Database, Statement}; + + let path = profile.access_db_path(); + tokio::fs::create_dir_all(path.parent().expect("access db path has parent")) + .await + .expect("create access db directory"); + + let uri = format!("sqlite://{}?mode=rwc", path.display()); + let db = Database::connect(&uri).await.expect("connect access db"); + db.execute_unprepared( + "CREATE TABLE location_rule_sets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + pattern JSON NOT NULL UNIQUE, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + );", + ) + .await + .expect("create location rule sets"); + db.execute_unprepared( + "CREATE TABLE location_rules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + location_id INTEGER NOT NULL, + action INTEGER NOT NULL, + exprs JSON NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + );", + ) + .await + .expect("create location rules"); + db.execute(Statement::from_string( + sea_orm::DatabaseBackend::Sqlite, + "INSERT INTO location_rule_sets (id, pattern, created_at, updated_at) + VALUES (1, '\"/\"', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')" + .to_string(), + )) + .await + .expect("insert location rule set"); + db.execute(Statement::from_string( + sea_orm::DatabaseBackend::Sqlite, + "INSERT INTO location_rules (location_id, action, exprs, created_at, updated_at) + VALUES (1, 0, '{\"infix\":\"*?\",\"polish\":\"*? \"}', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')" + .to_string(), + )) + .await + .expect("insert access rule"); + } + + fn empty_prepare_context() -> PrepareContext { + PrepareContext { + h3_settings: std::sync::Arc::new(dhttp::h3x::dhttp::settings::Settings::default()), + access_rules: std::sync::Arc::new( + dhttp::access::matcher::LocationRulesMatcher::default(), + ), + router_state: dummy_router_state(), + } + } + + #[tokio::test] + async fn identity_service_prepare_loads_default_access_db_when_it_exists() { + let home = dhttp::home::DhttpHome::new(unique_test_dir("identity-default-access-db")); + let name = fake_name("default-access.dhttp.net"); + let profile = home.identity_profile(name.clone()); + write_identity(&profile, &name).await; + write_identity_server_conf(&profile).await; + write_minimal_access_db(&profile).await; + + let source = IdentityServiceSource { + name: name.clone(), + home, + identity_profile: profile, + }; + + let prepared = source + .prepare(&empty_prepare_context()) + .await + .expect("default access db should load"); + + let (_location, rules) = prepared + .service + .access_rules + .match_rules("/") + .expect("identity default access db should provide root rules"); + assert_eq!(rules.len(), 1); + } + + #[tokio::test] + async fn identity_service_prepare_uses_empty_access_rules_when_default_db_is_missing() { + let home = dhttp::home::DhttpHome::new(unique_test_dir("identity-missing-access-db")); + let name = fake_name("missing-access.dhttp.net"); + let profile = home.identity_profile(name.clone()); + write_identity(&profile, &name).await; + write_identity_server_conf(&profile).await; + + let source = IdentityServiceSource { + name: name.clone(), + home, + identity_profile: profile, + }; + + let prepared = source + .prepare(&empty_prepare_context()) + .await + .expect("missing default access db should fall back to empty rules"); + + assert!( + prepared.service.access_rules.match_rules("/").is_err(), + "missing default access db should not synthesize rules" + ); + } + + #[tokio::test] + async fn identity_service_prepare_errors_when_default_access_db_exists_but_is_invalid() { + let home = dhttp::home::DhttpHome::new(unique_test_dir("identity-invalid-access-db")); + let name = fake_name("invalid-access.dhttp.net"); + let profile = home.identity_profile(name.clone()); + write_identity(&profile, &name).await; + write_identity_server_conf(&profile).await; + let db_path = profile.access_db_path(); + tokio::fs::create_dir_all(db_path.parent().expect("access db path has parent")) + .await + .expect("create access db directory"); + tokio::fs::write(&db_path, b"not sqlite") + .await + .expect("write invalid access db"); + + let source = IdentityServiceSource { + name, + home, + identity_profile: profile, + }; + + let error = match source.prepare(&empty_prepare_context()).await { + Ok(_) => panic!("invalid existing default access db should be an error"), + Err(error) => error, + }; + let report = snafu::Report::from_error(&error).to_string(); + + assert!( + report.contains("failed to load access rules"), + "unexpected error report: {report}" + ); + } + + #[tokio::test] + async fn identity_service_prepare_errors_when_explicit_access_rules_db_is_invalid() { + let home = dhttp::home::DhttpHome::new(unique_test_dir("identity-invalid-explicit-access")); + let name = fake_name("invalid-explicit-access.dhttp.net"); + let profile = home.identity_profile(name.clone()); + write_identity(&profile, &name).await; + write_identity_server_conf_with_access_rules(&profile, "sqlite:./db/access.db?mode=ro") + .await; + let db_path = profile.access_db_path(); + tokio::fs::create_dir_all(db_path.parent().expect("access db path has parent")) + .await + .expect("create access db directory"); + tokio::fs::write(&db_path, b"not sqlite") + .await + .expect("write invalid access db"); + + let source = IdentityServiceSource { + name, + home, + identity_profile: profile, + }; + + let error = match source.prepare(&empty_prepare_context()).await { + Ok(_) => panic!("invalid explicit access_rules db should be an error"), + Err(error) => error, + }; + let report = snafu::Report::from_error(&error).to_string(); + + assert!( + report.contains("failed to load access rules"), + "unexpected error report: {report}" + ); + } + #[tokio::test] async fn pishoo_config_service_loads_identity_from_home_when_tls_is_omitted() { let home = dhttp::home::DhttpHome::new(unique_test_dir("config-service-home")); diff --git a/pishoo/src/worker/config.rs b/pishoo/src/worker/config.rs index 4e7cd7b..6cb843a 100644 --- a/pishoo/src/worker/config.rs +++ b/pishoo/src/worker/config.rs @@ -4,6 +4,8 @@ //! [`IdentityServiceSource`](crate::service::source::IdentityServiceSource) values. //! Runtime preparation happens in [`crate::service::source`]. +use std::path::PathBuf; + use dhttp::home::DhttpHome; use futures::StreamExt; use gateway::error::Whatever; @@ -17,6 +19,11 @@ use crate::policy; pub enum BuildConfigError { #[snafu(transparent)] Whatever { source: Whatever }, + #[snafu(display("failed to inspect default access rules path `{}`", path.display()))] + InspectDefaultAccessRules { + path: PathBuf, + source: std::io::Error, + }, #[snafu(display("failed to load access rules"))] Policy { source: policy::PolicyError }, } diff --git a/xtask/deb/rules b/xtask/deb/rules index 4f0606d..6247c77 100755 --- a/xtask/deb/rules +++ b/xtask/deb/rules @@ -3,9 +3,9 @@ # dpkg-buildpackage calls this via debian/rules. # TRIPLE is passed as an environment variable by xtask (base triple, used for # target/ output paths). ZIG_TARGET is the Rust target passed to -# cargo-zigbuild. The package image pins Zig 0.14, whose GNU target default -# is glibc 2.28, so keep the target unsuffixed to avoid cargo-zigbuild/Zig -# target-version parsing drift. CARGO_FEATURES is +# cargo-zigbuild. The package image pins Zig 0.16; keep GNU targets unsuffixed so +# cargo-zigbuild and Zig own their current default glibc target selection +# instead of mixing stale glibc/kernel suffix syntax into the Rust target. CARGO_FEATURES is # optional. # # This file lives in xtask/deb/ and is copied into target/.../deb/src/debian/ diff --git a/xtask/release/deb/Dockerfile b/xtask/release/deb/Dockerfile index b8bb676..7ba3fad 100644 --- a/xtask/release/deb/Dockerfile +++ b/xtask/release/deb/Dockerfile @@ -29,12 +29,12 @@ RUN set -eux; \ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \ sh -s -- --default-toolchain nightly --profile minimal -y; \ if [ "$XTASK_RELEASE_TARGET" != "common" ]; then rustup target add "$XTASK_RELEASE_TARGET"; fi; \ - wget -q https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz; \ - tar -xf zig-linux-x86_64-0.14.0.tar.xz; \ - mv zig-linux-x86_64-0.14.0 /usr/local/zig; \ + wget -q https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz; \ + tar -xf zig-x86_64-linux-0.16.0.tar.xz; \ + mv zig-x86_64-linux-0.16.0 /usr/local/zig; \ ln -s /usr/local/zig/zig /usr/local/bin/zig; \ - rm zig-linux-x86_64-0.14.0.tar.xz; \ - cargo install cargo-zigbuild; \ + rm zig-x86_64-linux-0.16.0.tar.xz; \ + cargo install --locked cargo-zigbuild; \ chmod -R a+rX /opt/cargo /opt/rustup COPY xtask/release/deb/package.sh /opt/genmeta-release/package diff --git a/xtask/release/rpm/Dockerfile b/xtask/release/rpm/Dockerfile index bf8d8e2..b75ef4b 100644 --- a/xtask/release/rpm/Dockerfile +++ b/xtask/release/rpm/Dockerfile @@ -20,12 +20,12 @@ RUN set -eux; \ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \ sh -s -- --default-toolchain nightly --profile minimal -y; \ rustup target add "$XTASK_RELEASE_TARGET"; \ - wget -q https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz; \ - tar -xf zig-linux-x86_64-0.14.0.tar.xz; \ - mv zig-linux-x86_64-0.14.0 /usr/local/zig; \ + wget -q https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz; \ + tar -xf zig-x86_64-linux-0.16.0.tar.xz; \ + mv zig-x86_64-linux-0.16.0 /usr/local/zig; \ ln -s /usr/local/zig/zig /usr/local/bin/zig; \ - rm zig-linux-x86_64-0.14.0.tar.xz; \ - cargo install cargo-zigbuild; \ + rm zig-x86_64-linux-0.16.0.tar.xz; \ + cargo install --locked cargo-zigbuild; \ fi; \ chmod -R a+rX /opt/cargo /opt/rustup || true