210 lines
8.9 KiB
Rust
210 lines
8.9 KiB
Rust
use super::super::Directory;
|
|
use super::fixture::digest;
|
|
use std::io::{Read, Write};
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::os::unix::net::UnixListener;
|
|
|
|
#[test]
|
|
fn cache_client_authenticates_receipts_and_rejects_missing_corrupt_or_truncated_bytes() {
|
|
for behavior in [
|
|
"valid",
|
|
"missing",
|
|
"receipt",
|
|
"digest",
|
|
"truncated",
|
|
"oversized",
|
|
] {
|
|
let directory = Directory::new();
|
|
let state = directory.0.join("state");
|
|
std::fs::create_dir(&state).unwrap();
|
|
std::fs::set_permissions(&state, std::fs::Permissions::from_mode(0o700)).unwrap();
|
|
let token = "a".repeat(64);
|
|
let token_path = state.join("deverd.token");
|
|
std::fs::write(&token_path, &token).unwrap();
|
|
std::fs::set_permissions(token_path, std::fs::Permissions::from_mode(0o600)).unwrap();
|
|
let endpoint = directory.0.join("cache.sock");
|
|
let server = UnixListener::bind(&endpoint).unwrap();
|
|
let bytes = b"archive fixture";
|
|
let sha256 = digest(bytes);
|
|
let expected = sha256.clone();
|
|
let worker = std::thread::spawn(move || {
|
|
let (mut stream, _) = server.accept().unwrap();
|
|
let mut length = [0; 4];
|
|
stream.read_exact(&mut length).unwrap();
|
|
let mut payload = vec![0; u32::from_be_bytes(length) as usize];
|
|
stream.read_exact(&mut payload).unwrap();
|
|
let request: serde_json::Value = serde_json::from_slice(&payload).unwrap();
|
|
assert_eq!(request["token"], token);
|
|
assert_eq!(request["operation"]["artifact_get"]["sha256"], expected);
|
|
assert_eq!(request["operation"]["artifact_get"]["bytes"], bytes.len());
|
|
if behavior == "oversized" {
|
|
stream
|
|
.write_all(&(1024_u32 * 1024 + 1).to_be_bytes())
|
|
.unwrap();
|
|
return;
|
|
}
|
|
let artifact = if behavior == "missing" {
|
|
serde_json::Value::Null
|
|
} else {
|
|
serde_json::json!({"sha256":if behavior == "receipt" { "0".repeat(64) } else { expected }, "bytes":bytes.len()})
|
|
};
|
|
let response = serde_json::to_vec(
|
|
&serde_json::json!({"ok":true, "status":null, "error":null, "artifact":artifact}),
|
|
)
|
|
.unwrap();
|
|
stream
|
|
.write_all(&(response.len() as u32).to_be_bytes())
|
|
.unwrap();
|
|
stream.write_all(&response).unwrap();
|
|
if !matches!(behavior, "missing" | "receipt") {
|
|
let mut content = bytes.to_vec();
|
|
if behavior == "digest" {
|
|
content[0] ^= 1;
|
|
}
|
|
if behavior == "truncated" {
|
|
content.pop();
|
|
}
|
|
stream.write_all(&content).unwrap();
|
|
}
|
|
});
|
|
let result = dever::process::cache::read(&state, &endpoint, &sha256, bytes.len() as u64);
|
|
worker.join().unwrap();
|
|
if behavior == "valid" {
|
|
assert_eq!(result.unwrap(), bytes);
|
|
} else {
|
|
assert!(result.is_err(), "accepted {behavior}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn cache_client_rejects_untrusted_state_before_connecting() {
|
|
let directory = Directory::new();
|
|
std::fs::set_permissions(&directory.0, std::fs::Permissions::from_mode(0o777)).unwrap();
|
|
let error = dever::process::cache::read(
|
|
&directory.0,
|
|
&directory.0.join("absent.sock"),
|
|
&"a".repeat(64),
|
|
1,
|
|
)
|
|
.unwrap_err();
|
|
assert!(error.contains("not writable"), "{error}");
|
|
}
|
|
|
|
#[test]
|
|
fn cache_upload_requires_both_authenticated_admission_and_final_receipts() {
|
|
for rejected in ["none", "admission", "publication"] {
|
|
let directory = Directory::new();
|
|
let state = directory.0.join("state");
|
|
std::fs::create_dir(&state).unwrap();
|
|
std::fs::set_permissions(&state, std::fs::Permissions::from_mode(0o700)).unwrap();
|
|
let token = "b".repeat(64);
|
|
let path = state.join("deverd.token");
|
|
std::fs::write(&path, &token).unwrap();
|
|
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)).unwrap();
|
|
let endpoint = directory.0.join("cache.sock");
|
|
let listener = UnixListener::bind(&endpoint).unwrap();
|
|
let bytes = b"content addressed upload";
|
|
let server = std::thread::spawn(move || {
|
|
let (mut stream, _) = listener.accept().unwrap();
|
|
stream
|
|
.set_read_timeout(Some(std::time::Duration::from_secs(5)))
|
|
.unwrap();
|
|
let mut prefix = [0; 4];
|
|
stream.read_exact(&mut prefix).unwrap();
|
|
let mut payload = vec![0; u32::from_be_bytes(prefix) as usize];
|
|
stream.read_exact(&mut payload).unwrap();
|
|
let request: serde_json::Value = serde_json::from_slice(&payload).unwrap();
|
|
assert_eq!(request["token"], token);
|
|
let expected = serde_json::json!({"sha256":digest(bytes), "bytes":bytes.len()});
|
|
assert_eq!(request["operation"]["artifact_put"], expected);
|
|
for phase in ["admission", "publication"] {
|
|
let artifact = if rejected == phase {
|
|
serde_json::json!({"sha256":"0".repeat(64),"bytes":bytes.len()})
|
|
} else {
|
|
expected.clone()
|
|
};
|
|
let response = serde_json::to_vec(
|
|
&serde_json::json!({"ok":true,"status":null,"error":null,"artifact":artifact}),
|
|
)
|
|
.unwrap();
|
|
stream
|
|
.write_all(&(response.len() as u32).to_be_bytes())
|
|
.unwrap();
|
|
stream.write_all(&response).unwrap();
|
|
if phase == "admission" {
|
|
if rejected == phase {
|
|
assert_eq!(stream.read(&mut prefix).unwrap(), 0);
|
|
break;
|
|
}
|
|
let mut received = vec![0; bytes.len()];
|
|
stream.read_exact(&mut received).unwrap();
|
|
assert_eq!(received, bytes);
|
|
}
|
|
}
|
|
});
|
|
let result = dever::process::cache::write(&state, &endpoint, bytes);
|
|
server.join().unwrap();
|
|
assert_eq!(result.is_ok(), rejected == "none");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn signed_resource_get_uses_the_historical_deverd_envelope_and_exact_receipt() {
|
|
// 原 service.rs::resource 请求版本/catalog/扩展身份;服务先回复 receipt,再写裸资产。
|
|
for corrupt in [false, true] {
|
|
let directory = Directory::new();
|
|
let state = directory.0.join("state");
|
|
std::fs::create_dir(&state).unwrap();
|
|
let token = "c".repeat(64);
|
|
let token_path = state.join("deverd.token");
|
|
std::fs::write(&token_path, &token).unwrap();
|
|
std::fs::set_permissions(token_path, std::fs::Permissions::from_mode(0o600)).unwrap();
|
|
let endpoint = directory.0.join("resources.sock");
|
|
let listener = UnixListener::bind(&endpoint).unwrap();
|
|
let bytes = b"signed runtime fixture";
|
|
let catalog = "d".repeat(64);
|
|
let extension = serde_json::json!({"kind":{"type":"runtime","ecosystem":"npm"},"target":"linux-x86_64"});
|
|
let expected = serde_json::json!({"token":token,"operation":{"resource_get":{"version":"0.0.1","catalog":catalog,"extension":extension,"artifact":{"sha256":digest(bytes),"bytes":bytes.len()}}},"installed":[]});
|
|
let worker = std::thread::spawn(move || {
|
|
let (mut stream, _) = listener.accept().unwrap();
|
|
stream
|
|
.set_read_timeout(Some(std::time::Duration::from_secs(5)))
|
|
.unwrap();
|
|
let mut prefix = [0; 4];
|
|
stream.read_exact(&mut prefix).unwrap();
|
|
let mut request = vec![0; u32::from_be_bytes(prefix) as usize];
|
|
stream.read_exact(&mut request).unwrap();
|
|
assert_eq!(
|
|
serde_json::from_slice::<serde_json::Value>(&request).unwrap(),
|
|
expected
|
|
);
|
|
let response = serde_json::to_vec(&serde_json::json!({"ok":true,"status":null,"error":null,"artifact":{"sha256":digest(bytes),"bytes":bytes.len()}})).unwrap();
|
|
stream
|
|
.write_all(&(response.len() as u32).to_be_bytes())
|
|
.unwrap();
|
|
stream.write_all(&response).unwrap();
|
|
let mut content = bytes.to_vec();
|
|
if corrupt {
|
|
content[0] ^= 1;
|
|
}
|
|
stream.write_all(&content).unwrap();
|
|
});
|
|
let result = dever::process::cache::resource(
|
|
&state,
|
|
&endpoint,
|
|
"0.0.1",
|
|
&catalog,
|
|
Some(&extension),
|
|
&digest(bytes),
|
|
bytes.len() as u64,
|
|
);
|
|
worker.join().unwrap();
|
|
if corrupt {
|
|
assert!(result.unwrap_err().contains("SHA-256"));
|
|
} else {
|
|
assert_eq!(result.unwrap(), bytes);
|
|
}
|
|
}
|
|
}
|