From e39314c3f1bd7b0f785422c70a8f5ff674599151 Mon Sep 17 00:00:00 2001 From: eareimu Date: Tue, 7 Jul 2026 20:29:14 +0800 Subject: [PATCH 1/8] ci: upload package assets from publish reports --- .github/workflows/release.yml | 69 +++++++++++++++++++++++++++-------- xtask/Cargo.toml | 2 +- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index afcbc40..7b433be 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -224,7 +224,7 @@ jobs: if [[ "$mode" == "dry-run" ]]; then publish_cmd+=(--dry-run) fi - "${publish_cmd[@]}" deb + "${publish_cmd[@]}" --publish-report target/publish-reports/deb.toml deb - name: Upload deb packages to GitHub Release if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v') @@ -232,11 +232,23 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - shopt -s nullglob - assets=(target/common/deb/*.deb target/*/release/deb/*.deb) + report=target/publish-reports/deb.toml + test -f "$report" + mapfile -t assets < <(python3 - "$report" <<'PY' + import sys + import tomllib + + with open(sys.argv[1], "rb") as handle: + report = tomllib.load(handle) + + for manifest in report.get("manifests", []): + for asset in manifest.get("artifacts", []): + print(asset["local-path"]) + PY + ) if [ "${#assets[@]}" -eq 0 ]; then - echo "no deb release assets found" >&2 - exit 1 + echo "no deb release assets selected by publish report; skipping GitHub Release upload" + exit 0 fi for attempt in {1..30}; do if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then @@ -258,6 +270,7 @@ jobs: if-no-files-found: ignore path: | gateway/target/common/deb/** + gateway/target/publish-reports/*.toml linux-rpm: needs: github-release @@ -369,7 +382,7 @@ jobs: if [[ "$mode" == "dry-run" ]]; then publish_cmd+=(--dry-run) fi - "${publish_cmd[@]}" rpm + "${publish_cmd[@]}" --publish-report target/publish-reports/rpm.toml rpm - name: Upload rpm packages to GitHub Release if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v') @@ -377,11 +390,23 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - shopt -s nullglob - assets=(target/common/rpm/*.rpm target/*/release/rpm/*.rpm) + report=target/publish-reports/rpm.toml + test -f "$report" + mapfile -t assets < <(python3 - "$report" <<'PY' + import sys + import tomllib + + with open(sys.argv[1], "rb") as handle: + report = tomllib.load(handle) + + for manifest in report.get("manifests", []): + for asset in manifest.get("artifacts", []): + print(asset["local-path"]) + PY + ) if [ "${#assets[@]}" -eq 0 ]; then - echo "no rpm release assets found" >&2 - exit 1 + echo "no rpm release assets selected by publish report; skipping GitHub Release upload" + exit 0 fi for attempt in {1..30}; do if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then @@ -403,6 +428,7 @@ jobs: if-no-files-found: ignore path: | gateway/target/common/rpm/** + gateway/target/publish-reports/*.toml homebrew: needs: github-release @@ -519,7 +545,7 @@ jobs: if [[ "$mode" == "dry-run" ]]; then publish_cmd+=(--dry-run) fi - "${publish_cmd[@]}" brew + "${publish_cmd[@]}" --publish-report target/publish-reports/brew.toml brew - name: Create Homebrew tap pull request if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v') @@ -578,11 +604,23 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - shopt -s nullglob - assets=(target/*/release/brew/*.tar.gz target/common/brew/pishoo-*.rb) + report=target/publish-reports/brew.toml + test -f "$report" + mapfile -t assets < <(python3 - "$report" <<'PY' + import sys + import tomllib + + with open(sys.argv[1], "rb") as handle: + report = tomllib.load(handle) + + for manifest in report.get("manifests", []): + for asset in manifest.get("artifacts", []): + print(asset["local-path"]) + PY + ) if [ "${#assets[@]}" -eq 0 ]; then - echo "no Homebrew release assets found" >&2 - exit 1 + echo "no Homebrew release assets selected by publish report; skipping GitHub Release upload" + exit 0 fi for attempt in {1..30}; do if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then @@ -604,3 +642,4 @@ jobs: if-no-files-found: ignore path: | gateway/target/common/brew/** + gateway/target/publish-reports/*.toml diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 999d1b8..5e65816 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -8,5 +8,5 @@ publish = false resolver = "3" [dependencies] -genmeta-xtask-release = { git = "https://github.com/genmeta/genmeta-xtask-release.git", tag = "v0.2.0-beta.7", version = "0.2.0-beta.7" } +genmeta-xtask-release = { git = "https://github.com/genmeta/genmeta-xtask-release.git", tag = "v0.2.0-beta.8", version = "0.2.0-beta.8" } snafu = "0.9" From 82ab5fe523c57e42103827ab6df30de26579f7be Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 8 Jul 2026 17:54:35 +0800 Subject: [PATCH 2/8] chore: align dhttp facade dependency --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1877ae7..a3d9a76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,7 +89,7 @@ form_urlencoded = "1" percent-encoding = "2" socket2 = "0.6" x509-parser = "0.18" -dhttp = { version = "0.5.0-beta.2" } +dhttp = { version = "0.5.0-beta.3" } # --- System Interaction --- libc = "0.2" From 78e5234a5007b2970f2116d3aebd8fbc9728d148 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 8 Jul 2026 20:33:25 +0800 Subject: [PATCH 3/8] feat(gateway): evaluate access rules through policy trait --- gateway/src/reverse/access_control.rs | 112 +++++++++++++++++++++----- 1 file changed, 91 insertions(+), 21 deletions(-) diff --git a/gateway/src/reverse/access_control.rs b/gateway/src/reverse/access_control.rs index b596831..1bc5173 100644 --- a/gateway/src/reverse/access_control.rs +++ b/gateway/src/reverse/access_control.rs @@ -7,15 +7,17 @@ use axum::{ }; use dhttp::{ access::{ + action::RequestAction, expr::{ atomics::{AtomicLocationRuleExpr, EvalError}, eval::Evaluable, }, - matcher::{LocationRulesMatcher, MatchRuleFailed}, pattern::NormalPattern, + policy::{LocationRuleDecisionError, LocationRuleEvaluator, LocationRuleRequest}, }, h3x::{connection::ConnectionState, quic}, }; +use snafu::Report; use http::{HeaderMap, HeaderValue, Method, StatusCode}; use tracing::{info, warn}; @@ -47,24 +49,22 @@ impl<'a> AccessHttpRequest<'a> { } } -impl Evaluable> for AtomicLocationRuleExpr { - type Value = Result; - - fn eval(&self, request: &AccessHttpRequest<'_>) -> Self::Value { - Ok(match self { - Self::Any(..) => true, - Self::ClientName(pattern) => pattern.eval(&request.client_name)?, - Self::Method(method) => { +impl LocationRuleRequest for AccessHttpRequest<'_> { + fn eval_atomic(&self, expr: &AtomicLocationRuleExpr) -> Result { + Ok(match expr { + AtomicLocationRuleExpr::Any(..) => true, + AtomicLocationRuleExpr::ClientName(pattern) => pattern.eval(&self.client_name)?, + AtomicLocationRuleExpr::Method(method) => { let pattern: &NormalPattern = method.as_ref(); - pattern.eval(&request.method.as_str()) + pattern.eval(&self.method.as_str()) } - Self::Header(header) => request.headers.iter().any(|(key, value)| { + AtomicLocationRuleExpr::Header(header) => self.headers.iter().any(|(key, value)| { let Ok(value) = value.to_str() else { return false; }; header.eval(&(key.as_str(), value)) }), - Self::Query(query) => request.queries.iter().any(|pair| query.eval(pair)), + AtomicLocationRuleExpr::Query(query) => self.queries.iter().any(|pair| query.eval(pair)), }) } } @@ -72,7 +72,7 @@ impl Evaluable> for AtomicLocationRuleExpr { /// Shared state for the access control middleware. #[derive(Clone)] pub struct AccessControlState { - pub access_rules: Arc, + pub access_rules: Arc, pub server_name: Arc, } @@ -105,37 +105,46 @@ pub async fn access_control( let action = match state .access_rules - .match_rule(request.uri().path(), &http_request) + .evaluate(request.uri().path(), &http_request) + .await { - Ok((_location, action)) => action, - Err(MatchRuleFailed::MatchSet { .. }) => { + Ok(decision) => decision.action, + Err(LocationRuleDecisionError::NoRuleSet { .. }) => { // No ruleset matched the path — allow the server itself, deny others. if client_name.as_deref() == Some(&*state.server_name) { warn!( path = %request.uri().path(), "no ruleset matched, allowing self only" ); - dhttp::access::action::RequestAction::Allow + RequestAction::Allow } else { warn!( path = %request.uri().path(), client = client_name.as_deref(), "no ruleset matched, denying non-self client" ); - dhttp::access::action::RequestAction::Deny + RequestAction::Deny } } - Err(MatchRuleFailed::MatchRuleInSet) => { + Err(LocationRuleDecisionError::NoRuleInSet { .. }) => { // Ruleset matched but no rule matched — deny everyone. warn!( path = %request.uri().path(), "ruleset matched but no rule matched, denying all" ); - dhttp::access::action::RequestAction::Deny + RequestAction::Deny + } + Err(error @ LocationRuleDecisionError::Backend { .. }) => { + warn!( + path = %request.uri().path(), + error = %Report::from_error(&error), + "failed to evaluate access rules, denying request" + ); + RequestAction::Deny } }; - if action == dhttp::access::action::RequestAction::Deny { + if action == RequestAction::Deny { info!( client_name = client_name.as_deref(), uri = %request.uri(), @@ -146,3 +155,64 @@ pub async fn access_control( next.run(request).await } + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use axum::{body::Body, middleware::from_fn_with_state, routing::get, Router}; + use dhttp::access::{ + expr::atomics::AtomicLocationRuleExpr, + policy::{ + LocationRuleDecisionError, LocationRuleEvaluator, LocationRuleFuture, + LocationRuleRequest, + }, + }; + use http::{Request, StatusCode}; + use tower::ServiceExt; + + use super::AccessControlState; + + #[derive(Debug, snafu::Snafu)] + #[snafu(display("synthetic evaluator failure"))] + struct SyntheticEvaluatorError; + + struct FailingEvaluator; + + impl LocationRuleEvaluator for FailingEvaluator { + fn evaluate<'a>( + &'a self, + _path: &'a str, + _request: &'a (dyn LocationRuleRequest + Send + Sync), + ) -> LocationRuleFuture<'a> { + Box::pin(async move { Err(LocationRuleDecisionError::backend(SyntheticEvaluatorError)) }) + } + } + + #[tokio::test] + async fn backend_error_fails_closed_with_forbidden() { + let state = AccessControlState { + access_rules: Arc::new(FailingEvaluator), + server_name: Arc::from("server.pilot.dhttp.net"), + }; + let app = Router::new() + .route("/", get(|| async { "ok" })) + .layer(from_fn_with_state(state, super::access_control)); + + let response = app + .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::FORBIDDEN); + } + + #[test] + fn access_http_request_evaluates_any_client() { + let request = Request::builder().uri("/").body(()).unwrap(); + let access_request = super::AccessHttpRequest::new(None, &request); + let expr: AtomicLocationRuleExpr = "*?".parse().unwrap(); + + assert!(access_request.eval_atomic(&expr).unwrap()); + } +} From 070bc5d7dc32e0eabbb0d1c92496dc2d39729486 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 8 Jul 2026 20:39:59 +0800 Subject: [PATCH 4/8] feat(pishoo): keep access DB as live policy source --- pishoo/Cargo.toml | 2 +- pishoo/src/policy.rs | 318 +++++++++++++++------------------ pishoo/src/service/snapshot.rs | 2 +- pishoo/src/service/source.rs | 41 ++++- 4 files changed, 174 insertions(+), 189 deletions(-) diff --git a/pishoo/Cargo.toml b/pishoo/Cargo.toml index f52dfa4..4c21a91 100644 --- a/pishoo/Cargo.toml +++ b/pishoo/Cargo.toml @@ -24,7 +24,7 @@ required-features = ["sshd"] [dependencies] # --- Workspace Member --- gateway = { workspace = true } -dhttp = { workspace = true } +dhttp = { workspace = true, features = ["access-orm"] } h3x = { workspace = true, features = ["ipc"] } rustls = { workspace = true } rustls-pemfile = "2" diff --git a/pishoo/src/policy.rs b/pishoo/src/policy.rs index dc7cb88..884cc2c 100644 --- a/pishoo/src/policy.rs +++ b/pishoo/src/policy.rs @@ -1,18 +1,26 @@ use std::sync::Arc; use dhttp::access::{ - action::RequestAction, - expr::exprs::LocationRuleExprs, - matcher::{LocationRulesMatcher, PatternWithTime}, - pattern::{LocationPattern, LocationPatternKind}, + db::{ + evaluator::LocationRulesDatabase, + service::{error::EnsureStoreError, location_service::LocationService}, + }, + matcher::LocationRulesMatcher, + policy::LocationRuleEvaluator, }; -use sea_orm::{ConnectionTrait, DatabaseBackend, QueryResult, Statement}; -use serde::de::DeserializeOwned; use snafu::{ResultExt, Snafu}; -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct PolicyBundle { - pub location_rules: Arc, + pub location_rules: Arc, +} + +impl std::fmt::Debug for PolicyBundle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PolicyBundle") + .field("location_rules", &"") + .finish() + } } impl Default for PolicyBundle { @@ -28,196 +36,150 @@ pub enum PolicyError { #[snafu(display("failed to connect access_rules database `{uri}`"))] ConnectDb { uri: String, source: sea_orm::DbErr }, - #[snafu(display("failed to query access rule locations from `{uri}`"))] - QueryLocations { uri: String, source: sea_orm::DbErr }, - - #[snafu(display("failed to query access rules from `{uri}`"))] - QueryRules { uri: String, source: sea_orm::DbErr }, - - #[snafu(display("failed to read access rule location id from `{uri}`"))] - ReadLocationId { uri: String, source: sea_orm::DbErr }, - - #[snafu(display("failed to read access rule location pattern from `{uri}`"))] - ReadLocationPattern { - uri: String, - location_id: i32, - source: sea_orm::DbErr, - }, - - #[snafu(display("failed to decode access rule location pattern from `{uri}`"))] - DecodeLocationPattern { - uri: String, - location_id: i32, - source: serde_json::Error, - }, - - #[snafu(display("failed to read access rule id from `{uri}`"))] - ReadRuleId { - uri: String, - location_id: i32, - source: sea_orm::DbErr, - }, - - #[snafu(display("failed to read access rule action from `{uri}`"))] - ReadRuleAction { - uri: String, - rule_id: i32, - source: sea_orm::DbErr, - }, - - #[snafu(display("unexpected access rule action {action} in `{uri}`"))] - InvalidRuleAction { - uri: String, - rule_id: i32, - action: i32, - }, - - #[snafu(display("failed to read access rule expression from `{uri}`"))] - ReadRuleExprs { - uri: String, - rule_id: i32, - source: sea_orm::DbErr, - }, - - #[snafu(display("failed to decode access rule expression from `{uri}`"))] - DecodeRuleExprs { - uri: String, - rule_id: i32, - source: serde_json::Error, - }, + #[snafu(display("failed to validate access_rules database `{uri}`"))] + ValidateDb { uri: String, source: EnsureStoreError }, } -fn decode_json(value: serde_json::Value) -> Result -where - T: DeserializeOwned, -{ - serde_json::from_value(value) -} - -fn location_id(row: &QueryResult, uri: &str) -> Result { - row.try_get("", "id").context(ReadLocationIdSnafu { - uri: uri.to_string(), - }) -} +pub async fn load_policy_bundle(uri: Option<&str>) -> Result { + let Some(uri) = uri else { + return Ok(PolicyBundle::default()); + }; -fn location_pattern( - row: &QueryResult, - uri: &str, - location_id: i32, -) -> Result { - let value = row - .try_get("", "pattern") - .context(ReadLocationPatternSnafu { + let db = sea_orm::Database::connect(uri) + .await + .context(ConnectDbSnafu { + uri: uri.to_string(), + })?; + LocationService::new(&db) + .ensure_store() + .await + .context(ValidateDbSnafu { uri: uri.to_string(), - location_id, })?; - decode_json(value).context(DecodeLocationPatternSnafu { - uri: uri.to_string(), - location_id, - }) -} -fn rule_id(row: &QueryResult, uri: &str, location_id: i32) -> Result { - row.try_get("", "id").context(ReadRuleIdSnafu { - uri: uri.to_string(), - location_id, + Ok(PolicyBundle { + location_rules: Arc::new(LocationRulesDatabase::new(db)), }) } -fn rule_action(row: &QueryResult, uri: &str, rule_id: i32) -> Result { - let action = row.try_get("", "action").context(ReadRuleActionSnafu { - uri: uri.to_string(), - rule_id, - })?; - match action { - 0 => Ok(RequestAction::Allow), - 1 => Ok(RequestAction::Deny), - action => InvalidRuleActionSnafu { - uri: uri.to_string(), - rule_id, - action, +#[cfg(test)] +mod tests { + use dhttp::access::{ + action::RequestAction, + db::evaluator::LocationRulesDatabase, + expr::atomics::{AtomicLocationRuleExpr, EvalError}, + policy::LocationRuleRequest, + }; + use sea_orm::{ConnectionTrait, DatabaseBackend, Statement}; + + struct TestRequest; + + impl LocationRuleRequest for TestRequest { + fn eval_atomic(&self, expr: &AtomicLocationRuleExpr) -> Result { + use dhttp::access::expr::eval::Evaluable; + Ok(match expr { + AtomicLocationRuleExpr::Any(..) => true, + AtomicLocationRuleExpr::ClientName(pattern) => { + pattern.eval(&Some("alice.pilot.dhttp.net"))? + } + AtomicLocationRuleExpr::Method(_) => false, + AtomicLocationRuleExpr::Header(_) => false, + AtomicLocationRuleExpr::Query(_) => false, + }) } - .fail(), } -} -fn rule_exprs( - row: &QueryResult, - uri: &str, - rule_id: i32, -) -> Result { - let value = row.try_get("", "exprs").context(ReadRuleExprsSnafu { - uri: uri.to_string(), - rule_id, - })?; - decode_json(value).context(DecodeRuleExprsSnafu { - uri: uri.to_string(), - rule_id, - }) -} + async fn create_minimal_access_schema(db: &sea_orm::DatabaseConnection) { + db.execute(Statement::from_string( + DatabaseBackend::Sqlite, + "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)".to_owned(), + )) + .await + .expect("create location rule sets"); + db.execute(Statement::from_string( + DatabaseBackend::Sqlite, + "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, FOREIGN KEY(location_id) REFERENCES location_rule_sets(id) ON DELETE CASCADE)".to_owned(), + )) + .await + .expect("create location rules"); + db.execute(Statement::from_string( + 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_owned(), + )) + .await + .expect("insert root location"); + } -async fn load_location_rules( - database: &sea_orm::DatabaseConnection, - uri: &str, -) -> Result { - let locations = database - .query_all(Statement::from_string( + async fn replace_root_rule(db: &sea_orm::DatabaseConnection, action: i32, expr: &str) { + db.execute(Statement::from_string( DatabaseBackend::Sqlite, - "SELECT id, pattern FROM location_rule_sets".to_string(), + "DELETE FROM location_rules WHERE location_id = 1".to_owned(), )) .await - .context(QueryLocationsSnafu { - uri: uri.to_string(), - })?; + .expect("clear root rules"); + let expr_json = serde_json::json!({ + "infix": expr, + "polish": format!("{expr} "), + }) + .to_string() + .replace('\'', "''"); + db.execute(Statement::from_string( + DatabaseBackend::Sqlite, + format!( + "INSERT INTO location_rules (location_id, action, exprs, created_at, updated_at) VALUES (1, {action}, '{expr_json}', '2026-01-01T00:00:01Z', '2026-01-01T00:00:01Z')" + ), + )) + .await + .expect("insert root rule"); + } - let mut matcher = LocationRulesMatcher::default(); - for location in locations { - let location_id = location_id(&location, uri)?; - let pattern = location_pattern(&location, uri, location_id)?; - let rules = database - .query_all(Statement::from_sql_and_values( - DatabaseBackend::Sqlite, - "SELECT id, action, exprs FROM location_rules WHERE location_id = ? ORDER BY created_at ASC", - [location_id.into()], - )) + #[tokio::test] + async fn policy_bundle_uses_live_database_evaluator() { + let path = std::env::temp_dir().join(format!( + "pishoo-policy-live-db-{}.db", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let uri = format!("sqlite://{}?mode=rwc", path.display()); + let db = sea_orm::Database::connect(&uri) .await - .context(QueryRulesSnafu { - uri: uri.to_string(), - })?; - - let rules = rules - .into_iter() - .map(|row| { - let rule_id = rule_id(&row, uri, location_id)?; - Ok(( - rule_exprs(&row, uri, rule_id)?, - rule_action(&row, uri, rule_id)?, - )) - }) - .collect::, PolicyError>>()?; + .expect("create access db"); + create_minimal_access_schema(&db).await; + replace_root_rule(&db, 0, "*?").await; + drop(db); - matcher.map.insert( - PatternWithTime::::new(location_id.into(), pattern), - rules, + let uri = format!("sqlite://{}?mode=rw", path.display()); + let bundle = super::load_policy_bundle(Some(&uri)) + .await + .expect("load policy bundle"); + let request = TestRequest; + + assert_eq!( + bundle + .location_rules + .evaluate("/", &request) + .await + .expect("policy should decide") + .action, + RequestAction::Allow ); - } - - Ok(matcher) -} - -pub async fn load_policy_bundle(uri: Option<&str>) -> Result { - let Some(uri) = uri else { - return Ok(PolicyBundle::default()); - }; - let db = sea_orm::Database::connect(uri) - .await - .context(ConnectDbSnafu { - uri: uri.to_string(), - })?; - let location_rules = load_location_rules(&db, uri).await?; + let db = sea_orm::Database::connect(&uri).await.expect("reopen db"); + replace_root_rule(&db, 1, "*?").await; + + assert_eq!( + bundle + .location_rules + .evaluate("/", &request) + .await + .expect("policy should see updated DB") + .action, + RequestAction::Deny + ); - Ok(PolicyBundle { - location_rules: Arc::new(location_rules), - }) + let _ = std::fs::remove_file(path); + let _type_check: Option = None; + } } diff --git a/pishoo/src/service/snapshot.rs b/pishoo/src/service/snapshot.rs index 1fc19fe..fb7278e 100644 --- a/pishoo/src/service/snapshot.rs +++ b/pishoo/src/service/snapshot.rs @@ -19,7 +19,7 @@ use tracing::Instrument; pub struct ServerService { pub h3_settings: Arc, - pub access_rules: Arc, + pub access_rules: Arc, pub router_state: gateway::reverse::router::RouterState, pub server_node: Arc, pub access_log_dir: Option, diff --git a/pishoo/src/service/source.rs b/pishoo/src/service/source.rs index 8b55ae5..c999e25 100644 --- a/pishoo/src/service/source.rs +++ b/pishoo/src/service/source.rs @@ -17,6 +17,9 @@ use snafu::{OptionExt, ResultExt, Snafu}; use super::snapshot::ServerService; use crate::{config::load_identity_servers, worker::config::BuildConfigError}; +type SharedLocationRuleEvaluator = + Arc; + #[derive(Debug, Clone, PartialEq, Eq)] pub struct ListenRequestFingerprint { pub server_name: DhttpName<'static>, @@ -65,7 +68,7 @@ pub enum ServerSource { pub struct PrepareContext { pub h3_settings: Arc, - pub access_rules: Arc, + pub access_rules: SharedLocationRuleEvaluator, pub router_state: gateway::reverse::router::RouterState, } @@ -560,8 +563,8 @@ fn sqlite_access_rules_uri(path: &std::path::Path) -> String { async fn load_identity_access_rules( target_node: Option<&Arc>, identity_profile: &IdentityProfile, - fallback: Arc, -) -> Result, BuildConfigError> { + fallback: SharedLocationRuleEvaluator, +) -> Result { if let Some(uri) = target_node.and_then(|node| node.get::("access_rules").ok().flatten()) { @@ -749,10 +752,24 @@ fn hex_lower(bytes: &[u8]) -> String { #[cfg(test)] mod tests { - use dhttp::name::DhttpName; + use dhttp::{ + access::{ + expr::atomics::{AtomicLocationRuleExpr, EvalError}, + policy::LocationRuleRequest, + }, + name::DhttpName, + }; use super::*; + struct TestAccessRequest; + + impl LocationRuleRequest for TestAccessRequest { + fn eval_atomic(&self, expr: &AtomicLocationRuleExpr) -> Result { + Ok(matches!(expr, AtomicLocationRuleExpr::Any(..))) + } + } + #[test] fn fingerprint_incorporates_cert_and_key() { let name = DhttpName::try_from("test.dhttp.net".to_owned()).unwrap(); @@ -954,12 +971,13 @@ mod tests { .await .expect("default access db should load"); - let (_location, rules) = prepared + let decision = prepared .service .access_rules - .match_rules("/") - .expect("identity default access db should provide root rules"); - assert_eq!(rules.len(), 1); + .evaluate("/", &TestAccessRequest) + .await + .expect("identity default access db should provide root rule decision"); + assert_eq!(decision.location.to_string(), "/"); } #[tokio::test] @@ -982,7 +1000,12 @@ mod tests { .expect("missing default access db should fall back to empty rules"); assert!( - prepared.service.access_rules.match_rules("/").is_err(), + prepared + .service + .access_rules + .evaluate("/", &TestAccessRequest) + .await + .is_err(), "missing default access db should not synthesize rules" ); } From 4e361f3015931ce1c77121c7147205161f394792 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 8 Jul 2026 20:50:21 +0800 Subject: [PATCH 5/8] fix(pishoo): format access policy integration --- gateway/src/reverse/access_control.rs | 12 ++++++++---- pishoo/src/policy.rs | 5 ++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/gateway/src/reverse/access_control.rs b/gateway/src/reverse/access_control.rs index 1bc5173..d66fcba 100644 --- a/gateway/src/reverse/access_control.rs +++ b/gateway/src/reverse/access_control.rs @@ -17,8 +17,8 @@ use dhttp::{ }, h3x::{connection::ConnectionState, quic}, }; -use snafu::Report; use http::{HeaderMap, HeaderValue, Method, StatusCode}; +use snafu::Report; use tracing::{info, warn}; struct AccessHttpRequest<'a> { @@ -64,7 +64,9 @@ impl LocationRuleRequest for AccessHttpRequest<'_> { }; header.eval(&(key.as_str(), value)) }), - AtomicLocationRuleExpr::Query(query) => self.queries.iter().any(|pair| query.eval(pair)), + AtomicLocationRuleExpr::Query(query) => { + self.queries.iter().any(|pair| query.eval(pair)) + } }) } } @@ -160,7 +162,7 @@ pub async fn access_control( mod tests { use std::sync::Arc; - use axum::{body::Body, middleware::from_fn_with_state, routing::get, Router}; + use axum::{Router, body::Body, middleware::from_fn_with_state, routing::get}; use dhttp::access::{ expr::atomics::AtomicLocationRuleExpr, policy::{ @@ -185,7 +187,9 @@ mod tests { _path: &'a str, _request: &'a (dyn LocationRuleRequest + Send + Sync), ) -> LocationRuleFuture<'a> { - Box::pin(async move { Err(LocationRuleDecisionError::backend(SyntheticEvaluatorError)) }) + Box::pin( + async move { Err(LocationRuleDecisionError::backend(SyntheticEvaluatorError)) }, + ) } } diff --git a/pishoo/src/policy.rs b/pishoo/src/policy.rs index 884cc2c..eb16c92 100644 --- a/pishoo/src/policy.rs +++ b/pishoo/src/policy.rs @@ -37,7 +37,10 @@ pub enum PolicyError { ConnectDb { uri: String, source: sea_orm::DbErr }, #[snafu(display("failed to validate access_rules database `{uri}`"))] - ValidateDb { uri: String, source: EnsureStoreError }, + ValidateDb { + uri: String, + source: EnsureStoreError, + }, } pub async fn load_policy_bundle(uri: Option<&str>) -> Result { From d09cf7f4ec2fb5a1208c5cce1eab141bac34fd61 Mon Sep 17 00:00:00 2001 From: eareimu Date: Thu, 9 Jul 2026 01:32:15 +0800 Subject: [PATCH 6/8] chore: prepare pishoo v0.8.0-beta.4 --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ Cargo.lock | 42 ++++++++++++++++++------------------------ Cargo.toml | 2 +- gateway/Cargo.toml | 2 +- pishoo/Cargo.toml | 2 +- 5 files changed, 54 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17a350a..ef3e706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,39 @@ ## Unreleased +## [0.8.0-beta.4] - 2026-07-09 + +### Added + +- pishoo now keeps identity access-rule databases as live policy sources instead + of loading only a static in-memory snapshot. +- Gateway and pishoo access checks now evaluate rules through the shared + `dhttp` access policy evaluator trait. + +### Changed + +- pishoo enables the `dhttp` access ORM facade feature for access-rule database + evaluation. + +### Fixed + +- Release workflows upload package assets from publish reports instead of broad + local artifact globs. + +### Dependencies + +- Release manifests now target `dhttp` v0.5.0-beta.3, `dhttp-access` + v0.4.0-beta.1 through the `dhttp` facade, `dhttp-home` + v0.4.0-beta.1, `dhttp-identity` v0.3.0-beta.1, `dyns` + v0.6.0-beta.3, `h3x` v0.6.0-beta.3, and `dshell` + v0.6.0-beta.2. + +### Components + +- `gateway` v0.8.0-beta.4 +- `pishoo` v0.8.0-beta.4 +- `pishoo-common` v0.5.1-1 + ## [0.8.0-beta.3] - 2026-07-06 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 9b30524..a47a478 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1019,9 +1019,7 @@ dependencies = [ [[package]] name = "dhttp" -version = "0.5.0-beta.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2448459a33f3c10a4df05effd62160e8cbacf19ca81ba640db86ec4b61f3d86e" +version = "0.5.0-beta.3" dependencies = [ "bon", "bytes", @@ -1042,25 +1040,27 @@ dependencies = [ [[package]] name = "dhttp-access" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5dee91d9ec4bf5f8fbd49a2761b1c5454aa9b088f6c53fc278f7b7934de9f38" +version = "0.4.0-beta.1" dependencies = [ + "chrono", "derive_more", + "dhttp-home", "dhttp-identity", "globset", "peg", "regex", + "regex-automata", + "sea-orm", "serde", "serde_json", "snafu", + "tokio", + "tracing", ] [[package]] name = "dhttp-home" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81393cfab5863c0f1b7994c5da57c0a1e941cb62886e558c4832cfea37d1ea68" +version = "0.4.0-beta.1" dependencies = [ "dhttp-identity", "dirs", @@ -1075,9 +1075,7 @@ dependencies = [ [[package]] name = "dhttp-identity" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9006fe9bffd56d64fbe4260182039f104a84d612acdd30ec19a16583bb4ff671" +version = "0.3.0-beta.1" dependencies = [ "bytes", "futures", @@ -1223,9 +1221,7 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "dyns" -version = "0.6.0-beta.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a69fa5f6ff05223697e193821019349294d1173f4dbe29fb5d2b1b27193df67" +version = "0.6.0-beta.3" dependencies = [ "base64 0.22.1", "bitfield-struct", @@ -1509,7 +1505,7 @@ dependencies = [ [[package]] name = "gateway" -version = "0.8.0-beta.3" +version = "0.8.0-beta.4" dependencies = [ "async-compression", "axum", @@ -1668,9 +1664,7 @@ dependencies = [ [[package]] name = "h3x" -version = "0.6.0-beta.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9c036f15ff331def2cdb2f12428b7d62ec0ad7e8f3f0a6ff014c570cdb33853" +version = "0.6.0-beta.3" dependencies = [ "arc-swap", "async-channel", @@ -2952,7 +2946,7 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pishoo" -version = "0.8.0-beta.3" +version = "0.8.0-beta.4" dependencies = [ "axum", "clap", @@ -3440,9 +3434,9 @@ dependencies = [ [[package]] name = "qtraversal" -version = "0.7.0-beta.1" +version = "0.7.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe3e0de35ae950fa5fbe33799cdf10c80b6bce39afddd89b52f52720435fe3f" +checksum = "16fc8219fe16d92319e4ccd1c41ea87661a27abfacc4d40e77273b55fa384cf9" dependencies = [ "bon", "bytes", @@ -4504,7 +4498,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f103c50866b8743da9429b8a581d81a27c2d3a9c4ac7df8f8571c1dd7896eda" dependencies = [ - "heck 0.5.0", + "heck 0.4.1", "proc-macro2", "quote", "syn 2.0.118", @@ -5535,7 +5529,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a3d9a76..6cd6179 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.2" } +h3x = { version = "0.6.0-beta.3" } # --- IPC / RPC --- remoc = { version = "0.18", default-features = false, features = [ diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 832e7c4..30f788e 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gateway" -version = "0.8.0-beta.3" +version = "0.8.0-beta.4" edition = "2024" license = "Apache-2.0" diff --git a/pishoo/Cargo.toml b/pishoo/Cargo.toml index 4c21a91..f47a780 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.3" +version = "0.8.0-beta.4" edition = "2024" license = "Apache-2.0" From b09e2a99ccd08885dfad03ba75e1542f8818d9aa Mon Sep 17 00:00:00 2001 From: eareimu Date: Thu, 9 Jul 2026 15:17:50 +0800 Subject: [PATCH 7/8] chore: converge pishoo release lockfile --- Cargo.lock | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a47a478..a7f0a24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1020,6 +1020,8 @@ dependencies = [ [[package]] name = "dhttp" version = "0.5.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dedfca271d296f9c0d9fee052d337495aacb4d1b49eb4b29f09af9cd446757b1" dependencies = [ "bon", "bytes", @@ -1041,6 +1043,8 @@ dependencies = [ [[package]] name = "dhttp-access" version = "0.4.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbcd9d2805ace5bcac3c112e14b9df13d02f38f2a8a77897acb1cec536fe0715" dependencies = [ "chrono", "derive_more", @@ -1061,6 +1065,8 @@ dependencies = [ [[package]] name = "dhttp-home" version = "0.4.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af7727cd3f09f4f81bb928c3f7e77587a1a622796413bcbb8348e9a51b24f49" dependencies = [ "dhttp-identity", "dirs", @@ -1076,6 +1082,8 @@ dependencies = [ [[package]] name = "dhttp-identity" version = "0.3.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45559fec32be857d9d8db20dbb7f2a728e0b8a8a2cacfaf8480df6b1ae43c318" dependencies = [ "bytes", "futures", @@ -1222,6 +1230,8 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "dyns" version = "0.6.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8916a250117bdecaa9aa721465c646da282dc41523361b63c8cd01693c6869" dependencies = [ "base64 0.22.1", "bitfield-struct", @@ -1665,6 +1675,8 @@ dependencies = [ [[package]] name = "h3x" version = "0.6.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7e242a82f0df4f9c7a8a83440a9943468ff99bd3a4f004c47cc717bb155a7a3" dependencies = [ "arc-swap", "async-channel", @@ -4498,7 +4510,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f103c50866b8743da9429b8a581d81a27c2d3a9c4ac7df8f8571c1dd7896eda" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.118", @@ -5529,7 +5541,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] From 4ac01d3607c23882bca9b4dff9f2330b27f74920 Mon Sep 17 00:00:00 2001 From: eareimu Date: Thu, 9 Jul 2026 16:26:40 +0800 Subject: [PATCH 8/8] fix(release): support macOS asset uploads --- .github/workflows/release.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7b433be..85152e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -234,7 +234,10 @@ jobs: set -euo pipefail report=target/publish-reports/deb.toml test -f "$report" - mapfile -t assets < <(python3 - "$report" <<'PY' + assets=() + while IFS= read -r asset; do + assets+=("$asset") + done < <(python3 - "$report" <<'PY' import sys import tomllib @@ -392,7 +395,10 @@ jobs: set -euo pipefail report=target/publish-reports/rpm.toml test -f "$report" - mapfile -t assets < <(python3 - "$report" <<'PY' + assets=() + while IFS= read -r asset; do + assets+=("$asset") + done < <(python3 - "$report" <<'PY' import sys import tomllib @@ -606,7 +612,10 @@ jobs: set -euo pipefail report=target/publish-reports/brew.toml test -f "$report" - mapfile -t assets < <(python3 - "$report" <<'PY' + assets=() + while IFS= read -r asset; do + assets+=("$asset") + done < <(python3 - "$report" <<'PY' import sys import tomllib