use std::{
fmt,
net::{IpAddr, Ipv4Addr, SocketAddr},
path::{Path, PathBuf},
time::Duration,
};
use hyphae_contracts::v1::ApiLimitsV1;
use hyphae_engine::{
MAX_DOCUMENT_BYTES, MAX_DOCUMENT_DEPTH, MAX_DOCUMENT_NODES, MAX_RESULT_PROOF_BYTES,
};
use hyphae_query::ExecutionLimits;
use hyphae_storage::MAX_KEY_BYTES;
use subtle::ConstantTimeEq;
use thiserror::Error;
const MIN_BEARER_TOKEN_BYTES: usize = 32;
const MAX_BEARER_TOKEN_BYTES: usize = 4_096;
pub const DEFAULT_PORT: u16 = 8_787;
#[derive(Clone)]
pub struct BearerToken {
digest: [u8; 32],
}
impl BearerToken {
pub fn new(secret: impl AsRef<[u8]>) -> Result<Self, ServerConfigError> {
let secret = secret.as_ref();
if !(MIN_BEARER_TOKEN_BYTES..=MAX_BEARER_TOKEN_BYTES).contains(&secret.len()) {
return Err(ServerConfigError::InvalidBearerTokenLength {
minimum: MIN_BEARER_TOKEN_BYTES,
maximum: MAX_BEARER_TOKEN_BYTES,
actual: secret.len(),
});
}
if !secret.iter().all(|byte| (0x21..=0x7e).contains(byte)) {
return Err(ServerConfigError::InvalidBearerTokenCharacter);
}
Ok(Self {
digest: *blake3::hash(secret).as_bytes(),
})
}
pub(crate) fn verifies(&self, candidate: &[u8]) -> bool {
if !(MIN_BEARER_TOKEN_BYTES..=MAX_BEARER_TOKEN_BYTES).contains(&candidate.len()) {
return false;
}
let candidate = *blake3::hash(candidate).as_bytes();
bool::from(self.digest.ct_eq(&candidate))
}
}
impl fmt::Debug for BearerToken {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("BearerToken([REDACTED])")
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ServerLimits {
pub request_body_bytes: usize,
pub json_depth: usize,
pub json_nodes: usize,
pub request_body_timeout: Duration,
pub batch_items: usize,
pub concurrent_operations: usize,
pub response_bytes: usize,
pub proof_bytes: usize,
pub witness_bytes: u64,
pub query: ExecutionLimits,
}
impl Default for ServerLimits {
fn default() -> Self {
Self {
request_body_bytes: 4 * 1024 * 1024,
json_depth: MAX_DOCUMENT_DEPTH,
json_nodes: 100_000,
request_body_timeout: Duration::from_secs(10),
batch_items: 1_000,
concurrent_operations: 16,
response_bytes: 32 * 1024 * 1024,
proof_bytes: 16 * 1024 * 1024,
witness_bytes: 512 * 1024 * 1024,
query: ExecutionLimits::default(),
}
}
}
impl ServerLimits {
pub(crate) fn validate(&self) -> Result<(), ServerConfigError> {
let scalar_limits = [
self.request_body_bytes,
self.json_depth,
self.json_nodes,
self.batch_items,
self.concurrent_operations,
self.response_bytes,
self.proof_bytes,
self.query.max_returned_records,
self.query.max_groups,
self.query.max_filter_nodes,
self.query.max_filter_depth,
self.query.max_sort_fields,
self.query.max_group_fields,
self.query.max_metrics,
];
if scalar_limits.contains(&0)
|| self.witness_bytes == 0
|| self.query.max_scanned_records == 0
|| self.query.max_matched_records == 0
|| self.request_body_timeout.is_zero()
|| self.query.timeout.is_zero()
{
return Err(ServerConfigError::ZeroLimit);
}
if self.json_depth > MAX_DOCUMENT_DEPTH {
return Err(ServerConfigError::JsonDepthTooLarge {
maximum: MAX_DOCUMENT_DEPTH,
actual: self.json_depth,
});
}
if self.json_nodes > MAX_DOCUMENT_NODES {
return Err(ServerConfigError::JsonNodesTooLarge {
maximum: MAX_DOCUMENT_NODES,
actual: self.json_nodes,
});
}
if u64::try_from(self.proof_bytes).unwrap_or(u64::MAX) > MAX_RESULT_PROOF_BYTES {
return Err(ServerConfigError::ProofLimitTooLarge {
maximum: MAX_RESULT_PROOF_BYTES,
actual: u64::try_from(self.proof_bytes).unwrap_or(u64::MAX),
});
}
Ok(())
}
pub(crate) fn as_contract(&self) -> ApiLimitsV1 {
ApiLimitsV1 {
key_bytes: usize_to_u64(MAX_KEY_BYTES),
document_bytes: usize_to_u64(MAX_DOCUMENT_BYTES),
request_body_bytes: usize_to_u64(self.request_body_bytes),
json_depth: usize_to_u64(self.json_depth),
json_nodes: usize_to_u64(self.json_nodes),
request_body_timeout_ms: duration_millis(self.request_body_timeout),
batch_items: usize_to_u64(self.batch_items),
scanned_records: self.query.max_scanned_records,
matched_records: self.query.max_matched_records,
result_rows: usize_to_u64(self.query.max_returned_records),
aggregation_groups: usize_to_u64(self.query.max_groups),
filter_nodes: usize_to_u64(self.query.max_filter_nodes),
filter_depth: usize_to_u64(self.query.max_filter_depth),
sort_fields: usize_to_u64(self.query.max_sort_fields),
group_fields: usize_to_u64(self.query.max_group_fields),
metrics: usize_to_u64(self.query.max_metrics),
concurrent_operations: usize_to_u64(self.concurrent_operations),
query_timeout_ms: duration_millis(self.query.timeout),
proof_bytes: usize_to_u64(self.proof_bytes),
witness_bytes: self.witness_bytes,
response_bytes: usize_to_u64(self.response_bytes),
}
}
}
#[derive(Clone, Debug)]
pub struct ServerConfig {
pub data_dir: PathBuf,
pub bind: SocketAddr,
pub bearer_token: Option<BearerToken>,
pub limits: ServerLimits,
}
impl ServerConfig {
pub fn new(data_dir: impl Into<PathBuf>) -> Self {
Self {
data_dir: data_dir.into(),
bind: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), DEFAULT_PORT),
bearer_token: None,
limits: ServerLimits::default(),
}
}
pub(crate) fn validate(&self) -> Result<(), ServerConfigError> {
if self.data_dir.as_os_str().is_empty() {
return Err(ServerConfigError::EmptyDataDirectory);
}
if !self.bind.ip().is_loopback() && self.bearer_token.is_none() {
return Err(ServerConfigError::RemoteBindRequiresAuthentication { bind: self.bind });
}
self.limits.validate()
}
pub(crate) fn data_dir(&self) -> &Path {
&self.data_dir
}
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum ServerConfigError {
#[error("server data-directory path must not be empty")]
EmptyDataDirectory,
#[error("non-loopback bind {bind} requires a bearer token")]
RemoteBindRequiresAuthentication {
bind: SocketAddr,
},
#[error("bearer token is {actual} bytes; required range is {minimum}..={maximum}")]
InvalidBearerTokenLength {
minimum: usize,
maximum: usize,
actual: usize,
},
#[error("bearer token must contain only visible ASCII without whitespace")]
InvalidBearerTokenCharacter,
#[error("server resource limits must be nonzero")]
ZeroLimit,
#[error("JSON depth limit {actual} exceeds canonical maximum {maximum}")]
JsonDepthTooLarge {
maximum: usize,
actual: usize,
},
#[error("JSON node limit {actual} exceeds canonical maximum {maximum}")]
JsonNodesTooLarge {
maximum: usize,
actual: usize,
},
#[error("proof limit {actual} exceeds canonical maximum {maximum}")]
ProofLimitTooLarge {
maximum: u64,
actual: u64,
},
}
fn usize_to_u64(value: usize) -> u64 {
u64::try_from(value).unwrap_or(u64::MAX)
}
fn duration_millis(value: Duration) -> u64 {
u64::try_from(value.as_millis()).unwrap_or(u64::MAX)
}