use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use crate::utils::clock::Instant;
use super::codec::{self, BlockValue};
pub const MAX_TRANSFERS: usize = 32;
pub const TRANSFER_SERVE_FACTOR: usize = 2;
pub const MAX_UPLOADS: usize = 16;
pub const MAX_UPLOADS_PER_IP: usize = 2;
pub const TRANSFER_TTL: Duration = Duration::from_secs(60);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ResourceKey {
pub peer: SocketAddr,
pub path: String,
pub query: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Representation {
pub code: u8,
pub payload: Vec<u8>,
pub content_format: Option<u16>,
pub etag: Vec<u8>,
}
impl Representation {
pub fn new(code: u8, payload: Vec<u8>, content_format: Option<u16>) -> Representation {
let etag = etag_for(&payload, content_format);
Representation {
code,
payload,
content_format,
etag,
}
}
pub fn block(&self, block: BlockValue) -> Option<(&[u8], bool)> {
let offset = block.offset();
if offset >= self.payload.len() && !(offset == 0 && self.payload.is_empty()) {
return None;
}
let end = (offset + block.size()).min(self.payload.len());
Some((&self.payload[offset..end], end < self.payload.len()))
}
}
pub fn etag_for(payload: &[u8], content_format: Option<u16>) -> Vec<u8> {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
let cf = content_format.map_or([0xFF, 0xFF, 0xFF], |c| {
let b = c.to_be_bytes();
[1, b[0], b[1]]
});
for byte in cf.iter().chain(payload.iter()) {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash.to_be_bytes().to_vec()
}
#[derive(Debug)]
struct Cached {
cached: Instant,
rep: Arc<Representation>,
served: usize,
}
#[derive(Debug)]
pub struct Transfers {
entries: HashMap<ResourceKey, Cached>,
cap: usize,
ttl: Duration,
}
impl Default for Transfers {
fn default() -> Self {
Transfers::new(MAX_TRANSFERS, TRANSFER_TTL)
}
}
impl Transfers {
pub fn new(cap: usize, ttl: Duration) -> Transfers {
Transfers {
entries: HashMap::new(),
cap: cap.max(1),
ttl,
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
fn expire(&mut self, now: Instant) {
let ttl = self.ttl;
self.entries
.retain(|_, c| now.saturating_duration_since(c.cached) < ttl);
}
pub fn get(
&mut self,
key: &ResourceKey,
block: BlockValue,
now: Instant,
) -> Option<Arc<Representation>> {
self.expire(now);
let entry = self.entries.get_mut(key)?;
let len = entry.rep.block(block).map_or(0, |(slice, _)| slice.len());
let budget = entry
.rep
.payload
.len()
.saturating_mul(TRANSFER_SERVE_FACTOR);
if entry.served.saturating_add(len) > budget {
self.entries.remove(key);
return None;
}
entry.served += len;
Some(entry.rep.clone())
}
pub fn insert(
&mut self,
key: ResourceKey,
rep: Arc<Representation>,
served: usize,
now: Instant,
) -> Option<ResourceKey> {
self.expire(now);
if self
.entries
.get(&key)
.is_some_and(|c| Arc::ptr_eq(&c.rep, &rep))
{
return None;
}
let mut evicted = None;
if !self.entries.contains_key(&key) && self.entries.len() >= self.cap {
evicted = self
.entries
.iter()
.min_by_key(|(_, c)| c.cached)
.map(|(k, _)| k.clone());
if let Some(old) = &evicted {
self.entries.remove(old);
}
}
self.entries.insert(
key,
Cached {
cached: now,
rep,
served,
},
);
evicted
}
pub fn remove(&mut self, key: &ResourceKey) {
self.entries.remove(key);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UploadKey {
pub resource: ResourceKey,
pub method: u8,
}
#[derive(Debug)]
struct Upload {
body: Vec<u8>,
touched: Instant,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UploadStep {
Continue(BlockValue),
Complete {
body: Vec<u8>,
blocks: u32,
last: BlockValue,
},
Refuse {
code: u8,
size1: Option<usize>,
decision: &'static str,
reason: String,
},
}
#[derive(Debug)]
pub struct Uploads {
entries: HashMap<UploadKey, Upload>,
cap: usize,
per_ip: usize,
ttl: Duration,
max_body: usize,
}
impl Default for Uploads {
fn default() -> Self {
Uploads::new(
MAX_UPLOADS,
MAX_UPLOADS_PER_IP,
TRANSFER_TTL,
codec::MAX_REPRESENTATION_LEN,
)
}
}
impl Uploads {
pub fn new(cap: usize, per_ip: usize, ttl: Duration, max_body: usize) -> Uploads {
Uploads {
entries: HashMap::new(),
cap: cap.max(1),
per_ip: per_ip.max(1),
ttl,
max_body,
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
fn too_large(&self, what: String) -> UploadStep {
UploadStep::Refuse {
code: codec::CODE_REQUEST_ENTITY_TOO_LARGE,
size1: Some(self.max_body),
decision: "refused_upload_too_large",
reason: format!("{what}; this server takes at most {} bytes", self.max_body),
}
}
pub fn accept(
&mut self,
key: UploadKey,
block: BlockValue,
payload: &[u8],
size1: Option<u32>,
now: Instant,
) -> UploadStep {
let ttl = self.ttl;
self.entries
.retain(|_, u| now.saturating_duration_since(u.touched) < ttl);
if let Some(announced) = size1 {
if announced as usize > self.max_body {
self.entries.remove(&key);
return self.too_large(format!("the client announced {announced} bytes"));
}
}
if block.more && payload.len() != block.size() {
self.entries.remove(&key);
return UploadStep::Refuse {
code: codec::CODE_BAD_REQUEST,
size1: None,
decision: "refused_upload_bad_block",
reason: format!(
"block {} says more follow but carries {} bytes, not its size {}",
block.num,
payload.len(),
block.size()
),
};
}
if payload.len() > block.size() {
self.entries.remove(&key);
return UploadStep::Refuse {
code: codec::CODE_BAD_REQUEST,
size1: None,
decision: "refused_upload_bad_block",
reason: format!(
"block {} carries {} bytes, over its size {}",
block.num,
payload.len(),
block.size()
),
};
}
if block.num == 0 {
self.entries.remove(&key);
if !block.more {
return UploadStep::Complete {
body: payload.to_vec(),
blocks: 1,
last: block,
};
}
let ip = key.resource.peer.ip();
loop {
let held = self
.entries
.iter()
.filter(|(k, _)| k.resource.peer.ip() == ip);
if held.clone().count() < self.per_ip {
break;
}
let Some(oldest) = held.min_by_key(|(_, u)| u.touched).map(|(k, _)| k.clone())
else {
break;
};
self.entries.remove(&oldest);
tracing::info!(
"CoAP Block1 upload {} {}{} from {} evicted: {} uploads from that IP are \
in progress decision=upload_evicted_per_ip",
codec::method_name(oldest.method).unwrap_or("?"),
oldest.resource.path,
oldest
.resource
.query
.as_ref()
.map(|q| format!("?{q}"))
.unwrap_or_default(),
oldest.resource.peer,
self.per_ip
);
}
if self.entries.len() >= self.cap {
return UploadStep::Refuse {
code: codec::CODE_SERVICE_UNAVAILABLE,
size1: None,
decision: "refused_upload_table_full",
reason: format!("{} uploads are already in progress", self.cap),
};
}
self.entries.insert(
key,
Upload {
body: payload.to_vec(),
touched: now,
},
);
return UploadStep::Continue(block);
}
let expected = self.entries.get(&key).map(|u| u.body.len());
if expected != Some(block.offset()) {
self.entries.remove(&key);
return UploadStep::Refuse {
code: codec::CODE_REQUEST_ENTITY_INCOMPLETE,
size1: None,
decision: "refused_upload_out_of_order",
reason: match expected {
Some(have) => format!(
"block {} starts at byte {}, but {have} bytes have arrived",
block.num,
block.offset()
),
None => format!("block {} arrived with no upload in progress", block.num),
},
};
}
let upload = self.entries.get_mut(&key).expect("checked above");
if upload.body.len() + payload.len() > self.max_body {
let have = upload.body.len() + payload.len();
self.entries.remove(&key);
return self.too_large(format!("the upload reached {have} bytes"));
}
upload.body.extend_from_slice(payload);
upload.touched = now;
if block.more {
return UploadStep::Continue(block);
}
let upload = self.entries.remove(&key).expect("present above");
UploadStep::Complete {
body: upload.body,
blocks: block.num + 1,
last: block,
}
}
}