use crate::support::{Fixture, TestResult};
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use haematite::db::DatabaseConfig;
use haematite::store::NodeCacheBudget;
use haematite_cli::serve::pass::{Gate, Unreachable};
use haematite_cli::serve::service::Service;
use haematite_cli::serve::storage::Storage;
use haematite_cli::serve::transport::Limits;
use haematite_cli::serve::transport::network::{Network, NetworkConfig, TlsFiles};
use serde_json::{Value, json};
use std::error::Error;
use std::fs;
use std::net::SocketAddr;
use std::num::{NonZeroU32, NonZeroUsize};
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::Arc;
use std::time::Duration;
fn certificate(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src/serve/network_fixtures")
.join(name)
}
fn keys() -> String {
let key = ed25519_dalek::SigningKey::from_bytes(&[31; 32]);
let x = URL_SAFE_NO_PAD.encode(key.verifying_key().as_bytes());
json!({"keys": [{"kty": "OKP", "crv": "Ed25519", "kid": "check", "x": x, "alg": "EdDSA", "use": "sig"}]})
.to_string()
}
fn store(directory: &Path, tls: bool) -> Result<SocketAddr, Box<dyn Error>> {
let storage = Storage::create(
DatabaseConfig {
data_dir: directory.join(if tls { "tls-store" } else { "plain-store" }),
shard_count: 1,
distributed: None,
executor_threads: Some(1),
node_cache_budget: Some(NodeCacheBudget::Unlimited),
retention: haematite::Retention::KeepAll,
},
NodeCacheBudget::Unlimited,
Duration::from_secs(1),
)?;
let service = Arc::new(
Service::new(storage, NonZeroU32::MIN.saturating_add(9))?.with_lys(Gate::new(
&keys(),
"https://lys.example",
"wards",
Arc::new(Unreachable),
)?),
);
let limits = Limits {
max_frame: NonZeroU32::MIN.saturating_add(65_535),
max_clients: NonZeroUsize::new(4).ok_or("zero clients")?,
frame_timeout: Duration::from_secs(60),
};
let config = NetworkConfig {
listen: "127.0.0.1:0".parse()?,
tls: tls.then(|| TlsFiles {
certificate_file: certificate("server.pem"),
key_file: certificate("server.key"),
}),
max_connections: None,
};
let network = Network::bind(&config, &service, limits.max_clients)?;
let address = network.address();
std::thread::Builder::new()
.name("check-store".to_owned())
.spawn(move || {
if let Err(error) = network.run(&service, limits) {
eprintln!("check store ended: {error}");
}
})?;
Ok(address)
}
fn entry(address: SocketAddr) -> Value {
json!({"endpoint": address.to_string(), "server_name": "localhost"})
}
fn door(fixture: &Fixture, check: bool, trusted: bool) -> Result<Output, Box<dyn Error>> {
let path = fixture.root().join("door.json");
fs::write(&path, serde_json::to_vec_pretty(&fixture.config)?)?;
let mut command = Command::new(env!("CARGO_BIN_EXE_haematite"));
command.arg("door");
if check {
command.arg("--check");
}
command
.arg("--config")
.arg(&path)
.current_dir(fixture.root())
.env_remove("SSL_CERT_FILE");
if trusted {
command.env("SSL_CERT_FILE", certificate("ca.pem"));
}
Ok(command.output()?)
}
fn text(output: &Output) -> String {
format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
}
fn listing(path: &Path) -> std::io::Result<Vec<(std::ffi::OsString, Vec<u8>)>> {
let mut entries = fs::read_dir(path)?
.map(|entry| {
let entry = entry?;
let bytes = if entry.file_type()?.is_file() {
fs::read(entry.path())?
} else {
Vec::new()
};
Ok((entry.file_name(), bytes))
})
.collect::<std::io::Result<Vec<_>>>()?;
entries.sort();
Ok(entries)
}
fn refused_both_ways(fixture: &Fixture, trusted: bool, name: &str) -> TestResult {
let checked = door(fixture, true, trusted)?;
assert_eq!(checked.status.code(), Some(78), "{}", text(&checked));
assert!(text(&checked).contains(name), "--check: {}", text(&checked));
let started = door(fixture, false, trusted)?;
assert_eq!(started.status.code(), Some(1), "{}", text(&started));
assert!(text(&started).contains(name), "start: {}", text(&started));
assert!(
!text(&started).contains("lys_could_not_be_asked"),
"Lys was asked before the store was refused: {}",
text(&started)
);
Ok(())
}
#[test]
fn check_says_a_network_store_was_reached_and_answered_hello_and_changes_nothing() -> TestResult {
let mut fixture = Fixture::new()?;
let address = store(fixture.root(), true)?;
fixture.config["stores"]["play"] = entry(address);
let path = fixture.root().join("door.json");
fs::write(&path, serde_json::to_vec_pretty(&fixture.config)?)?;
let before = listing(fixture.root())?;
let output = door(&fixture, true, true)?;
assert_eq!(output.status.code(), Some(0), "{}", text(&output));
assert!(
text(&output).contains(&format!(
"door_store_reached: store play: {address}: hello answered"
)),
"{}",
text(&output)
);
assert_eq!(listing(fixture.root())?, before);
Ok(())
}
#[test]
fn an_untrusted_network_store_is_refused_by_name_at_check_and_at_start() -> TestResult {
let mut fixture = Fixture::new()?;
let address = store(fixture.root(), true)?;
fixture.config["stores"]["play"] = entry(address);
refused_both_ways(&fixture, false, "door_store_tls_untrusted: store play")
}
#[test]
fn a_plain_tcp_network_store_is_refused_by_name_at_check_and_at_start() -> TestResult {
let mut fixture = Fixture::new()?;
let address = store(fixture.root(), false)?;
fixture.config["stores"]["play"] = entry(address);
refused_both_ways(&fixture, true, "door_store_not_tls: store play")
}
#[test]
fn a_network_entry_without_server_name_or_a_parsed_endpoint_is_refused_by_name() -> TestResult {
for (stored, name) in [
(
json!({"endpoint": "127.0.0.1:1"}),
"door_store_server_name_missing: store play",
),
(
json!({"endpoint": "tcp://127.0.0.1:1", "server_name": "localhost"}),
"door_store_endpoint_invalid: store play",
),
(
json!({"endpoint": "127.0.0.1:1", "server_name": "localhost", "tls": false}),
"door_store_field_unknown: store play",
),
] {
let mut fixture = Fixture::new()?;
fixture.config["stores"]["play"] = stored;
refused_both_ways(&fixture, true, name)?;
}
Ok(())
}