use std::collections::HashMap;
use std::io::{self, BufReader, Read};
use std::net::TcpStream;
use std::sync::atomic::Ordering::Relaxed;
use std::sync::PoisonError;
use std::time::Duration;
use crate::group;
use crate::server::{self, BodyError, BodyPlan, Drip, Request, Resp};
use crate::store::{lock, Outcome, ReqLog, Shared};
pub const SCENARIOS: &[&str] = &[
"atomic",
"partial-put",
"cut-body",
"drop-connection",
"freeze-upload",
"sizeless",
"slow",
"foreign-marker",
"markerless",
"auth-401",
"auth-403",
"doc-drift",
"flaky",
"rate-limit",
"redirect",
"readonly",
"no-service",
"search-400",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Scenario {
Atomic,
PartialPut { first_attempt_bytes: usize },
CutBody {
after_bytes: usize,
fake_length: bool,
},
DropConnection,
FreezeUpload,
Sizeless,
Slow {
chunk_delay_ms: u64,
chunk_size: usize,
},
ForeignMarker,
Markerless,
Auth401 { user: String, pass: String },
Auth403 { user: String, pass: String },
DocDrift,
Flaky { first_failures: u32 },
RateLimit {
first_429s: usize,
retry_after_secs: u64,
},
Redirect { location_path: String },
ReadOnly,
NoService,
Search400,
}
pub(crate) const IO_TIMEOUT: Duration = Duration::from_secs(30);
const FREEZE_HORIZON_SECS: u64 = 900;
const FAKE_LENGTH_LIE: usize = 1024;
const DRAIN_CAP: usize = 256 * 1024;
pub(crate) fn serve(shared: &Shared, stream: TcpStream) {
let _ = stream.set_read_timeout(Some(IO_TIMEOUT));
let _ = stream.set_write_timeout(Some(IO_TIMEOUT));
let mut stream = stream;
let head = match server::read_head_raw(&mut stream) {
Ok(Some(head)) => head,
Err(e) if e.kind() == io::ErrorKind::InvalidData => {
answer_bad_request(shared, &mut stream, "", "");
drain(&mut stream);
return;
}
_ => return,
};
let mut req = match server::parse_head(&head) {
Ok(req) => req,
Err(e) if e.kind() == io::ErrorKind::InvalidData => {
answer_bad_request(shared, &mut stream, "", "");
return;
}
Err(_) => return,
};
let path = normalize_path(&req.target);
if matches!(shared.scenario, Scenario::FreezeUpload) && req.method == "PUT" {
log_request(shared, &req.method, &path, Outcome::Stalled);
for _ in 0..FREEZE_HORIZON_SECS {
std::thread::sleep(Duration::from_secs(1));
}
return;
}
{
let mut seq = lock(&shared.first_request);
if matches!(shared.scenario, Scenario::DropConnection) && bump(&mut seq, &path) == 0 {
log_request(shared, &req.method, &path, Outcome::Reset);
return;
}
}
let mut reserved = [0u8; 1];
if stream.read_exact(&mut reserved).is_err() {
return;
}
let Ok(sock) = stream.try_clone() else { return };
let mut reader = BufReader::new(sock);
let plan = body_plan(shared, &req.method, &path);
if let Err(err) = server::read_body_into(&mut reader, &mut req, plan) {
match err {
BodyError::Malformed => answer_bad_request(shared, &mut stream, &req.method, &path),
BodyError::Aborted(received) => {
let outcome = if received == 0 {
Outcome::Reset
} else {
Outcome::PartialRead { bytes: received }
};
log_request(shared, &req.method, &path, outcome);
}
}
return;
}
handle(shared, &mut stream, &req, &path);
}
fn drain(stream: &mut TcpStream) {
let _ = stream.set_read_timeout(Some(Duration::from_millis(500)));
let mut sink = [0u8; 8192];
let mut drained = 0usize;
while drained < DRAIN_CAP {
match stream.read(&mut sink) {
Ok(0) | Err(_) => break,
Ok(n) => drained += n,
}
}
}
fn handle(shared: &Shared, stream: &mut TcpStream, req: &Request, path: &str) {
if !shared.members.is_empty() {
if matches!(req.method.as_str(), "GET" | "HEAD") {
group::get_first(shared, stream, req, path);
} else {
group::refuse_writes(shared, stream, req, path);
}
return;
}
let unauthenticated = shared
.auth_b64
.as_ref()
.is_some_and(|expected| !auth_decision(req.header("Authorization"), expected));
if unauthenticated {
let forbidden403 = matches!(shared.scenario, Scenario::Auth403 { .. });
let resp = if forbidden403 {
forbidden()
} else {
unauthorized()
};
log_request(shared, &req.method, path, Outcome::Status(resp.status));
let _ = server::write_response(stream, &bodyless_for_head(&req.method, resp));
return;
}
if shared.flaky_first.is_some_and(|k| {
let mut seq = lock(&shared.first_request);
bump(&mut seq, path) < k
}) {
log_request(shared, &req.method, path, Outcome::Status(503));
let _ = server::write_response(
stream,
&bodyless_for_head(&req.method, plain(503, b"flaky\n", None)),
);
return;
}
if let Some((first_429s, retry_after_secs)) = shared.rate_first {
let seen = {
let mut seq = lock(&shared.first_request);
bump(&mut seq, path)
};
if seen < first_429s as u32 {
log_request(shared, &req.method, path, Outcome::Status(429));
let mut resp = plain(429, b"", None);
resp.extra_headers
.push(("Retry-After", retry_after_secs.to_string()));
let _ = server::write_response(stream, &resp);
return;
}
}
if let Scenario::Redirect { location_path } = &shared.scenario {
if matches!(req.method.as_str(), "GET" | "HEAD") {
log_request(shared, &req.method, path, Outcome::Status(301));
let mut resp = plain(301, b"", None);
resp.extra_headers.push(("Location", location_path.clone()));
let _ = server::write_response(stream, &resp);
return;
}
}
if let Scenario::Search400 = shared.scenario {
if matches!(req.method.as_str(), "GET" | "HEAD") && path == "service/rest/v1/search/assets"
{
if let Some(repo) = query_param(&req.target, "repository") {
if !matches!(repo, "raw-main" | "raw-all") {
log_request(shared, &req.method, path, Outcome::Status(400));
let body = format!("unknown repository: {repo}\n");
let resp = plain(400, body.as_bytes(), None);
let _ = server::write_response(stream, &bodyless_for_head(&req.method, resp));
return;
}
}
}
}
match req.method.as_str() {
"GET" => {
if let Some(payload) = drift_or_store(shared, path) {
let total = payload.len();
match range_start(req.header("Range")) {
Some(start) if start >= total => {
log_request(shared, &req.method, path, Outcome::Status(416));
let mut resp = plain(416, b"range not satisfiable\n", shared.drip);
resp.extra_headers
.push(("Content-Range", format!("bytes */{total}")));
let _ = server::write_response(stream, &resp);
}
Some(start) => {
let body = payload[start..].to_vec();
log_request(shared, &req.method, path, Outcome::Status(206));
let resp = Resp {
status: 206,
extra_headers: vec![(
"Content-Range",
format!("bytes {start}-{}/{total}", total - 1),
)],
content_length: body.len(),
body,
drip: shared.drip,
hide_length: false,
};
let _ = server::write_response(stream, &resp);
}
None => {
log_request(shared, &req.method, path, Outcome::Status(200));
if let Some((after_bytes, fake_length)) = cut_plan(shared, path) {
let taken = after_bytes.min(total);
let mut resp = plain(200, &payload[..taken], shared.drip);
resp.content_length = if fake_length {
total + FAKE_LENGTH_LIE
} else {
total
};
let _ = server::write_response(stream, &resp);
return;
}
let resp = Resp {
status: 200,
extra_headers: Vec::new(),
content_length: total,
body: payload,
drip: shared.drip,
hide_length: shared.sizeless,
};
let _ = server::write_response(stream, &resp);
}
}
} else {
log_request(shared, &req.method, path, Outcome::Status(404));
let _ = server::write_response(stream, &plain(404, b"not found\n", shared.drip));
}
}
"HEAD" => {
let found = lock(&shared.store).get(path).map(Vec::len);
let (status, content_length) = match found {
Some(len) => (200, len),
None => (404, b"not found\n".len()),
};
log_request(shared, &req.method, path, Outcome::Status(status));
let resp = Resp {
status,
extra_headers: Vec::new(),
content_length,
body: Vec::new(),
drip: shared.drip,
hide_length: shared.sizeless && status == 200,
};
let _ = server::write_response(stream, &resp);
}
"PUT" => {
if let Some(bytes) = transform_put(shared, path, &req.body) {
lock(&shared.store).insert(path.to_owned(), bytes);
}
log_request(shared, &req.method, path, Outcome::Status(201));
let _ = server::write_response(stream, &plain(201, b"", None));
}
"DELETE" => {
if matches!(shared.scenario, Scenario::ReadOnly) {
log_request(shared, &req.method, path, Outcome::Status(403));
let _ = server::write_response(stream, &plain(403, b"read-only\n", None));
return;
}
let existed = lock(&shared.store).remove(path).is_some();
let (status, body): (u16, &[u8]) = if existed {
(204, b"")
} else {
(404, b"not found\n")
};
log_request(shared, &req.method, path, Outcome::Status(status));
let _ = server::write_response(stream, &plain(status, body, None));
}
_ => {
log_request(shared, &req.method, path, Outcome::Status(405));
let _ = server::write_response(
stream,
&bodyless_for_head(&req.method, plain(405, b"method not allowed\n", None)),
);
}
}
}
fn body_plan(shared: &Shared, method: &str, path: &str) -> BodyPlan {
let ("PUT", Some(cut)) = (method, shared.partial_first_put) else {
return BodyPlan::Full;
};
let mut seq = lock(&shared.first_put);
if bump(&mut seq, path) == 0 {
BodyPlan::Partial(cut)
} else {
BodyPlan::Full
}
}
fn cut_plan(shared: &Shared, path: &str) -> Option<(usize, bool)> {
let (cut, fake) = shared.cut_first_get?;
let mut seq = lock(&shared.first_get);
(bump(&mut seq, path) == 0).then_some((cut, fake))
}
fn bodyless_for_head(method: &str, mut resp: Resp) -> Resp {
if method == "HEAD" {
resp.body = Vec::new();
}
resp
}
fn drift_or_store(shared: &Shared, path: &str) -> Option<Vec<u8>> {
if shared.drift.load(Relaxed) && is_version_path(path) {
return Some(drift_doc(first_segment(path)));
}
lock(&shared.store).get(path).cloned()
}
fn transform_put(shared: &Shared, path: &str, body: &[u8]) -> Option<Vec<u8>> {
if !path.ends_with(".sha256") {
return Some(body.to_vec());
}
match shared.scenario {
Scenario::Markerless => None,
Scenario::ForeignMarker => Some(zero_digest(body)),
_ => Some(body.to_vec()),
}
}
pub(crate) fn plain(status: u16, body: &[u8], drip: Option<Drip>) -> Resp {
Resp {
status,
extra_headers: Vec::new(),
content_length: body.len(),
body: body.to_vec(),
drip,
hide_length: false,
}
}
fn unauthorized() -> Resp {
Resp {
status: 401,
extra_headers: vec![("WWW-Authenticate", "Basic realm=\"nexus\"".to_owned())],
content_length: b"auth required\n".len(),
body: b"auth required\n".to_vec(),
drip: None,
hide_length: false,
}
}
fn forbidden() -> Resp {
plain(403, b"forbidden\n", None)
}
fn answer_bad_request(shared: &Shared, stream: &mut TcpStream, method: &str, path: &str) {
log_request(shared, method, path, Outcome::Status(400));
let _ = server::write_response(stream, &plain(400, b"bad request\n", None));
}
pub(crate) fn log_request(shared: &Shared, method: &str, path: &str, outcome: Outcome) {
shared
.log
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(ReqLog {
method: method.to_owned(),
path: path.to_owned(),
outcome,
});
}
fn bump(counters: &mut HashMap<String, u32>, path: &str) -> u32 {
let seen = counters.entry(path.to_owned()).or_insert(0);
let prev = *seen;
*seen += 1;
prev
}
fn normalize_path(target: &str) -> String {
let no_query = target.split('?').next().unwrap_or(target);
no_query.strip_prefix('/').unwrap_or(no_query).to_owned()
}
fn query_param<'a>(target: &'a str, name: &str) -> Option<&'a str> {
let query = target.split_once('?')?.1;
query.split('&').find_map(|pair| {
let (key, value) = pair.split_once('=')?;
(key == name).then_some(value)
})
}
fn range_start(header: Option<&str>) -> Option<usize> {
let value = header?;
let (unit, spec) = value.split_once('=')?;
if !unit.trim().eq_ignore_ascii_case("bytes") {
return None;
}
if spec.contains(',') {
return None;
}
let (start, end) = spec.split_once('-')?;
if !end.trim().is_empty() {
return None;
}
start.trim().parse().ok()
}
fn is_version_path(path: &str) -> bool {
path.ends_with("/version.json")
}
fn first_segment(path: &str) -> &str {
path.split('/').next().unwrap_or_default()
}
fn drift_doc(version: &str) -> Vec<u8> {
format!("{{\"schema_version\":1,\"version\":\"{version}\",\"artifacts\":[\"ghost.zip\"]}}")
.into_bytes()
}
fn zero_digest(marker: &[u8]) -> Vec<u8> {
let line_end = marker
.iter()
.position(|&b| b == b'\n')
.unwrap_or(marker.len());
let mut out = marker.to_vec();
if line_end >= 64 {
out[..64].fill(b'0');
}
out
}
fn auth_decision(header: Option<&str>, expected_b64: &str) -> bool {
let Some(value) = header else { return false };
let Some((scheme, token)) = value.split_once(' ') else {
return false;
};
scheme.eq_ignore_ascii_case("basic") && token.trim() == expected_b64
}
#[cfg(test)]
mod tests {
use super::{
auth_decision, drift_doc, first_segment, is_version_path, normalize_path, range_start,
zero_digest,
};
#[test]
fn zeroes_64_char_digest_of_marker() {
let marker = format!("{} sample.zip\n", "a".repeat(64));
let out = zero_digest(marker.as_bytes());
assert_eq!(
out,
format!("{} sample.zip\n", "0".repeat(64)).into_bytes()
);
}
#[test]
fn short_marker_is_stored_verbatim() {
assert_eq!(zero_digest(b"tooshort\n"), b"tooshort\n");
}
#[test]
fn drift_doc_has_foreign_artifact_and_path_version() {
assert_eq!(
drift_doc("1.14.0"),
br#"{"schema_version":1,"version":"1.14.0","artifacts":["ghost.zip"]}"#.to_vec()
);
}
#[test]
fn version_paths_and_segments() {
assert!(is_version_path("1.14.0/version.json"));
assert!(!is_version_path("1.14.0/version.json.sha256"));
assert!(!is_version_path("version.json"));
assert_eq!(first_segment("1.14.0/version.json"), "1.14.0");
}
#[test]
fn normalize_strips_query_and_leading_slash() {
assert_eq!(normalize_path("/v/a%20b?x=1"), "v/a%20b");
assert_eq!(normalize_path("plain"), "plain");
}
#[test]
fn auth_decision_table() {
assert!(auth_decision(Some("Basic Y2k6c2VjcmV0"), "Y2k6c2VjcmV0"));
assert!(auth_decision(Some("basic Y2k6c2VjcmV0"), "Y2k6c2VjcmV0"));
assert!(!auth_decision(None, "Y2k6c2VjcmV0"));
assert!(!auth_decision(Some("Bearer Y2k6c2VjcmV0"), "Y2k6c2VjcmV0"));
assert!(!auth_decision(Some("Basic d3Jvbmc="), "Y2k6c2VjcmV0"));
assert!(!auth_decision(Some("Basic"), "Y2k6c2VjcmV0"));
}
#[test]
fn range_start_accepts_only_single_open_bytes_range() {
assert_eq!(range_start(Some("bytes=5-")), Some(5));
assert_eq!(range_start(Some("bytes=0-")), Some(0));
assert_eq!(range_start(Some("Bytes=12-")), Some(12));
assert_eq!(range_start(Some("bytes=0-4")), None);
assert_eq!(range_start(Some("bytes=-5")), None);
assert_eq!(range_start(Some("bytes=0-4,10-")), None);
assert_eq!(range_start(Some("items=5-")), None);
assert_eq!(range_start(Some("bytes=")), None);
assert_eq!(range_start(Some("bytes=q-")), None);
assert_eq!(range_start(None), None);
}
}