1use std::{
4 fmt,
5 net::{IpAddr, Ipv4Addr, SocketAddr},
6 path::{Path, PathBuf},
7 time::Duration,
8};
9
10use hyphae_contracts::v1::ApiLimitsV1;
11use hyphae_engine::{
12 MAX_DOCUMENT_BYTES, MAX_DOCUMENT_DEPTH, MAX_DOCUMENT_NODES, MAX_RESULT_PROOF_BYTES,
13};
14use hyphae_query::ExecutionLimits;
15use hyphae_storage::MAX_KEY_BYTES;
16use subtle::ConstantTimeEq;
17use thiserror::Error;
18
19const MIN_BEARER_TOKEN_BYTES: usize = 32;
20const MAX_BEARER_TOKEN_BYTES: usize = 4_096;
21
22pub const DEFAULT_PORT: u16 = 8_787;
24
25#[derive(Clone)]
27pub struct BearerToken {
28 digest: [u8; 32],
29}
30
31impl BearerToken {
32 pub fn new(secret: impl AsRef<[u8]>) -> Result<Self, ServerConfigError> {
40 let secret = secret.as_ref();
41 if !(MIN_BEARER_TOKEN_BYTES..=MAX_BEARER_TOKEN_BYTES).contains(&secret.len()) {
42 return Err(ServerConfigError::InvalidBearerTokenLength {
43 minimum: MIN_BEARER_TOKEN_BYTES,
44 maximum: MAX_BEARER_TOKEN_BYTES,
45 actual: secret.len(),
46 });
47 }
48 if !secret.iter().all(|byte| (0x21..=0x7e).contains(byte)) {
49 return Err(ServerConfigError::InvalidBearerTokenCharacter);
50 }
51 Ok(Self {
52 digest: *blake3::hash(secret).as_bytes(),
53 })
54 }
55
56 pub(crate) fn verifies(&self, candidate: &[u8]) -> bool {
57 if !(MIN_BEARER_TOKEN_BYTES..=MAX_BEARER_TOKEN_BYTES).contains(&candidate.len()) {
58 return false;
59 }
60 let candidate = *blake3::hash(candidate).as_bytes();
61 bool::from(self.digest.ct_eq(&candidate))
62 }
63}
64
65impl fmt::Debug for BearerToken {
66 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67 formatter.write_str("BearerToken([REDACTED])")
68 }
69}
70
71#[derive(Clone, Debug, Eq, PartialEq)]
73pub struct ServerLimits {
74 pub request_body_bytes: usize,
76 pub json_depth: usize,
78 pub json_nodes: usize,
80 pub request_body_timeout: Duration,
82 pub batch_items: usize,
84 pub concurrent_operations: usize,
86 pub response_bytes: usize,
88 pub proof_bytes: usize,
90 pub witness_bytes: u64,
92 pub query: ExecutionLimits,
94}
95
96impl Default for ServerLimits {
97 fn default() -> Self {
98 Self {
99 request_body_bytes: 4 * 1024 * 1024,
100 json_depth: MAX_DOCUMENT_DEPTH,
101 json_nodes: 100_000,
102 request_body_timeout: Duration::from_secs(10),
103 batch_items: 1_000,
104 concurrent_operations: 16,
105 response_bytes: 32 * 1024 * 1024,
106 proof_bytes: 16 * 1024 * 1024,
107 witness_bytes: 512 * 1024 * 1024,
108 query: ExecutionLimits::default(),
109 }
110 }
111}
112
113impl ServerLimits {
114 pub(crate) fn validate(&self) -> Result<(), ServerConfigError> {
115 let scalar_limits = [
116 self.request_body_bytes,
117 self.json_depth,
118 self.json_nodes,
119 self.batch_items,
120 self.concurrent_operations,
121 self.response_bytes,
122 self.proof_bytes,
123 self.query.max_returned_records,
124 self.query.max_groups,
125 self.query.max_filter_nodes,
126 self.query.max_filter_depth,
127 self.query.max_sort_fields,
128 self.query.max_group_fields,
129 self.query.max_metrics,
130 ];
131 if scalar_limits.contains(&0)
132 || self.witness_bytes == 0
133 || self.query.max_scanned_records == 0
134 || self.query.max_matched_records == 0
135 || self.request_body_timeout.is_zero()
136 || self.query.timeout.is_zero()
137 {
138 return Err(ServerConfigError::ZeroLimit);
139 }
140 if self.json_depth > MAX_DOCUMENT_DEPTH {
141 return Err(ServerConfigError::JsonDepthTooLarge {
142 maximum: MAX_DOCUMENT_DEPTH,
143 actual: self.json_depth,
144 });
145 }
146 if self.json_nodes > MAX_DOCUMENT_NODES {
147 return Err(ServerConfigError::JsonNodesTooLarge {
148 maximum: MAX_DOCUMENT_NODES,
149 actual: self.json_nodes,
150 });
151 }
152 if u64::try_from(self.proof_bytes).unwrap_or(u64::MAX) > MAX_RESULT_PROOF_BYTES {
153 return Err(ServerConfigError::ProofLimitTooLarge {
154 maximum: MAX_RESULT_PROOF_BYTES,
155 actual: u64::try_from(self.proof_bytes).unwrap_or(u64::MAX),
156 });
157 }
158 Ok(())
159 }
160
161 pub(crate) fn as_contract(&self) -> ApiLimitsV1 {
162 ApiLimitsV1 {
163 key_bytes: usize_to_u64(MAX_KEY_BYTES),
164 document_bytes: usize_to_u64(MAX_DOCUMENT_BYTES),
165 request_body_bytes: usize_to_u64(self.request_body_bytes),
166 json_depth: usize_to_u64(self.json_depth),
167 json_nodes: usize_to_u64(self.json_nodes),
168 request_body_timeout_ms: duration_millis(self.request_body_timeout),
169 batch_items: usize_to_u64(self.batch_items),
170 scanned_records: self.query.max_scanned_records,
171 matched_records: self.query.max_matched_records,
172 result_rows: usize_to_u64(self.query.max_returned_records),
173 aggregation_groups: usize_to_u64(self.query.max_groups),
174 filter_nodes: usize_to_u64(self.query.max_filter_nodes),
175 filter_depth: usize_to_u64(self.query.max_filter_depth),
176 sort_fields: usize_to_u64(self.query.max_sort_fields),
177 group_fields: usize_to_u64(self.query.max_group_fields),
178 metrics: usize_to_u64(self.query.max_metrics),
179 concurrent_operations: usize_to_u64(self.concurrent_operations),
180 query_timeout_ms: duration_millis(self.query.timeout),
181 proof_bytes: usize_to_u64(self.proof_bytes),
182 witness_bytes: self.witness_bytes,
183 response_bytes: usize_to_u64(self.response_bytes),
184 }
185 }
186}
187
188#[derive(Clone, Debug)]
190pub struct ServerConfig {
191 pub data_dir: PathBuf,
193 pub bind: SocketAddr,
195 pub bearer_token: Option<BearerToken>,
197 pub limits: ServerLimits,
199}
200
201impl ServerConfig {
202 pub fn new(data_dir: impl Into<PathBuf>) -> Self {
204 Self {
205 data_dir: data_dir.into(),
206 bind: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), DEFAULT_PORT),
207 bearer_token: None,
208 limits: ServerLimits::default(),
209 }
210 }
211
212 pub(crate) fn validate(&self) -> Result<(), ServerConfigError> {
213 if self.data_dir.as_os_str().is_empty() {
214 return Err(ServerConfigError::EmptyDataDirectory);
215 }
216 if !self.bind.ip().is_loopback() && self.bearer_token.is_none() {
217 return Err(ServerConfigError::RemoteBindRequiresAuthentication { bind: self.bind });
218 }
219 self.limits.validate()
220 }
221
222 pub(crate) fn data_dir(&self) -> &Path {
223 &self.data_dir
224 }
225}
226
227#[derive(Clone, Debug, Error, Eq, PartialEq)]
229pub enum ServerConfigError {
230 #[error("server data-directory path must not be empty")]
232 EmptyDataDirectory,
233 #[error("non-loopback bind {bind} requires a bearer token")]
235 RemoteBindRequiresAuthentication {
236 bind: SocketAddr,
238 },
239 #[error("bearer token is {actual} bytes; required range is {minimum}..={maximum}")]
241 InvalidBearerTokenLength {
242 minimum: usize,
244 maximum: usize,
246 actual: usize,
248 },
249 #[error("bearer token must contain only visible ASCII without whitespace")]
251 InvalidBearerTokenCharacter,
252 #[error("server resource limits must be nonzero")]
254 ZeroLimit,
255 #[error("JSON depth limit {actual} exceeds canonical maximum {maximum}")]
257 JsonDepthTooLarge {
258 maximum: usize,
260 actual: usize,
262 },
263 #[error("JSON node limit {actual} exceeds canonical maximum {maximum}")]
265 JsonNodesTooLarge {
266 maximum: usize,
268 actual: usize,
270 },
271 #[error("proof limit {actual} exceeds canonical maximum {maximum}")]
273 ProofLimitTooLarge {
274 maximum: u64,
276 actual: u64,
278 },
279}
280
281fn usize_to_u64(value: usize) -> u64 {
282 u64::try_from(value).unwrap_or(u64::MAX)
283}
284
285fn duration_millis(value: Duration) -> u64 {
286 u64::try_from(value.as_millis()).unwrap_or(u64::MAX)
287}