| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
|
| 16 |
|
| 17 |
use crate::home::{Home, RunnerConfig}; |
| 18 |
use crate::images::{cache_path, chunk_cache, manifest_path, save_manifest, write_image, Client, Moved}; |
| 19 |
use anyhow::{bail, Context, Result}; |
| 20 |
use flow_core::api::{SwarmAddr, SwarmPeer}; |
| 21 |
use jet::images::{is_hash, Manifest}; |
| 22 |
use jet::swarm::{Bits, Pick, Picker, Source, Ticket}; |
| 23 |
use oxy_core::identity::Identity; |
| 24 |
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; |
| 25 |
use rustls::crypto::CryptoProvider; |
| 26 |
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName, UnixTime}; |
| 27 |
use rustls::{DigitallySignedStruct, SignatureScheme}; |
| 28 |
use std::collections::HashMap; |
| 29 |
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write}; |
| 30 |
use std::net::{TcpListener, TcpStream}; |
| 31 |
use std::path::{Path, PathBuf}; |
| 32 |
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; |
| 33 |
use std::sync::{Arc, Mutex}; |
| 34 |
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; |
| 35 |
|
| 36 |
|
| 37 |
const MAX_CONNS: usize = 16; |
| 38 |
|
| 39 |
const MAX_OPEN: usize = 4 * MAX_CONNS; |
| 40 |
|
| 41 |
const PEER_WORKERS: usize = 4; |
| 42 |
const HUB_WORKERS: usize = 8; |
| 43 |
|
| 44 |
const REFRESH: Duration = Duration::from_millis(500); |
| 45 |
|
| 46 |
const HUB_FAILURES: usize = 5; |
| 47 |
|
| 48 |
const MAX_BODY: usize = 64 * 1024; |
| 49 |
|
| 50 |
static PART: AtomicU64 = AtomicU64::new(0); |
| 51 |
|
| 52 |
fn now_ms() -> u64 { |
| 53 |
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0) |
| 54 |
} |
| 55 |
|
| 56 |
|
| 57 |
|
| 58 |
struct Offer { |
| 59 |
bits: Option<Bits>, |
| 60 |
fetching: Bits, |
| 61 |
} |
| 62 |
|
| 63 |
struct State { |
| 64 |
root: PathBuf, |
| 65 |
site: String, |
| 66 |
client: Client, |
| 67 |
|
| 68 |
hub: Mutex<Option<Identity>>, |
| 69 |
offers: Mutex<HashMap<String, Offer>>, |
| 70 |
|
| 71 |
located: Mutex<HashMap<String, (PathBuf, u64, u32)>>, |
| 72 |
|
| 73 |
sessions: Mutex<HashMap<String, (String, u64)>>, |
| 74 |
conns: AtomicUsize, |
| 75 |
|
| 76 |
|
| 77 |
bucket: Mutex<(f64, Instant)>, |
| 78 |
rate: f64, |
| 79 |
tls: Arc<rustls::ServerConfig>, |
| 80 |
} |
| 81 |
|
| 82 |
|
| 83 |
pub struct Swarm { |
| 84 |
state: Arc<State>, |
| 85 |
pub addr: SwarmAddr, |
| 86 |
} |
| 87 |
|
| 88 |
impl Swarm { |
| 89 |
|
| 90 |
pub fn start(home: &Home, cfg: &RunnerConfig) -> Result<Option<Swarm>> { |
| 91 |
let Some(sc) = &cfg.swarm else { return Ok(None) }; |
| 92 |
let listener = TcpListener::bind(&sc.listen).with_context(|| format!("swarm: listen on {}", sc.listen))?; |
| 93 |
let client = Client::new(&cfg.hub, &cfg.token, cfg.ca.as_deref())?; |
| 94 |
let hub = client.hub_identity().map_err(|e| eprintln!("swarm: the hub's key: {e:#}; trying again when a peer comes")).ok(); |
| 95 |
let rate = sc.upload_mib_s.unwrap_or(50).max(1) as f64 * (1 << 20) as f64; |
| 96 |
let (cert, key) = certificate(&home.root)?; |
| 97 |
let fingerprint = blake3::hash(&cert).to_hex().to_string(); |
| 98 |
let tls = server_tls(cert, key)?; |
| 99 |
let state = Arc::new(State { |
| 100 |
root: home.root.clone(), |
| 101 |
site: sc.site.clone(), |
| 102 |
client, |
| 103 |
hub: Mutex::new(hub), |
| 104 |
offers: Mutex::new(HashMap::new()), |
| 105 |
located: Mutex::new(HashMap::new()), |
| 106 |
sessions: Mutex::new(HashMap::new()), |
| 107 |
conns: AtomicUsize::new(0), |
| 108 |
bucket: Mutex::new((rate, Instant::now())), |
| 109 |
rate, |
| 110 |
tls, |
| 111 |
}); |
| 112 |
|
| 113 |
|
| 114 |
|
| 115 |
let mut unscanned = Vec::new(); |
| 116 |
for h in home.images() { |
| 117 |
match std::fs::read(manifest_path(&home.root, &h)).ok().and_then(|b| serde_json::from_slice::<Manifest>(&b).ok()) { |
| 118 |
Some(m) if m.hash == h => offer_whole(&state, &m, &home.rootfs(&h)), |
| 119 |
_ => unscanned.push((h.clone(), home.rootfs(&h), home.image_arch(&h))), |
| 120 |
} |
| 121 |
} |
| 122 |
let scanning = state.clone(); |
| 123 |
std::thread::spawn(move || { |
| 124 |
for (h, path, arch) in unscanned { |
| 125 |
match jet::images::scan_file(&path, arch, |_, _| Ok(())) { |
| 126 |
Ok(m) if m.hash == h => { |
| 127 |
save_manifest(&scanning.root, &m).ok(); |
| 128 |
offer_whole(&scanning, &m, &path); |
| 129 |
} |
| 130 |
Ok(_) => eprintln!("swarm: {} does not hold the image its name says; not offering it", path.display()), |
| 131 |
Err(e) => eprintln!("swarm: scan {}: {e}", path.display()), |
| 132 |
} |
| 133 |
} |
| 134 |
}); |
| 135 |
let serving = state.clone(); |
| 136 |
std::thread::spawn(move || accept(listener, serving)); |
| 137 |
let addr = SwarmAddr { addr: sc.advertise.clone().unwrap_or_else(|| sc.listen.clone()), site: sc.site.clone(), cert: fingerprint }; |
| 138 |
eprintln!( |
| 139 |
"swarm: serving image chunks on {} over TLS (certificate {}, site {:?}, up to {} MiB/s)", |
| 140 |
sc.listen, |
| 141 |
&addr.cert[..12], |
| 142 |
sc.site, |
| 143 |
sc.upload_mib_s.unwrap_or(50) |
| 144 |
); |
| 145 |
Ok(Some(Swarm { state, addr })) |
| 146 |
} |
| 147 |
|
| 148 |
|
| 149 |
|
| 150 |
|
| 151 |
pub fn fetch(&self, home: &Home, hash: &str) -> Result<Option<Moved>> { |
| 152 |
let start = Instant::now(); |
| 153 |
let client = &self.state.client; |
| 154 |
let Some(m) = client.manifest(hash)? else { return Ok(None) }; |
| 155 |
if m.hash != hash { |
| 156 |
bail!("the hub's manifest for {} names {}", &hash[..12], &m.hash[..12]); |
| 157 |
} |
| 158 |
save_manifest(&home.root, &m)?; |
| 159 |
let needed: Vec<String> = m.needed().into_iter().map(String::from).collect(); |
| 160 |
let cache = chunk_cache(home); |
| 161 |
let mut have = Bits::new(needed.len()); |
| 162 |
for (i, h) in needed.iter().enumerate() { |
| 163 |
if cache_path(&cache, h).is_file() { |
| 164 |
have.set(i); |
| 165 |
} |
| 166 |
} |
| 167 |
self.state.offers.lock().unwrap().insert(hash.into(), Offer { bits: Some(have.clone()), fetching: Bits::new(needed.len()) }); |
| 168 |
let announced = client |
| 169 |
.announce(hash, "fetching") |
| 170 |
.map_err(|e| eprintln!("swarm: announce {}: {e:#}; fetching from the hub alone", &hash[..12])) |
| 171 |
.ok(); |
| 172 |
let seed = now_ms() ^ (std::process::id() as u64) << 32; |
| 173 |
let picker = Mutex::new(Picker::new(needed.len(), have, seed)); |
| 174 |
let peers: Mutex<Vec<Arc<Peer>>> = Mutex::new(Vec::new()); |
| 175 |
if let Some(a) = &announced { |
| 176 |
self.join(&a.peers, &a.ticket, hash, needed.len(), &peers, &picker); |
| 177 |
} |
| 178 |
let (bytes, peer_bytes, got) = (AtomicU64::new(0), AtomicU64::new(0), AtomicUsize::new(0)); |
| 179 |
let (failed, hub_failures) = (Mutex::new(None::<anyhow::Error>), AtomicUsize::new(0)); |
| 180 |
{ |
| 181 |
let (picker, peers, needed, cache) = (&picker, &peers, &needed, &cache); |
| 182 |
let (bytes, peer_bytes, got, failed, hub_failures) = (&bytes, &peer_bytes, &got, &failed, &hub_failures); |
| 183 |
let (state, m) = (&self.state, &m); |
| 184 |
let worker = move |src: Source| loop { |
| 185 |
if failed.lock().unwrap().is_some() { |
| 186 |
break; |
| 187 |
} |
| 188 |
let peer = match src { |
| 189 |
Source::Peer(p) => Some(peers.lock().unwrap()[p].clone()), |
| 190 |
Source::Hub => None, |
| 191 |
}; |
| 192 |
if peer.as_ref().is_some_and(|p| !p.alive.load(Ordering::Relaxed)) { |
| 193 |
break; |
| 194 |
} |
| 195 |
|
| 196 |
|
| 197 |
let pick = picker.lock().unwrap().pick(src); |
| 198 |
let i = match pick { |
| 199 |
Pick::Done => break, |
| 200 |
Pick::Wait => { |
| 201 |
std::thread::sleep(Duration::from_millis(20)); |
| 202 |
continue; |
| 203 |
} |
| 204 |
Pick::Chunk(i) => i, |
| 205 |
}; |
| 206 |
if let Some(o) = state.offers.lock().unwrap().get_mut(&m.hash) { |
| 207 |
o.fetching.set(i); |
| 208 |
} |
| 209 |
let h = &needed[i]; |
| 210 |
let fetched = match &peer { |
| 211 |
Some(p) => self.peer_chunk(p, h), |
| 212 |
None => client.chunk(h), |
| 213 |
}; |
| 214 |
let kept = fetched.and_then(|b| { |
| 215 |
if blake3::hash(&b).to_hex().as_str() != h { |
| 216 |
bail!("chunk {} came back with other bytes", &h[..12]); |
| 217 |
} |
| 218 |
let p = cache_path(cache, h); |
| 219 |
std::fs::create_dir_all(p.parent().expect("a chunk directory"))?; |
| 220 |
let tmp = p.with_extension(format!("part{}", PART.fetch_add(1, Ordering::Relaxed))); |
| 221 |
std::fs::write(&tmp, &b)?; |
| 222 |
std::fs::rename(&tmp, &p)?; |
| 223 |
Ok(b.len() as u64) |
| 224 |
}); |
| 225 |
match kept { |
| 226 |
Ok(n) => { |
| 227 |
if let Some(Offer { bits: Some(b), fetching }) = state.offers.lock().unwrap().get_mut(&m.hash) { |
| 228 |
b.set(i); |
| 229 |
fetching.clear(i); |
| 230 |
} |
| 231 |
picker.lock().unwrap().got(i); |
| 232 |
bytes.fetch_add(n, Ordering::Relaxed); |
| 233 |
got.fetch_add(1, Ordering::Relaxed); |
| 234 |
if peer.is_some() { |
| 235 |
peer_bytes.fetch_add(n, Ordering::Relaxed); |
| 236 |
} |
| 237 |
} |
| 238 |
Err(e) => { |
| 239 |
picker.lock().unwrap().failed(i); |
| 240 |
if let Some(o) = state.offers.lock().unwrap().get_mut(&m.hash) { |
| 241 |
o.fetching.clear(i); |
| 242 |
} |
| 243 |
match (&peer, src) { |
| 244 |
|
| 245 |
(Some(_), _) if e.is::<Busy>() => std::thread::sleep(Duration::from_millis(200)), |
| 246 |
(Some(p), Source::Peer(idx)) => { |
| 247 |
eprintln!("swarm: peer {} failed ({e:#}); carrying on without it", p.name); |
| 248 |
p.alive.store(false, Ordering::Relaxed); |
| 249 |
picker.lock().unwrap().drop_peer(idx); |
| 250 |
break; |
| 251 |
} |
| 252 |
_ => { |
| 253 |
if hub_failures.fetch_add(1, Ordering::Relaxed) + 1 >= HUB_FAILURES { |
| 254 |
*failed.lock().unwrap() = Some(e); |
| 255 |
break; |
| 256 |
} |
| 257 |
std::thread::sleep(Duration::from_millis(500)); |
| 258 |
} |
| 259 |
} |
| 260 |
} |
| 261 |
} |
| 262 |
}; |
| 263 |
std::thread::scope(|s| { |
| 264 |
for _ in 0..HUB_WORKERS { |
| 265 |
s.spawn(move || worker(Source::Hub)); |
| 266 |
} |
| 267 |
let mut spawned = 0; |
| 268 |
let mut spawn_new = || { |
| 269 |
let n = peers.lock().unwrap().len(); |
| 270 |
for p in spawned..n { |
| 271 |
for _ in 0..PEER_WORKERS { |
| 272 |
s.spawn(move || worker(Source::Peer(p))); |
| 273 |
} |
| 274 |
} |
| 275 |
spawned = n; |
| 276 |
}; |
| 277 |
spawn_new(); |
| 278 |
|
| 279 |
|
| 280 |
let mut asked_hub = Instant::now(); |
| 281 |
let interval = Duration::from_secs(announced.as_ref().map(|a| a.interval_s).unwrap_or(30).max(5)); |
| 282 |
let mut last = Instant::now(); |
| 283 |
while picker.lock().unwrap().missing() > 0 && failed.lock().unwrap().is_none() { |
| 284 |
std::thread::sleep(Duration::from_millis(100)); |
| 285 |
if last.elapsed() < REFRESH { |
| 286 |
continue; |
| 287 |
} |
| 288 |
last = Instant::now(); |
| 289 |
let now: Vec<(usize, Arc<Peer>)> = |
| 290 |
peers.lock().unwrap().iter().cloned().enumerate().filter(|(_, p)| p.alive.load(Ordering::Relaxed)).collect(); |
| 291 |
for (idx, p) in now { |
| 292 |
match self.peer_have(&p, hash, needed.len()) { |
| 293 |
Ok((bits, fetching)) => picker.lock().unwrap().peer_fetching(idx, bits, fetching), |
| 294 |
Err(e) if e.is::<Busy>() => {} |
| 295 |
Err(_) => { |
| 296 |
p.alive.store(false, Ordering::Relaxed); |
| 297 |
picker.lock().unwrap().drop_peer(idx); |
| 298 |
} |
| 299 |
} |
| 300 |
} |
| 301 |
if announced.is_some() && asked_hub.elapsed() >= interval { |
| 302 |
asked_hub = Instant::now(); |
| 303 |
if let Ok(a) = client.announce(hash, "fetching") { |
| 304 |
self.join(&a.peers, &a.ticket, hash, needed.len(), peers, picker); |
| 305 |
spawn_new(); |
| 306 |
} |
| 307 |
} |
| 308 |
} |
| 309 |
}); |
| 310 |
} |
| 311 |
if let Some(e) = failed.into_inner().unwrap() { |
| 312 |
client.announce(hash, "stopped").ok(); |
| 313 |
return Err(e); |
| 314 |
} |
| 315 |
write_image(home, &m)?; |
| 316 |
offer_whole(&self.state, &m, &home.rootfs(hash)); |
| 317 |
client.announce(hash, "done").ok(); |
| 318 |
Ok(Some(Moved { |
| 319 |
chunks: got.into_inner(), |
| 320 |
bytes: bytes.into_inner(), |
| 321 |
peer_bytes: peer_bytes.into_inner(), |
| 322 |
elapsed: start.elapsed(), |
| 323 |
})) |
| 324 |
} |
| 325 |
|
| 326 |
|
| 327 |
|
| 328 |
fn join(&self, found: &[SwarmPeer], ticket: &str, hash: &str, len: usize, peers: &Mutex<Vec<Arc<Peer>>>, picker: &Mutex<Picker>) { |
| 329 |
for sp in found { |
| 330 |
if peers.lock().unwrap().iter().any(|p| p.runner == sp.runner) { |
| 331 |
continue; |
| 332 |
} |
| 333 |
if !is_hash(&sp.cert) { |
| 334 |
eprintln!("swarm: peer {} gave no certificate (an older flow); not asking it", sp.name); |
| 335 |
continue; |
| 336 |
} |
| 337 |
let joined = hello(&sp.addr, &sp.cert, ticket).and_then(|(agent, session)| { |
| 338 |
let p = Peer { |
| 339 |
runner: sp.runner, |
| 340 |
name: sp.name.clone(), |
| 341 |
base: format!("https://{}/swarm/v1", sp.addr), |
| 342 |
agent, |
| 343 |
session, |
| 344 |
alive: AtomicBool::new(true), |
| 345 |
}; |
| 346 |
let (bits, fetching) = self.peer_have(&p, hash, len)?; |
| 347 |
Ok((p, bits, fetching)) |
| 348 |
}); |
| 349 |
match joined { |
| 350 |
Ok((p, bits, fetching)) => { |
| 351 |
let mut list = peers.lock().unwrap(); |
| 352 |
picker.lock().unwrap().peer_fetching(list.len(), bits, fetching); |
| 353 |
list.push(Arc::new(p)); |
| 354 |
} |
| 355 |
Err(e) => eprintln!("swarm: peer {} ({}): {e:#}", sp.name, sp.addr), |
| 356 |
} |
| 357 |
} |
| 358 |
} |
| 359 |
|
| 360 |
|
| 361 |
|
| 362 |
|
| 363 |
fn peer_have(&self, p: &Peer, hash: &str, len: usize) -> Result<(Bits, Bits)> { |
| 364 |
let url = format!("{}/images/{hash}/have", p.base); |
| 365 |
let r = match p.agent.get(&url).set("Authorization", &format!("Bearer {}", p.session)).call() { |
| 366 |
Ok(r) => r, |
| 367 |
Err(ureq::Error::Status(404, _)) => return Ok((Bits::new(len), Bits::new(len))), |
| 368 |
Err(e) => return Err(peer_error(&url, e)), |
| 369 |
}; |
| 370 |
let mut body = Vec::new(); |
| 371 |
r.into_reader().take(len as u64 / 4 + 64).read_to_end(&mut body)?; |
| 372 |
if body == b"complete" { |
| 373 |
return Ok((Bits::full(len), Bits::new(len))); |
| 374 |
} |
| 375 |
let (have, fetching) = body.split_at(len.div_ceil(8).min(body.len())); |
| 376 |
Ok((Bits::from_bytes(len, have), Bits::from_bytes(len, fetching))) |
| 377 |
} |
| 378 |
|
| 379 |
fn peer_chunk(&self, p: &Peer, hash: &str) -> Result<Vec<u8>> { |
| 380 |
let url = format!("{}/chunks/{hash}", p.base); |
| 381 |
let r = p.agent.get(&url).set("Authorization", &format!("Bearer {}", p.session)).call().map_err(|e| peer_error(&url, e))?; |
| 382 |
let mut out = Vec::new(); |
| 383 |
r.into_reader().take(oxy_core::chunk::MAX_SIZE as u64 + 1).read_to_end(&mut out)?; |
| 384 |
Ok(out) |
| 385 |
} |
| 386 |
} |
| 387 |
|
| 388 |
|
| 389 |
|
| 390 |
struct Peer { |
| 391 |
runner: i64, |
| 392 |
name: String, |
| 393 |
base: String, |
| 394 |
agent: ureq::Agent, |
| 395 |
session: String, |
| 396 |
alive: AtomicBool, |
| 397 |
} |
| 398 |
|
| 399 |
|
| 400 |
#[derive(Debug)] |
| 401 |
struct Busy; |
| 402 |
|
| 403 |
impl std::fmt::Display for Busy { |
| 404 |
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 405 |
f.write_str("the peer is busy") |
| 406 |
} |
| 407 |
} |
| 408 |
|
| 409 |
impl std::error::Error for Busy {} |
| 410 |
|
| 411 |
fn peer_error(url: &str, e: ureq::Error) -> anyhow::Error { |
| 412 |
match e { |
| 413 |
ureq::Error::Status(503, _) => anyhow::Error::new(Busy), |
| 414 |
ureq::Error::Status(code, r) => anyhow::anyhow!("{url}: HTTP {code}: {}", r.into_string().unwrap_or_default().trim()), |
| 415 |
e => anyhow::anyhow!("{url}: {e}"), |
| 416 |
} |
| 417 |
} |
| 418 |
|
| 419 |
|
| 420 |
|
| 421 |
fn hello(addr: &str, cert: &str, ticket: &str) -> Result<(ureq::Agent, String)> { |
| 422 |
let agent = peer_agent(cert)?; |
| 423 |
let url = format!("https://{addr}/swarm/v1/hello"); |
| 424 |
let r: serde_json::Value = agent.post(&url).send_string(ticket).map_err(|e| peer_error(&url, e))?.into_json()?; |
| 425 |
let session = r["session"].as_str().map(String::from).context("the peer gave no session")?; |
| 426 |
Ok((agent, session)) |
| 427 |
} |
| 428 |
|
| 429 |
fn provider() -> Arc<CryptoProvider> { |
| 430 |
Arc::new(rustls::crypto::ring::default_provider()) |
| 431 |
} |
| 432 |
|
| 433 |
|
| 434 |
|
| 435 |
fn peer_agent(cert: &str) -> Result<ureq::Agent> { |
| 436 |
let provider = provider(); |
| 437 |
let cfg = rustls::ClientConfig::builder_with_provider(provider.clone()) |
| 438 |
.with_safe_default_protocol_versions()? |
| 439 |
.dangerous() |
| 440 |
.with_custom_certificate_verifier(Arc::new(Pinned { cert: cert.to_string(), provider })) |
| 441 |
.with_no_client_auth(); |
| 442 |
Ok(ureq::AgentBuilder::new().tls_config(Arc::new(cfg)).timeout_connect(Duration::from_secs(2)).timeout(Duration::from_secs(20)).build()) |
| 443 |
} |
| 444 |
|
| 445 |
|
| 446 |
|
| 447 |
|
| 448 |
#[derive(Debug)] |
| 449 |
struct Pinned { |
| 450 |
cert: String, |
| 451 |
provider: Arc<CryptoProvider>, |
| 452 |
} |
| 453 |
|
| 454 |
impl ServerCertVerifier for Pinned { |
| 455 |
fn verify_server_cert( |
| 456 |
&self, |
| 457 |
end_entity: &CertificateDer<'_>, |
| 458 |
_intermediates: &[CertificateDer<'_>], |
| 459 |
_name: &ServerName<'_>, |
| 460 |
_ocsp: &[u8], |
| 461 |
_now: UnixTime, |
| 462 |
) -> std::result::Result<ServerCertVerified, rustls::Error> { |
| 463 |
if blake3::hash(end_entity.as_ref()).to_hex().as_str() == self.cert { |
| 464 |
Ok(ServerCertVerified::assertion()) |
| 465 |
} else { |
| 466 |
Err(rustls::Error::General("not the peer the hub named: its certificate is another".into())) |
| 467 |
} |
| 468 |
} |
| 469 |
|
| 470 |
fn verify_tls12_signature( |
| 471 |
&self, |
| 472 |
message: &[u8], |
| 473 |
cert: &CertificateDer<'_>, |
| 474 |
dss: &DigitallySignedStruct, |
| 475 |
) -> std::result::Result<HandshakeSignatureValid, rustls::Error> { |
| 476 |
rustls::crypto::verify_tls12_signature(message, cert, dss, &self.provider.signature_verification_algorithms) |
| 477 |
} |
| 478 |
|
| 479 |
fn verify_tls13_signature( |
| 480 |
&self, |
| 481 |
message: &[u8], |
| 482 |
cert: &CertificateDer<'_>, |
| 483 |
dss: &DigitallySignedStruct, |
| 484 |
) -> std::result::Result<HandshakeSignatureValid, rustls::Error> { |
| 485 |
rustls::crypto::verify_tls13_signature(message, cert, dss, &self.provider.signature_verification_algorithms) |
| 486 |
} |
| 487 |
|
| 488 |
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> { |
| 489 |
self.provider.signature_verification_algorithms.supported_schemes() |
| 490 |
} |
| 491 |
} |
| 492 |
|
| 493 |
|
| 494 |
|
| 495 |
|
| 496 |
fn certificate(root: &Path) -> Result<(Vec<u8>, Vec<u8>)> { |
| 497 |
let dir = root.join("swarm"); |
| 498 |
let (cert_path, key_path) = (dir.join("cert.der"), dir.join("key.der")); |
| 499 |
if let (Ok(c), Ok(k)) = (std::fs::read(&cert_path), std::fs::read(&key_path)) { |
| 500 |
return Ok((c, k)); |
| 501 |
} |
| 502 |
std::fs::create_dir_all(&dir)?; |
| 503 |
let made = rcgen::generate_simple_self_signed(vec!["flow-swarm".to_string()]).context("make the swarm's certificate")?; |
| 504 |
let (cert, key) = (made.cert.der().to_vec(), made.signing_key.serialize_der()); |
| 505 |
{ |
| 506 |
let mut f = crate::portable::private(std::fs::OpenOptions::new().write(true).create(true).truncate(true)).open(&key_path)?; |
| 507 |
f.write_all(&key)?; |
| 508 |
} |
| 509 |
std::fs::write(&cert_path, &cert)?; |
| 510 |
Ok((cert, key)) |
| 511 |
} |
| 512 |
|
| 513 |
fn server_tls(cert: Vec<u8>, key: Vec<u8>) -> Result<Arc<rustls::ServerConfig>> { |
| 514 |
let cfg = rustls::ServerConfig::builder_with_provider(provider()) |
| 515 |
.with_safe_default_protocol_versions()? |
| 516 |
.with_no_client_auth() |
| 517 |
.with_single_cert(vec![CertificateDer::from(cert)], PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key))) |
| 518 |
.context("the swarm's certificate")?; |
| 519 |
Ok(Arc::new(cfg)) |
| 520 |
} |
| 521 |
|
| 522 |
|
| 523 |
fn offer_whole(state: &State, m: &Manifest, path: &Path) { |
| 524 |
let mut located = state.located.lock().unwrap(); |
| 525 |
let mut at = 0u64; |
| 526 |
for c in &m.chunks { |
| 527 |
if !c.is_zeros() { |
| 528 |
located.entry(c.hash().to_string()).or_insert_with(|| (path.to_path_buf(), at, c.1)); |
| 529 |
} |
| 530 |
at += c.1 as u64; |
| 531 |
} |
| 532 |
state.offers.lock().unwrap().insert(m.hash.clone(), Offer { bits: None, fetching: Bits::new(0) }); |
| 533 |
} |
| 534 |
|
| 535 |
fn accept(listener: TcpListener, state: Arc<State>) { |
| 536 |
for conn in listener.incoming() { |
| 537 |
let Ok(conn) = conn else { continue }; |
| 538 |
if state.conns.load(Ordering::Relaxed) >= MAX_OPEN { |
| 539 |
continue; |
| 540 |
} |
| 541 |
let state = state.clone(); |
| 542 |
std::thread::spawn(move || { |
| 543 |
|
| 544 |
|
| 545 |
let busy = state.conns.fetch_add(1, Ordering::Relaxed) >= MAX_CONNS; |
| 546 |
serve_tls(conn, &state, busy).ok(); |
| 547 |
state.conns.fetch_sub(1, Ordering::Relaxed); |
| 548 |
}); |
| 549 |
} |
| 550 |
} |
| 551 |
|
| 552 |
fn serve_tls(conn: TcpStream, state: &State, busy: bool) -> std::io::Result<()> { |
| 553 |
conn.set_read_timeout(Some(Duration::from_secs(30)))?; |
| 554 |
conn.set_write_timeout(Some(Duration::from_secs(30)))?; |
| 555 |
conn.set_nodelay(true)?; |
| 556 |
let tls = rustls::ServerConnection::new(state.tls.clone()).map_err(std::io::Error::other)?; |
| 557 |
serve_conn(rustls::StreamOwned::new(tls, conn), state, busy) |
| 558 |
} |
| 559 |
|
| 560 |
fn respond(conn: &mut impl Write, code: u16, kind: &str, body: &[u8]) -> std::io::Result<()> { |
| 561 |
let reason = match code { |
| 562 |
200 => "OK", |
| 563 |
400 => "Bad Request", |
| 564 |
401 => "Unauthorized", |
| 565 |
403 => "Forbidden", |
| 566 |
404 => "Not Found", |
| 567 |
503 => "Service Unavailable", |
| 568 |
_ => "Error", |
| 569 |
}; |
| 570 |
write!(conn, "HTTP/1.1 {code} {reason}\r\nContent-Type: {kind}\r\nContent-Length: {}\r\n\r\n", body.len())?; |
| 571 |
conn.write_all(body)?; |
| 572 |
conn.flush() |
| 573 |
} |
| 574 |
|
| 575 |
|
| 576 |
fn serve_conn<S: Read + Write>(conn: S, state: &State, busy: bool) -> std::io::Result<()> { |
| 577 |
let mut r = BufReader::new(conn); |
| 578 |
loop { |
| 579 |
let mut line = String::new(); |
| 580 |
if r.read_line(&mut line)? == 0 { |
| 581 |
return Ok(()); |
| 582 |
} |
| 583 |
let mut parts = line.split_whitespace(); |
| 584 |
let (method, path) = (parts.next().unwrap_or("").to_string(), parts.next().unwrap_or("").to_string()); |
| 585 |
let (mut length, mut auth, mut close) = (0usize, String::new(), false); |
| 586 |
loop { |
| 587 |
let mut h = String::new(); |
| 588 |
if r.read_line(&mut h)? == 0 { |
| 589 |
return Ok(()); |
| 590 |
} |
| 591 |
let h = h.trim_end(); |
| 592 |
if h.is_empty() { |
| 593 |
break; |
| 594 |
} |
| 595 |
if let Some((k, v)) = h.split_once(':') { |
| 596 |
match k.trim().to_ascii_lowercase().as_str() { |
| 597 |
"content-length" => length = v.trim().parse().unwrap_or(usize::MAX), |
| 598 |
"authorization" => auth = v.trim().to_string(), |
| 599 |
"connection" => close = v.trim().eq_ignore_ascii_case("close"), |
| 600 |
_ => {} |
| 601 |
} |
| 602 |
} |
| 603 |
} |
| 604 |
if length > MAX_BODY { |
| 605 |
return respond(r.get_mut(), 400, "text/plain", b"too large"); |
| 606 |
} |
| 607 |
let mut body = vec![0; length]; |
| 608 |
r.read_exact(&mut body)?; |
| 609 |
if busy { |
| 610 |
return respond(r.get_mut(), 503, "text/plain", b"busy: ask again shortly"); |
| 611 |
} |
| 612 |
let (code, kind, reply) = route(state, &method, &path, &auth, &body); |
| 613 |
if code == 200 && kind == "application/octet-stream" { |
| 614 |
throttle(state, reply.len()); |
| 615 |
} |
| 616 |
respond(r.get_mut(), code, kind, &reply)?; |
| 617 |
if close { |
| 618 |
return Ok(()); |
| 619 |
} |
| 620 |
} |
| 621 |
} |
| 622 |
|
| 623 |
fn route(state: &State, method: &str, path: &str, auth: &str, body: &[u8]) -> (u16, &'static str, Vec<u8>) { |
| 624 |
let text = |code, s: &str| (code, "text/plain", s.as_bytes().to_vec()); |
| 625 |
if method == "POST" && path == "/swarm/v1/hello" { |
| 626 |
return match admit(state, &String::from_utf8_lossy(body)) { |
| 627 |
Ok(session) => (200, "application/json", serde_json::json!({ "session": session }).to_string().into_bytes()), |
| 628 |
Err(why) => text(403, &why), |
| 629 |
}; |
| 630 |
} |
| 631 |
let session = auth.strip_prefix("Bearer ").unwrap_or(""); |
| 632 |
let admitted = { |
| 633 |
let mut sessions = state.sessions.lock().unwrap(); |
| 634 |
let now = now_ms(); |
| 635 |
sessions.retain(|_, (_, until)| *until > now); |
| 636 |
sessions.contains_key(session) |
| 637 |
}; |
| 638 |
if !admitted { |
| 639 |
return text(401, "show a ticket at /swarm/v1/hello first"); |
| 640 |
} |
| 641 |
let rest = path.strip_prefix("/swarm/v1/").unwrap_or(""); |
| 642 |
match (method, rest.split('/').collect::<Vec<_>>().as_slice()) { |
| 643 |
("GET", ["images", image, "have"]) => match state.offers.lock().unwrap().get(*image) { |
| 644 |
Some(Offer { bits: None, .. }) => (200, "text/plain", b"complete".to_vec()), |
| 645 |
Some(Offer { bits: Some(b), fetching }) => (200, "application/x-have", [b.to_bytes(), fetching.to_bytes()].concat()), |
| 646 |
None => text(404, "this runner does not hold that image"), |
| 647 |
}, |
| 648 |
("GET", ["chunks", chunk]) if is_hash(chunk) => match read_chunk(state, chunk) { |
| 649 |
Some(b) => (200, "application/octet-stream", b), |
| 650 |
None => text(404, "no such chunk here"), |
| 651 |
}, |
| 652 |
_ => text(404, "not a swarm path"), |
| 653 |
} |
| 654 |
} |
| 655 |
|
| 656 |
|
| 657 |
|
| 658 |
fn admit(state: &State, ticket: &str) -> std::result::Result<String, String> { |
| 659 |
let hub = { |
| 660 |
let mut hub = state.hub.lock().unwrap(); |
| 661 |
if hub.is_none() { |
| 662 |
*hub = state.client.hub_identity().ok(); |
| 663 |
} |
| 664 |
hub.clone().ok_or("this runner cannot reach its hub to check tickets")? |
| 665 |
}; |
| 666 |
let t = Ticket::open(ticket, &hub, now_ms())?; |
| 667 |
if t.site != state.site { |
| 668 |
return Err(format!("this runner is in site {:?}, the ticket's runner in {:?}", state.site, t.site)); |
| 669 |
} |
| 670 |
let session = random_token().map_err(|e| format!("make a session: {e}"))?; |
| 671 |
state.sessions.lock().unwrap().insert(session.clone(), (t.name, t.expires_ms)); |
| 672 |
Ok(session) |
| 673 |
} |
| 674 |
|
| 675 |
fn random_token() -> std::io::Result<String> { |
| 676 |
let mut b = [0u8; 32]; |
| 677 |
std::fs::File::open("/dev/urandom")?.read_exact(&mut b)?; |
| 678 |
Ok(hex::encode(b)) |
| 679 |
} |
| 680 |
|
| 681 |
fn read_chunk(state: &State, hash: &str) -> Option<Vec<u8>> { |
| 682 |
if let Ok(b) = std::fs::read(cache_path(&state.root.join("chunks"), hash)) { |
| 683 |
return Some(b); |
| 684 |
} |
| 685 |
let (path, at, len) = state.located.lock().unwrap().get(hash).cloned()?; |
| 686 |
let mut f = std::fs::File::open(path).ok()?; |
| 687 |
f.seek(SeekFrom::Start(at)).ok()?; |
| 688 |
let mut b = vec![0; len as usize]; |
| 689 |
f.read_exact(&mut b).ok()?; |
| 690 |
Some(b) |
| 691 |
} |
| 692 |
|
| 693 |
|
| 694 |
fn throttle(state: &State, n: usize) { |
| 695 |
let wait = { |
| 696 |
let mut b = state.bucket.lock().unwrap(); |
| 697 |
let now = Instant::now(); |
| 698 |
b.0 = (b.0 + now.duration_since(b.1).as_secs_f64() * state.rate).min(state.rate); |
| 699 |
b.1 = now; |
| 700 |
b.0 -= n as f64; |
| 701 |
if b.0 < 0.0 { |
| 702 |
-b.0 / state.rate |
| 703 |
} else { |
| 704 |
0.0 |
| 705 |
} |
| 706 |
}; |
| 707 |
if wait > 0.0 { |
| 708 |
std::thread::sleep(Duration::from_secs_f64(wait)); |
| 709 |
} |
| 710 |
} |
| 711 |
|
| 712 |
#[cfg(test)] |
| 713 |
mod tests { |
| 714 |
use super::*; |
| 715 |
use oxy_core::identity::SecretKey; |
| 716 |
|
| 717 |
|
| 718 |
|
| 719 |
|
| 720 |
|
| 721 |
struct FakeHub { |
| 722 |
url: String, |
| 723 |
chunks_served: Arc<AtomicUsize>, |
| 724 |
runners: Arc<Mutex<Vec<(String, SwarmPeer)>>>, |
| 725 |
} |
| 726 |
|
| 727 |
fn fake_hub(m: Manifest, image: Vec<u8>, site: &str) -> FakeHub { |
| 728 |
slow_hub(m, image, site, Duration::ZERO) |
| 729 |
} |
| 730 |
|
| 731 |
|
| 732 |
fn slow_hub(m: Manifest, image: Vec<u8>, site: &str, delay: Duration) -> FakeHub { |
| 733 |
let sk = SecretKey::generate(); |
| 734 |
let id = sk.identity("hub", "hub@example", 1); |
| 735 |
let key = id.hash(); |
| 736 |
let mut chunks = HashMap::new(); |
| 737 |
let mut at = 0usize; |
| 738 |
for c in &m.chunks { |
| 739 |
chunks.insert(c.hash().to_string(), image[at..at + c.len()].to_vec()); |
| 740 |
at += c.len(); |
| 741 |
} |
| 742 |
let listener = TcpListener::bind("127.0.0.1:0").unwrap(); |
| 743 |
let url = format!("http://{}", listener.local_addr().unwrap()); |
| 744 |
let served = Arc::new(AtomicUsize::new(0)); |
| 745 |
let runners: Arc<Mutex<Vec<(String, SwarmPeer)>>> = Arc::new(Mutex::new(Vec::new())); |
| 746 |
let (count, known, site) = (served.clone(), runners.clone(), site.to_string()); |
| 747 |
let shared = Arc::new((m, chunks, id, key, sk)); |
| 748 |
std::thread::spawn(move || { |
| 749 |
for conn in listener.incoming() { |
| 750 |
let Ok(conn) = conn else { continue }; |
| 751 |
let (shared, count, known, site) = (shared.clone(), count.clone(), known.clone(), site.clone()); |
| 752 |
std::thread::spawn(move || { |
| 753 |
let (m, chunks, id, key, sk) = &*shared; |
| 754 |
let mut out = conn.try_clone().unwrap(); |
| 755 |
let mut r = BufReader::new(conn); |
| 756 |
loop { |
| 757 |
let mut line = String::new(); |
| 758 |
if r.read_line(&mut line).unwrap_or(0) == 0 { |
| 759 |
return; |
| 760 |
} |
| 761 |
let path = line.split_whitespace().nth(1).unwrap_or("").to_string(); |
| 762 |
let (mut len, mut token) = (0, String::new()); |
| 763 |
loop { |
| 764 |
let mut h = String::new(); |
| 765 |
r.read_line(&mut h).unwrap(); |
| 766 |
let h = h.trim_end().to_string(); |
| 767 |
if h.is_empty() { |
| 768 |
break; |
| 769 |
} |
| 770 |
let lower = h.to_ascii_lowercase(); |
| 771 |
if let Some(v) = lower.strip_prefix("content-length:") { |
| 772 |
len = v.trim().parse().unwrap(); |
| 773 |
} |
| 774 |
if let Some(v) = h.strip_prefix("Authorization: Bearer ") { |
| 775 |
token = v.to_string(); |
| 776 |
} |
| 777 |
} |
| 778 |
let mut body = vec![0; len]; |
| 779 |
r.read_exact(&mut body).unwrap(); |
| 780 |
let (code, reply): (u16, Vec<u8>) = if path == "/.well-known/oxyhub-key" { |
| 781 |
(200, id.encode()) |
| 782 |
} else if path == format!("/api/v1/flow/images/{}", m.hash) { |
| 783 |
(200, serde_json::to_vec(m).unwrap()) |
| 784 |
} else if let Some(c) = path.strip_prefix("/api/v1/flow/images/chunks/") { |
| 785 |
count.fetch_add(1, Ordering::Relaxed); |
| 786 |
std::thread::sleep(delay); |
| 787 |
(200, chunks[c].clone()) |
| 788 |
} else if path == "/api/v1/flow/swarm/announce" { |
| 789 |
let runners = known.lock().unwrap(); |
| 790 |
let me = runners.iter().find(|(t, _)| *t == token).map(|(_, p)| p.clone()).unwrap(); |
| 791 |
let ticket = Ticket { |
| 792 |
hub: key.to_hex(), |
| 793 |
runner: me.runner, |
| 794 |
name: me.name.clone(), |
| 795 |
site: site.clone(), |
| 796 |
expires_ms: now_ms() + 60_000, |
| 797 |
}; |
| 798 |
let peers: Vec<SwarmPeer> = runners.iter().filter(|(t, _)| *t != token).map(|(_, p)| p.clone()).collect(); |
| 799 |
let a = flow_core::api::Announced { ticket: ticket.seal(key, sk), peers, interval_s: 5 }; |
| 800 |
(200, serde_json::to_vec(&a).unwrap()) |
| 801 |
} else { |
| 802 |
(404, b"no".to_vec()) |
| 803 |
}; |
| 804 |
let head = format!("HTTP/1.1 {code} X\r\nContent-Length: {}\r\n\r\n", reply.len()); |
| 805 |
if out.write_all(head.as_bytes()).and_then(|_| out.write_all(&reply)).is_err() { |
| 806 |
return; |
| 807 |
} |
| 808 |
} |
| 809 |
}); |
| 810 |
} |
| 811 |
}); |
| 812 |
FakeHub { url, chunks_served: served, runners } |
| 813 |
} |
| 814 |
|
| 815 |
fn free_port() -> u16 { |
| 816 |
TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port() |
| 817 |
} |
| 818 |
|
| 819 |
|
| 820 |
|
| 821 |
fn runner(dir: &Path, hub: &FakeHub, id: i64, token: &str, port: u16, site: &str) -> (Home, Swarm) { |
| 822 |
let toml = format!( |
| 823 |
"hub = \"{}\"\ntoken = \"{token}\"\nname = \"r{id}\"\n[swarm]\nlisten = \"127.0.0.1:{port}\"\nsite = \"{site}\"\n", |
| 824 |
hub.url |
| 825 |
); |
| 826 |
std::fs::write(dir.join(crate::home::RUNNER_TOML), toml).unwrap(); |
| 827 |
let home = Home::locate(Some(dir)).unwrap(); |
| 828 |
let cfg = home.config.runner.clone().unwrap(); |
| 829 |
let swarm = Swarm::start(&home, &cfg).unwrap().unwrap(); |
| 830 |
hub.runners.lock().unwrap().push(( |
| 831 |
token.into(), |
| 832 |
SwarmPeer { |
| 833 |
runner: id, |
| 834 |
name: format!("r{id}"), |
| 835 |
addr: format!("127.0.0.1:{port}"), |
| 836 |
cert: swarm.addr.cert.clone(), |
| 837 |
complete: true, |
| 838 |
}, |
| 839 |
)); |
| 840 |
(home, swarm) |
| 841 |
} |
| 842 |
|
| 843 |
fn image() -> Vec<u8> { |
| 844 |
|
| 845 |
let mut x = 0x9e3779b97f4a7c15u64; |
| 846 |
let mut b: Vec<u8> = (0..3 << 20) |
| 847 |
.map(|_| { |
| 848 |
x ^= x << 13; |
| 849 |
x ^= x >> 7; |
| 850 |
x ^= x << 17; |
| 851 |
x as u8 |
| 852 |
}) |
| 853 |
.collect(); |
| 854 |
b[1 << 20..(1 << 20) + (300 << 10)].fill(0); |
| 855 |
b |
| 856 |
} |
| 857 |
|
| 858 |
#[test] |
| 859 |
fn a_runner_fetches_an_image_from_its_peer_and_not_the_hub() { |
| 860 |
let bytes = image(); |
| 861 |
let m = jet::images::scan(&bytes[..], Some("x86_64".into()), |_, _| Ok(())).unwrap(); |
| 862 |
let hub = fake_hub(m.clone(), bytes.clone(), "lab"); |
| 863 |
let (a_dir, b_dir) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap()); |
| 864 |
|
| 865 |
|
| 866 |
std::fs::create_dir_all(a_dir.path().join("cas")).unwrap(); |
| 867 |
std::fs::write(a_dir.path().join("cas").join(format!("{}.ext4", m.hash)), &bytes).unwrap(); |
| 868 |
let (_a_home, a) = runner(a_dir.path(), &hub, 1, "ta", free_port(), "lab"); |
| 869 |
let deadline = Instant::now() + Duration::from_secs(20); |
| 870 |
while !a.state.offers.lock().unwrap().contains_key(&m.hash) { |
| 871 |
assert!(Instant::now() < deadline, "runner A never offered its image"); |
| 872 |
std::thread::sleep(Duration::from_millis(20)); |
| 873 |
} |
| 874 |
let (b_home, b) = runner(b_dir.path(), &hub, 2, "tb", free_port(), "lab"); |
| 875 |
let moved = b.fetch(&b_home, &m.hash).unwrap().unwrap(); |
| 876 |
assert_eq!(std::fs::read(b_home.rootfs(&m.hash)).unwrap(), bytes, "the image came back whole"); |
| 877 |
assert_eq!(moved.peer_bytes, moved.bytes, "every chunk came from the peer"); |
| 878 |
assert_eq!(hub.chunks_served.load(Ordering::Relaxed), 0, "the hub sent no chunks"); |
| 879 |
assert_eq!(moved.chunks, m.needed().len()); |
| 880 |
|
| 881 |
assert!(matches!(b.state.offers.lock().unwrap().get(&m.hash), Some(Offer { bits: None, .. }))); |
| 882 |
} |
| 883 |
|
| 884 |
#[test] |
| 885 |
fn runners_starting_together_share_what_each_has_fetched() { |
| 886 |
let bytes = image(); |
| 887 |
let m = jet::images::scan(&bytes[..], None, |_, _| Ok(())).unwrap(); |
| 888 |
let needed = m.needed().len(); |
| 889 |
|
| 890 |
let hub = slow_hub(m.clone(), bytes.clone(), "lab", Duration::from_millis(400)); |
| 891 |
let (b_dir, c_dir) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap()); |
| 892 |
let (b_home, b) = runner(b_dir.path(), &hub, 2, "tb", free_port(), "lab"); |
| 893 |
let (c_home, c) = runner(c_dir.path(), &hub, 3, "tc", free_port(), "lab"); |
| 894 |
let (mb, mc) = std::thread::scope(|s| { |
| 895 |
let fb = s.spawn(|| b.fetch(&b_home, &m.hash).unwrap().unwrap()); |
| 896 |
let fc = s.spawn(|| c.fetch(&c_home, &m.hash).unwrap().unwrap()); |
| 897 |
(fb.join().unwrap(), fc.join().unwrap()) |
| 898 |
}); |
| 899 |
assert_eq!(std::fs::read(b_home.rootfs(&m.hash)).unwrap(), bytes); |
| 900 |
assert_eq!(std::fs::read(c_home.rootfs(&m.hash)).unwrap(), bytes); |
| 901 |
let served = hub.chunks_served.load(Ordering::Relaxed); |
| 902 |
assert!(served < 2 * needed, "the hub sent {served} chunks for two runners needing {needed} each"); |
| 903 |
assert!(mb.peer_bytes + mc.peer_bytes > 0, "they shared"); |
| 904 |
} |
| 905 |
|
| 906 |
#[test] |
| 907 |
fn without_peers_the_hub_sends_everything_and_a_ticket_from_another_site_is_refused() { |
| 908 |
let bytes = image(); |
| 909 |
let m = jet::images::scan(&bytes[..], None, |_, _| Ok(())).unwrap(); |
| 910 |
let hub = fake_hub(m.clone(), bytes.clone(), "lab"); |
| 911 |
let dir = tempfile::tempdir().unwrap(); |
| 912 |
let (home, s) = runner(dir.path(), &hub, 1, "t1", free_port(), "lab"); |
| 913 |
let moved = s.fetch(&home, &m.hash).unwrap().unwrap(); |
| 914 |
assert_eq!(moved.peer_bytes, 0); |
| 915 |
assert_eq!(hub.chunks_served.load(Ordering::Relaxed), m.needed().len()); |
| 916 |
assert_eq!(std::fs::read(home.rootfs(&m.hash)).unwrap(), bytes); |
| 917 |
|
| 918 |
|
| 919 |
let other = tempfile::tempdir().unwrap(); |
| 920 |
let port = free_port(); |
| 921 |
let (_h, o) = runner(other.path(), &hub, 3, "t3", port, "elsewhere"); |
| 922 |
let ticket = s.state.client.announce(&m.hash, "fetching").unwrap().ticket; |
| 923 |
let err = hello(&format!("127.0.0.1:{port}"), &o.addr.cert, &ticket).unwrap_err(); |
| 924 |
assert!(format!("{err:#}").contains("HTTP 403"), "{err:#}"); |
| 925 |
|
| 926 |
let url = format!("https://127.0.0.1:{port}/swarm/v1/chunks/{}", m.chunks[0].hash()); |
| 927 |
assert!(matches!(peer_agent(&o.addr.cert).unwrap().get(&url).call(), Err(ureq::Error::Status(401, _)))); |
| 928 |
|
| 929 |
assert!(ureq::get(&url.replacen("https", "http", 1)).call().is_err()); |
| 930 |
} |
| 931 |
|
| 932 |
#[test] |
| 933 |
fn a_peer_with_another_certificate_is_not_trusted() { |
| 934 |
let bytes = image(); |
| 935 |
let m = jet::images::scan(&bytes[..], None, |_, _| Ok(())).unwrap(); |
| 936 |
let hub = fake_hub(m.clone(), bytes.clone(), "lab"); |
| 937 |
let (a_dir, b_dir) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap()); |
| 938 |
std::fs::create_dir_all(a_dir.path().join("cas")).unwrap(); |
| 939 |
std::fs::write(a_dir.path().join("cas").join(format!("{}.ext4", m.hash)), &bytes).unwrap(); |
| 940 |
let (_a_home, a) = runner(a_dir.path(), &hub, 1, "ta", free_port(), "lab"); |
| 941 |
while !a.state.offers.lock().unwrap().contains_key(&m.hash) { |
| 942 |
std::thread::sleep(Duration::from_millis(20)); |
| 943 |
} |
| 944 |
let (b_home, b) = runner(b_dir.path(), &hub, 2, "tb", free_port(), "lab"); |
| 945 |
|
| 946 |
|
| 947 |
hub.runners.lock().unwrap()[0].1.cert = b.addr.cert.clone(); |
| 948 |
let moved = b.fetch(&b_home, &m.hash).unwrap().unwrap(); |
| 949 |
assert_eq!(moved.peer_bytes, 0, "nothing was taken from the stranger"); |
| 950 |
assert_eq!(hub.chunks_served.load(Ordering::Relaxed), m.needed().len()); |
| 951 |
assert_eq!(std::fs::read(b_home.rootfs(&m.hash)).unwrap(), bytes); |
| 952 |
|
| 953 |
let mut cfg = b_home.config.runner.clone().unwrap(); |
| 954 |
cfg.swarm.as_mut().unwrap().listen = "127.0.0.1:0".into(); |
| 955 |
let again = Swarm::start(&b_home, &cfg).unwrap().unwrap(); |
| 956 |
assert_eq!(again.addr.cert, b.addr.cert); |
| 957 |
} |
| 958 |
|
| 959 |
#[test] |
| 960 |
fn the_upload_rate_holds_back_a_burst() { |
| 961 |
let dir = tempfile::tempdir().unwrap(); |
| 962 |
let (cert, key) = certificate(dir.path()).unwrap(); |
| 963 |
let state = State { |
| 964 |
tls: server_tls(cert, key).unwrap(), |
| 965 |
root: PathBuf::new(), |
| 966 |
site: String::new(), |
| 967 |
client: Client::new("http://127.0.0.1:1", "t", None).unwrap(), |
| 968 |
hub: Mutex::new(None), |
| 969 |
offers: Mutex::new(HashMap::new()), |
| 970 |
located: Mutex::new(HashMap::new()), |
| 971 |
sessions: Mutex::new(HashMap::new()), |
| 972 |
conns: AtomicUsize::new(0), |
| 973 |
bucket: Mutex::new((1_000_000.0, Instant::now())), |
| 974 |
rate: 1_000_000.0, |
| 975 |
}; |
| 976 |
let start = Instant::now(); |
| 977 |
throttle(&state, 1_000_000); |
| 978 |
assert!(start.elapsed() < Duration::from_millis(100), "the first second's worth goes at once"); |
| 979 |
throttle(&state, 300_000); |
| 980 |
assert!(start.elapsed() >= Duration::from_millis(250), "then it waits for the rate"); |
| 981 |
} |
| 982 |
} |