wip: preserve two-node IndeeHub UAT fixes
This commit is contained in:
@@ -338,6 +338,20 @@ impl ApiHandler {
|
||||
origin == format!("http://{host}") || origin == format!("https://{host}")
|
||||
}
|
||||
|
||||
// Browsers do not send Origin on a same-origin GET. Fetch Metadata is a
|
||||
// browser-controlled header and distinguishes the dashboard from an app on
|
||||
// another port, which is only same-site. Mutating requests and WebSockets
|
||||
// still require the exact Origin above.
|
||||
fn has_terminal_read_origin(headers: &hyper::HeaderMap) -> bool {
|
||||
if headers.contains_key(hyper::header::ORIGIN) {
|
||||
return Self::has_terminal_origin(headers);
|
||||
}
|
||||
headers
|
||||
.get("sec-fetch-site")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
== Some("same-origin")
|
||||
}
|
||||
|
||||
/// Permissive origin check for the share-to-mesh iframe intent: any scheme
|
||||
/// http(s):// followed by the configured host_ip, optionally `:port`. Apps
|
||||
/// proxied under other ports (APP_PORTS) call this from within the same
|
||||
@@ -533,7 +547,25 @@ impl ApiHandler {
|
||||
|
||||
match (method, path.as_str()) {
|
||||
// RPC — auth is handled inside rpc handler per-method
|
||||
(Method::POST, "/rpc/v1") => self.rpc_handler.clone().handle(req_with_bytes).await,
|
||||
(Method::POST, "/rpc/v1") => {
|
||||
let origin = self.app_cors_origin(&headers);
|
||||
let mut response = self.rpc_handler.clone().handle(req_with_bytes).await?;
|
||||
if !origin.is_empty() {
|
||||
response.headers_mut().insert(
|
||||
"access-control-allow-origin",
|
||||
hyper::header::HeaderValue::from_str(&origin)?,
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
"access-control-allow-credentials",
|
||||
hyper::header::HeaderValue::from_static("true"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
hyper::header::VARY,
|
||||
hyper::header::HeaderValue::from_static("Origin"),
|
||||
);
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
// AIUI model proxy — session-gated forwarder to Claude/Ollama,
|
||||
// replacing the unauthenticated claude-api-proxy.py sidecar and
|
||||
@@ -586,7 +618,7 @@ impl ApiHandler {
|
||||
if !self.is_authenticated(&headers).await {
|
||||
return Ok(Self::unauthorized());
|
||||
}
|
||||
if !Self::has_terminal_origin(&headers) {
|
||||
if !Self::has_terminal_read_origin(&headers) {
|
||||
return Ok(build_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
"application/json",
|
||||
@@ -939,6 +971,18 @@ mod terminal_origin_tests {
|
||||
ApiHandler::has_terminal_origin(&headers)
|
||||
}
|
||||
|
||||
fn read_allowed(host: &str, origin: Option<&str>, fetch_site: Option<&str>) -> bool {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(HOST, HeaderValue::from_str(host).unwrap());
|
||||
if let Some(origin) = origin {
|
||||
headers.insert(ORIGIN, HeaderValue::from_str(origin).unwrap());
|
||||
}
|
||||
if let Some(fetch_site) = fetch_site {
|
||||
headers.insert("sec-fetch-site", HeaderValue::from_str(fetch_site).unwrap());
|
||||
}
|
||||
ApiHandler::has_terminal_read_origin(&headers)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_accepts_only_the_exact_dashboard_authority() {
|
||||
assert!(allowed("node.local", Some("https://node.local")));
|
||||
@@ -949,4 +993,22 @@ mod terminal_origin_tests {
|
||||
assert!(!allowed("node.local", Some("null")));
|
||||
assert!(!allowed("node.local", None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_read_accepts_same_origin_fetch_but_not_same_site_apps() {
|
||||
assert!(read_allowed("node.local", None, Some("same-origin")));
|
||||
assert!(!read_allowed("node.local", None, Some("same-site")));
|
||||
assert!(!read_allowed("node.local", None, Some("cross-site")));
|
||||
assert!(!read_allowed("node.local", None, None));
|
||||
assert!(!read_allowed(
|
||||
"node.local",
|
||||
Some("https://evil.example"),
|
||||
Some("same-site")
|
||||
));
|
||||
assert!(!read_allowed(
|
||||
"node.local",
|
||||
Some("https://evil.example"),
|
||||
Some("same-origin")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,11 @@ impl RpcHandler {
|
||||
.require_fips()
|
||||
.single_delivery()
|
||||
.timeout(std::time::Duration::from_secs(20))
|
||||
.send_content_json(&self.config.data_dir, &binding.contract.seller_did, &body)
|
||||
.send_rental_control_json(
|
||||
&self.config.data_dir,
|
||||
&binding.contract.seller_did,
|
||||
&body,
|
||||
)
|
||||
.await?;
|
||||
anyhow::ensure!(
|
||||
response.status().is_success(),
|
||||
|
||||
@@ -35,6 +35,23 @@ use tokio::net::UdpSocket;
|
||||
/// path filter can restrict the exposed surface.
|
||||
pub const PEER_PORT: u16 = 5679;
|
||||
|
||||
fn is_rental_control_route(path: &str) -> bool {
|
||||
let parts: Vec<_> = path.split('/').collect();
|
||||
let valid_id = |value: &str| {
|
||||
!value.is_empty()
|
||||
&& value
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
|
||||
};
|
||||
parts.len() == 6
|
||||
&& parts[0].is_empty()
|
||||
&& parts[1] == "content"
|
||||
&& valid_id(parts[2])
|
||||
&& parts[3] == "rental"
|
||||
&& valid_id(parts[4])
|
||||
&& matches!(parts[5], "prepare" | "start")
|
||||
}
|
||||
|
||||
/// Whether a FIPS-side HTTP status should trigger a fall-back to Tor in
|
||||
/// `Auto` mode. A `404` over FIPS often means the peer's mesh listener
|
||||
/// doesn't expose that path (e.g. a peer on an older build with a stricter
|
||||
@@ -527,8 +544,27 @@ impl<'a> PeerRequest<'a> {
|
||||
request.send_encoded_json(&encoded).await
|
||||
}
|
||||
|
||||
/// Rental preparation/start carries an already-settled capability, never a
|
||||
/// payment token. Authenticate the exact control route without relaxing the
|
||||
/// purchase-only guard above or replaying any purchase on a retry.
|
||||
pub(crate) async fn send_rental_control_json<B: serde::Serialize>(
|
||||
self,
|
||||
data_dir: &std::path::Path,
|
||||
expected_seller: &str,
|
||||
body: &B,
|
||||
) -> Result<(reqwest::Response, crate::transport::TransportKind)> {
|
||||
anyhow::ensure!(
|
||||
is_rental_control_route(self.path),
|
||||
"Not a rental control route"
|
||||
);
|
||||
let (request, encoded) = self
|
||||
.prepare_authenticated_content_json(data_dir, expected_seller, body)
|
||||
.await?;
|
||||
request.send_encoded_json(&encoded).await
|
||||
}
|
||||
|
||||
async fn prepare_content_json<B: serde::Serialize>(
|
||||
mut self,
|
||||
self,
|
||||
data_dir: &std::path::Path,
|
||||
expected_seller: &str,
|
||||
body: &B,
|
||||
@@ -537,6 +573,16 @@ impl<'a> PeerRequest<'a> {
|
||||
self.path.starts_with("/content/purchase/"),
|
||||
"Not a purchase route"
|
||||
);
|
||||
self.prepare_authenticated_content_json(data_dir, expected_seller, body)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn prepare_authenticated_content_json<B: serde::Serialize>(
|
||||
mut self,
|
||||
data_dir: &std::path::Path,
|
||||
expected_seller: &str,
|
||||
body: &B,
|
||||
) -> Result<(Self, Vec<u8>)> {
|
||||
let peer = crate::federation::load_unique_payment_peer(data_dir, self.onion_host).await?;
|
||||
anyhow::ensure!(
|
||||
peer.did == expected_seller,
|
||||
@@ -887,6 +933,28 @@ impl<'a> PeerRequest<'a> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rental_control_routes_are_exact_and_cannot_be_purchase_routes() {
|
||||
assert!(is_rental_control_route(
|
||||
"/content/registered_abc/rental/1234-5678/prepare"
|
||||
));
|
||||
assert!(is_rental_control_route(
|
||||
"/content/registered_abc/rental/1234-5678/start"
|
||||
));
|
||||
for path in [
|
||||
"/content/purchase/abc",
|
||||
"/content/registered_abc/rental/1234-5678/settle",
|
||||
"/content/registered_abc/rental/1234-5678/prepare/extra",
|
||||
"/content/../rental/1234-5678/prepare",
|
||||
"/content/registered_abc/rental/1234-5678/prepare?token=x",
|
||||
] {
|
||||
assert!(
|
||||
!is_rental_control_route(path),
|
||||
"unexpected rental control route: {path}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn purchase_post_serializes_once_and_binds_the_actual_bytes_to_the_peer() {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
Reference in New Issue
Block a user