1use std::collections::HashMap;
50use std::convert::Infallible;
51use std::fmt::Write as _;
52use std::net::SocketAddr;
53use std::path::{Path, PathBuf};
54use std::sync::Arc;
55use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
56
57use alloy::primitives::Address;
58use alloy::signers::local::PrivateKeySigner;
59use alloy::sol_types::Eip712Domain;
60use anyhow::Context as _;
61use bytes::Bytes;
62use decdn_client::bundle::{BundleManifest, parse_hash};
63use decdn_client::session::ChainConfig;
64use decdn_client::uri::{DecdnUri, UriTarget};
65use decdn_common::cli::{self, ClientFetchArgs, GatewayArgs};
66use decdn_common::config::load_file_config;
67use decdn_common::redact::{sanitize_err_chain, sanitize_error_sources};
68use decdn_incentive::payment_pool::PaymentPool;
69use decdn_incentive::{CapabilityGrant, PoolId, voucher_domain};
70use http_body_util::combinators::BoxBody;
71use http_body_util::{BodyExt as _, Full, Limited, StreamBody};
72use hyper::body::{Body as HttpBody, Frame};
73use hyper::service::service_fn;
74use hyper::{HeaderMap, Method, Request, Response, StatusCode, header};
75use hyper_util::rt::TokioIo;
76use tokio::io::{AsyncReadExt as _, DuplexStream};
77use tokio::net::TcpListener;
78use tokio::sync::{Mutex, oneshot};
79
80use super::fetch::{BlobFetch, Credential, MAX_MANIFEST_BYTES, manifest_cap_mb, resolve_chain};
81use crate::known_chains;
82
83mod worker;
84
85use worker::{FetchBackend, Job, PullWorker};
86
87pub const TOKEN_HEADER: &str = "x-decdn-token";
89
90pub const HOST_HEADER: &str = "x-decdn-host";
92
93pub const PENDING_TTL: Duration = Duration::from_mins(15);
95
96const MAX_DELEGATES_PER_SITE: usize = 8;
100
101const MAX_PAGE_DELEGATES: usize = 1024;
104
105const MAX_BODY_BYTES: usize = 64 * 1024;
107
108const GIVE_UP_AFTER_SECS: u64 = 120;
111
112const MAX_PULLS: usize = 16;
116
117const MAX_CACHED_MANIFESTS: usize = 16;
119
120const MANIFEST_CACHE_BYTES: usize = 64 << 20;
123
124const PIPE_BYTES: usize = 1 << 20;
126
127const CHUNK_BYTES: usize = 256 * 1024;
129
130type Body = BoxBody<Bytes, std::io::Error>;
131
132pub async fn gateway(args: &GatewayArgs, config_path: Option<&Path>) -> anyhow::Result<()> {
140 anyhow::ensure!(
141 args.listen.ip().is_loopback(),
142 "--listen must be a loopback address"
143 );
144 let mut common = keyless(&args.common)?;
145 fill_chain_defaults(&mut common, config_path)?;
146 let chain = resolve_chain(&common, &load_file_config(config_path)?)?.config;
147 common.give_up_after_secs = common.give_up_after_secs.or(Some(GIVE_UP_AFTER_SECS));
148 let root = common
149 .data_dir
150 .clone()
151 .unwrap_or_else(|| cli::common::expand_tilde(Path::new("~/.decdn/gateway")));
152 private_dir(&root)?;
153 let token = load_or_create_token(&root.join("token"))?;
154 let store_dir = tempfile::Builder::new()
157 .prefix("store-")
158 .tempdir_in(&root)
159 .context("create the gateway's buyer store dir")?;
160 private_dir(store_dir.path())?;
161 common.data_dir = Some(store_dir.path().to_path_buf());
162
163 let listener = TcpListener::bind(args.listen)
164 .await
165 .with_context(|| format!("listen on {}", args.listen))?;
166 let pulls = PullWorker::start(
167 FetchBackend {
168 common: common.clone(),
169 config_path: config_path.map(Path::to_path_buf),
170 },
171 MAX_PULLS,
172 )
173 .await
174 .context("start the gateway's pull worker")?;
175 let gateway = Arc::new(Gateway {
176 common,
177 token,
178 domain: voucher_domain(chain.chain_id, chain.payment_pool),
179 chain,
180 delegates: Mutex::new(HashMap::new()),
181 pulls,
182 manifests: std::sync::Mutex::new(ManifestCache::default()),
183 manifest_fill: Mutex::new(()),
184 hosts: allowed_hosts(args.listen),
185 });
186
187 println!(
188 "decdn gateway on http://{}/ (Ctrl-C to stop)\ngateway token: {} (also in {})",
189 args.listen,
190 gateway.token,
191 root.join("token").display()
192 );
193 let served = serve(&gateway, &listener).await;
194 gateway.pulls.stop().await;
196 served
197}
198
199async fn serve(gateway: &Arc<Gateway>, listener: &TcpListener) -> anyhow::Result<()> {
201 loop {
202 let (stream, _) = tokio::select! {
203 accepted = listener.accept() => accepted.context("accept a connection")?,
204 _ = tokio::signal::ctrl_c() => return Ok(()),
205 };
206 let gateway = Arc::clone(gateway);
207 tokio::spawn(async move {
208 let service = service_fn(move |req| {
209 let gateway = Arc::clone(&gateway);
210 async move { Ok::<_, Infallible>(gateway.handle(req).await) }
211 });
212 if let Err(e) = hyper::server::conn::http1::Builder::new()
213 .timer(hyper_util::rt::TokioTimer::new())
214 .header_read_timeout(Duration::from_secs(10))
215 .serve_connection(TokioIo::new(stream), service)
216 .await
217 {
218 tracing::debug!(error = %e, "gateway connection ended");
219 }
220 });
221 }
222}
223
224fn keyless(common: &ClientFetchArgs) -> anyhow::Result<ClientFetchArgs> {
228 anyhow::ensure!(
229 common.keystore.is_none()
230 && common.capability.is_none()
231 && common.capability_file.is_none(),
232 "the gateway holds its own delegate keys: register capabilities through \
233 `PUT /_capability` instead of --keystore / --capability / --capability-file"
234 );
235 let mut common = common.clone();
236 common.keystore_password_file = None;
237 Ok(common)
238}
239
240fn fill_chain_defaults(
243 common: &mut ClientFetchArgs,
244 config_path: Option<&Path>,
245) -> anyhow::Result<()> {
246 let file = load_file_config(config_path)?;
247 let bc = file.blockchain.as_ref();
248 let Some(chain) = known_chains::resolve(None)? else {
249 return Ok(());
250 };
251 let addresses = chain.addresses()?;
252 if common.rpc_url.is_none() && bc.and_then(|b| b.rpc_url.as_ref()).is_none() {
253 common.rpc_url = Some(chain.public_rpc.to_owned());
254 }
255 if common.chain_id.is_none() && bc.and_then(|b| b.chain_id).is_none() {
256 common.chain_id = Some(chain.chain_id);
257 }
258 if common.payment_pool_address.is_none()
259 && bc.and_then(|b| b.payment_pool_address.as_ref()).is_none()
260 {
261 common.payment_pool_address = Some(addresses.payment_pool);
262 }
263 if common.slash_judge_address.is_none()
264 && bc.and_then(|b| b.slash_judge_address.as_ref()).is_none()
265 {
266 common.slash_judge_address = Some(addresses.slash_judge);
267 }
268 if common.capacity_bond_address.is_none()
269 && bc.and_then(|b| b.capacity_bond_address.as_ref()).is_none()
270 {
271 common.capacity_bond_address = Some(addresses.capacity_bond);
272 }
273 Ok(())
274}
275
276fn allowed_hosts(listen: SocketAddr) -> Vec<String> {
279 let port = listen.port();
280 vec![
281 listen.to_string(),
282 format!("localhost:{port}"),
283 format!("127.0.0.1:{port}"),
284 format!("[::1]:{port}"),
285 ]
286}
287
288fn private_dir(dir: &Path) -> anyhow::Result<()> {
290 std::fs::create_dir_all(dir).with_context(|| format!("create {}", dir.display()))?;
291 #[cfg(unix)]
292 {
293 use std::os::unix::fs::PermissionsExt as _;
294 std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))
295 .with_context(|| format!("restrict {}", dir.display()))?;
296 }
297 Ok(())
298}
299
300fn load_or_create_token(path: &Path) -> anyhow::Result<String> {
303 if let Ok(token) = std::fs::read_to_string(path) {
304 let token = token.trim().to_owned();
305 anyhow::ensure!(
306 token.len() == 64 && token.bytes().all(|b| b.is_ascii_hexdigit()),
307 "{} is not a gateway token; delete it to make a new one",
308 path.display()
309 );
310 return Ok(token);
311 }
312 let token = alloy::hex::encode(rand::random::<[u8; 32]>());
313 std::fs::write(path, &token).with_context(|| format!("write {}", path.display()))?;
314 #[cfg(unix)]
315 {
316 use std::os::unix::fs::PermissionsExt as _;
317 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
318 .with_context(|| format!("restrict {}", path.display()))?;
319 }
320 Ok(token)
321}
322
323fn token_matches(token: &str, given: &str) -> bool {
325 token.len() == given.len()
326 && token
327 .bytes()
328 .zip(given.bytes())
329 .fold(0u8, |acc, (a, b)| acc | (a ^ b))
330 == 0
331}
332
333fn now_secs() -> u64 {
334 SystemTime::now()
335 .duration_since(UNIX_EPOCH)
336 .map_or(0, |d| d.as_secs())
337}
338
339#[derive(Debug, Clone, PartialEq, Eq)]
341enum Scope {
342 Any,
344 Domain(String),
346 Host(String),
348}
349
350impl Scope {
351 fn parse(s: &str) -> Result<Self, String> {
352 let s = s.trim().to_ascii_lowercase();
353 if s == "*" {
354 return Ok(Self::Any);
355 }
356 if let Some(domain) = s.strip_prefix("*.") {
357 return valid_host(domain).map(|()| Self::Domain(domain.to_owned()));
358 }
359 valid_host(&s).map(|()| Self::Host(s))
360 }
361
362 fn covers(&self, host: &str) -> bool {
363 match self {
364 Self::Any => true,
365 Self::Domain(domain) => {
366 host == domain
367 || host
368 .strip_suffix(domain.as_str())
369 .is_some_and(|prefix| prefix.ends_with('.'))
370 }
371 Self::Host(h) => host == h,
372 }
373 }
374
375 const fn rank(&self) -> u8 {
377 match self {
378 Self::Host(_) => 2,
379 Self::Domain(_) => 1,
380 Self::Any => 0,
381 }
382 }
383}
384
385impl std::fmt::Display for Scope {
386 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387 match self {
388 Self::Any => f.write_str("*"),
389 Self::Domain(d) => write!(f, "*.{d}"),
390 Self::Host(h) => f.write_str(h),
391 }
392 }
393}
394
395fn valid_host(host: &str) -> Result<(), String> {
396 let ok = !host.is_empty()
397 && host.len() <= 253
398 && host.split('.').all(|label| {
399 !label.is_empty()
400 && label.len() <= 63
401 && label
402 .bytes()
403 .all(|b| b.is_ascii_alphanumeric() || b == b'-')
404 });
405 if ok {
406 Ok(())
407 } else {
408 Err(format!("`{host}` is not a host name"))
409 }
410}
411
412fn origin_host(origin: &str) -> Option<String> {
416 let rest = origin
417 .strip_prefix("https://")
418 .or_else(|| origin.strip_prefix("http://"))?;
419 let host = rest.split(['/', ':']).next()?;
420 let host = host.to_ascii_lowercase();
421 valid_host(&host).ok().map(|()| host)
422}
423
424#[derive(Debug, PartialEq, Eq)]
426enum Caller {
427 Page { origin: String, host: String },
430 Token,
432}
433
434impl Caller {
435 fn of(origin: Option<&str>, authorized: bool) -> Result<Self, &'static str> {
436 match origin {
437 Some(origin) => {
438 if let Some(host) = origin_host(origin) {
439 Ok(Self::Page {
440 origin: origin.to_owned(),
441 host,
442 })
443 } else if origin == "null" {
444 Err("an opaque origin may not call the gateway\n")
445 } else if authorized {
446 Ok(Self::Token)
447 } else {
448 Err("token required\n")
449 }
450 }
451 None if authorized => Ok(Self::Token),
452 None => Err("a page calls with its Origin; use the gateway token otherwise\n"),
453 }
454 }
455
456 fn origin(&self) -> Option<&str> {
458 match self {
459 Self::Page { origin, .. } => Some(origin),
460 Self::Token => None,
461 }
462 }
463
464 fn site(&self) -> Option<String> {
466 match self {
467 Self::Page { host, .. } => Some(site(host)),
468 Self::Token => None,
469 }
470 }
471}
472
473fn site(host: &str) -> String {
477 if host.parse::<std::net::IpAddr>().is_ok() {
478 return host.to_owned();
479 }
480 psl::domain_str(host).unwrap_or(host).to_owned()
481}
482
483fn may_register(caller: &Caller, scope: &Scope) -> Result<(), String> {
487 let Caller::Page { host, .. } = caller else {
488 return Ok(());
489 };
490 if !scope.covers(host) {
491 return Err(format!(
492 "{host} may not register scope `{scope}`: it does not cover {host}"
493 ));
494 }
495 if let Scope::Domain(domain) = scope
496 && psl::suffix_str(domain) == Some(domain.as_str())
497 {
498 return Err(format!(
499 "{host} may not register scope `{scope}`: {domain} is a public suffix"
500 ));
501 }
502 Ok(())
503}
504
505fn owner_matches(on_chain: Address, signed_by: Address, pool_id: PoolId) -> Result<(), String> {
508 if on_chain.is_zero() {
509 Err(format!(
510 "pool {pool_id} does not exist on this deployment\n"
511 ))
512 } else if on_chain != signed_by {
513 Err(format!(
514 "the capability is not signed by the owner of pool {pool_id}\n"
515 ))
516 } else {
517 Ok(())
518 }
519}
520
521struct Delegate {
523 signer: Arc<PrivateKeySigner>,
524 minted: Instant,
525 site: Option<String>,
527 capability: Option<Registered>,
528}
529
530fn may_mint(delegates: &HashMap<Address, Delegate>, site: Option<&str>) -> Result<(), String> {
536 let Some(site) = site else {
537 return Ok(());
538 };
539 let pages = || delegates.values().filter(|d| d.site.is_some());
540 if pages().count() >= MAX_PAGE_DELEGATES {
541 return Err("too many pages hold delegates\n".to_owned());
542 }
543 if pages().filter(|d| d.site.as_deref() == Some(site)).count() >= MAX_DELEGATES_PER_SITE {
544 return Err(format!(
545 "pages of {site} hold {MAX_DELEGATES_PER_SITE} delegates\n"
546 ));
547 }
548 Ok(())
549}
550
551struct Registered {
552 grant: CapabilityGrant,
553 scope: Scope,
554 origin: Option<String>,
556 since: Instant,
558 spent: bool,
559}
560
561impl Registered {
562 const fn preference(&self) -> (u8, bool, std::cmp::Reverse<Instant>) {
567 (
568 self.scope.rank(),
569 self.origin.is_none(),
570 std::cmp::Reverse(self.since),
571 )
572 }
573}
574
575#[derive(serde::Deserialize)]
576struct RegisterRequest {
577 capability: String,
578 scope: String,
579}
580
581struct Gateway {
582 common: ClientFetchArgs,
583 token: String,
584 chain: ChainConfig,
586 domain: Eip712Domain,
588 delegates: Mutex<HashMap<Address, Delegate>>,
589 pulls: PullWorker,
591 manifests: std::sync::Mutex<ManifestCache>,
592 manifest_fill: Mutex<()>,
595 hosts: Vec<String>,
596}
597
598#[derive(Default)]
601struct ManifestCache {
602 entries: std::collections::VecDeque<([u8; 32], Arc<BundleManifest>, usize)>,
604 bytes: usize,
605}
606
607impl ManifestCache {
608 fn get(&mut self, hash: &[u8; 32]) -> Option<Arc<BundleManifest>> {
609 let at = self.entries.iter().position(|(h, _, _)| h == hash)?;
610 let entry = self.entries.remove(at)?;
611 let manifest = Arc::clone(&entry.1);
612 self.entries.push_back(entry);
613 Some(manifest)
614 }
615
616 fn insert(&mut self, hash: [u8; 32], manifest: Arc<BundleManifest>, size: usize) {
617 if let Some(at) = self.entries.iter().position(|(h, _, _)| *h == hash)
618 && let Some((_, _, old)) = self.entries.remove(at)
619 {
620 self.bytes = self.bytes.saturating_sub(old);
621 }
622 self.entries.push_back((hash, manifest, size));
623 self.bytes = self.bytes.saturating_add(size);
624 while self.entries.len() > 1
625 && (self.entries.len() > MAX_CACHED_MANIFESTS || self.bytes > MANIFEST_CACHE_BYTES)
626 {
627 if let Some((_, _, evicted)) = self.entries.pop_front() {
628 self.bytes = self.bytes.saturating_sub(evicted);
629 }
630 }
631 }
632}
633
634enum PullError {
636 Funding(anyhow::Error),
638 NotManifest(anyhow::Error),
641 Other(anyhow::Error),
642}
643
644struct Paying {
646 signer: Arc<PrivateKeySigner>,
647 grant: CapabilityGrant,
648}
649
650impl Gateway {
651 async fn handle<B>(self: Arc<Self>, req: Request<B>) -> Response<Body>
652 where
653 B: HttpBody<Data = Bytes>,
654 B::Error: std::error::Error + Send + Sync + 'static,
655 {
656 let host = req
657 .headers()
658 .get(header::HOST)
659 .and_then(|h| h.to_str().ok())
660 .unwrap_or("");
661 if !self.hosts.iter().any(|allowed| allowed == host) {
662 return text(StatusCode::FORBIDDEN, "unknown Host\n");
663 }
664 let origin = req
665 .headers()
666 .get(header::ORIGIN)
667 .and_then(|o| o.to_str().ok())
668 .map(str::to_owned);
669 let authorized = self.authorized(req.headers());
670 let path = req.uri().path().to_owned();
671 let method = req.method().clone();
672 match (method, path.as_str()) {
673 (Method::OPTIONS, "/_delegate" | "/_capability") => preflight(origin.as_deref()),
674 (Method::OPTIONS, _) => text(StatusCode::FORBIDDEN, "no cross-origin access\n"),
675 (Method::GET, "/") => html(
676 StatusCode::OK,
677 "<h1>decdn gateway</h1><p>Running. Connect the deCDN browser extension with \
678 the token <code>decdn gateway</code> printed when it started.</p>",
679 ),
680 (Method::GET, "/favicon.ico") => text(StatusCode::NOT_FOUND, ""),
681 (Method::GET, "/_delegate") => {
682 let response = match Caller::of(origin.as_deref(), authorized) {
683 Ok(caller) => self.mint(&caller).await,
684 Err(e) => text(StatusCode::FORBIDDEN, e),
685 };
686 with_cors(response, origin.as_deref())
687 }
688 (Method::PUT, "/_capability") => {
689 let response = match Caller::of(origin.as_deref(), authorized) {
690 Ok(caller) => self.register(req, &caller).await,
691 Err(e) => text(StatusCode::FORBIDDEN, e),
692 };
693 with_cors(response, origin.as_deref())
694 }
695 (Method::GET, "/_capability") if authorized => self.list().await,
696 (Method::GET, "/_capability") => text(StatusCode::UNAUTHORIZED, "token required\n"),
697 (Method::HEAD, _) => {
699 let mut response = text(StatusCode::METHOD_NOT_ALLOWED, "");
700 response
701 .headers_mut()
702 .insert(header::ALLOW, header::HeaderValue::from_static("GET"));
703 response
704 }
705 (Method::GET, _) => {
706 let target = req
707 .uri()
708 .path_and_query()
709 .map_or("/", |pq| pq.as_str())
710 .to_owned();
711 self.content(req.headers(), authorized, target.trim_start_matches('/'))
712 .await
713 }
714 _ => text(StatusCode::METHOD_NOT_ALLOWED, "method not allowed\n"),
715 }
716 }
717
718 fn authorized(&self, headers: &HeaderMap) -> bool {
719 headers
720 .get(TOKEN_HEADER)
721 .and_then(|t| t.to_str().ok())
722 .is_some_and(|given| token_matches(&self.token, given))
723 }
724
725 fn prune(delegates: &mut HashMap<Address, Delegate>) {
728 let now = now_secs();
729 delegates.retain(|_, d| match &d.capability {
730 None => d.minted.elapsed() < PENDING_TTL,
731 Some(registered) => registered.grant.expiry > now,
732 });
733 }
734
735 async fn mint(&self, caller: &Caller) -> Response<Body> {
736 let mut delegates = self.delegates.lock().await;
737 Self::prune(&mut delegates);
738 let site = caller.site();
739 if let Err(e) = may_mint(&delegates, site.as_deref()) {
740 return text(StatusCode::TOO_MANY_REQUESTS, &e);
741 }
742 let signer = PrivateKeySigner::random();
743 let address = signer.address();
744 delegates.insert(
745 address,
746 Delegate {
747 signer: Arc::new(signer),
748 minted: Instant::now(),
749 site,
750 capability: None,
751 },
752 );
753 json(
754 StatusCode::OK,
755 &serde_json::json!({ "address": address.to_string() }),
756 )
757 }
758
759 async fn register<B>(&self, req: Request<B>, caller: &Caller) -> Response<Body>
760 where
761 B: HttpBody<Data = Bytes>,
762 B::Error: std::error::Error + Send + Sync + 'static,
763 {
764 let body = match Limited::new(req.into_body(), MAX_BODY_BYTES)
765 .collect()
766 .await
767 {
768 Ok(body) => body.to_bytes(),
769 Err(_) => return text(StatusCode::PAYLOAD_TOO_LARGE, "body too large\n"),
770 };
771 let request: RegisterRequest = match serde_json::from_slice(&body) {
772 Ok(request) => request,
773 Err(e) => {
774 return text(
775 StatusCode::UNPROCESSABLE_ENTITY,
776 &format!("expected {{\"capability\", \"scope\"}}: {e}\n"),
777 );
778 }
779 };
780 let scope = match Scope::parse(&request.scope) {
781 Ok(scope) => scope,
782 Err(e) => return text(StatusCode::UNPROCESSABLE_ENTITY, &format!("{e}\n")),
783 };
784 if let Err(e) = may_register(caller, &scope) {
785 return text(StatusCode::FORBIDDEN, &format!("{e}\n"));
786 }
787 let grant = match CapabilityGrant::from_token(request.capability.trim()) {
788 Ok(grant) => grant,
789 Err(e) => return text(StatusCode::UNPROCESSABLE_ENTITY, &format!("{e}\n")),
790 };
791 if grant.expiry <= now_secs() {
792 return text(
793 StatusCode::UNPROCESSABLE_ENTITY,
794 "the capability has expired\n",
795 );
796 }
797 if let Err((status, e)) = self.verify_owner(&grant).await {
798 return text(status, &e);
799 }
800 let mut delegates = self.delegates.lock().await;
801 Self::prune(&mut delegates);
802 let Some(delegate) = delegates.get_mut(&grant.signer) else {
803 return text(
804 StatusCode::CONFLICT,
805 "the capability names no delegate this gateway is holding: get one from \
806 GET /_delegate\n",
807 );
808 };
809 if delegate.capability.is_some() {
810 return text(
811 StatusCode::CONFLICT,
812 "that delegate already holds a capability: get a new one from GET /_delegate\n",
813 );
814 }
815 let response = serde_json::json!({
816 "signer": grant.signer.to_string(),
817 "scope": scope.to_string(),
818 "expiry": grant.expiry,
819 });
820 delegate.capability = Some(Registered {
821 grant,
822 scope,
823 origin: caller.origin().map(str::to_owned),
824 since: Instant::now(),
825 spent: false,
826 });
827 json(StatusCode::CREATED, &response)
828 }
829
830 async fn verify_owner(&self, grant: &CapabilityGrant) -> Result<(), (StatusCode, String)> {
834 let unprocessable = |e: String| (StatusCode::UNPROCESSABLE_ENTITY, e);
835 let signed_by = grant
836 .owner(&self.domain)
837 .map_err(|e| unprocessable(format!("{e}\n")))?;
838 let unreachable = |e: String| {
839 (
840 StatusCode::BAD_GATEWAY,
841 format!("could not read the capability's pool on-chain: {e}\n"),
842 )
843 };
844 let rpc = decdn_client::provider::build_read_provider(&self.chain.rpc_url)
845 .map_err(|e| unreachable(sanitize_err_chain(&e)))?;
846 let pool = PaymentPool::new(self.chain.payment_pool, rpc)
847 .getPool(grant.pool_id)
848 .call()
849 .await
850 .map_err(|e| unreachable(sanitize_error_sources(&e)))?;
851 owner_matches(pool.owner, signed_by, grant.pool_id).map_err(unprocessable)
852 }
853
854 async fn list(&self) -> Response<Body> {
855 let mut delegates = self.delegates.lock().await;
856 Self::prune(&mut delegates);
857 let list: Vec<_> = delegates
858 .values()
859 .filter_map(|d| d.capability.as_ref())
860 .map(|r| {
861 serde_json::json!({
862 "signer": r.grant.signer.to_string(),
863 "pool": r.grant.pool_id.to_string(),
864 "scope": r.scope.to_string(),
865 "origin": r.origin,
866 "cap": r.grant.spending_cap,
867 "expiry": r.grant.expiry,
868 "spent": r.spent,
869 })
870 })
871 .collect();
872 json(StatusCode::OK, &serde_json::Value::Array(list))
873 }
874
875 async fn paying_for(&self, host: &str) -> Option<Paying> {
878 let mut delegates = self.delegates.lock().await;
879 Self::prune(&mut delegates);
880 delegates
881 .values()
882 .filter_map(|d| d.capability.as_ref().map(|r| (d, r)))
883 .filter(|(_, r)| !r.spent && r.scope.covers(host))
884 .max_by_key(|(_, r)| r.preference())
885 .map(|(d, r)| Paying {
886 signer: Arc::clone(&d.signer),
887 grant: r.grant.clone(),
888 })
889 }
890
891 async fn mark_spent(&self, signer: Address) {
895 let mut delegates = self.delegates.lock().await;
896 let Some(registered) = delegates
897 .get_mut(&signer)
898 .and_then(|d| d.capability.as_mut())
899 else {
900 return;
901 };
902 if registered.origin.is_some() {
903 delegates.remove(&signer);
904 } else {
905 registered.spent = true;
906 }
907 }
908
909 async fn content(
910 self: &Arc<Self>,
911 headers: &HeaderMap,
912 authorized: bool,
913 rest: &str,
914 ) -> Response<Body> {
915 if !authorized {
916 return text(StatusCode::UNAUTHORIZED, "token required\n");
917 }
918 let Some(page_host) = headers
919 .get(HOST_HEADER)
920 .and_then(|h| h.to_str().ok())
921 .map(str::to_ascii_lowercase)
922 else {
923 return text(StatusCode::UNAUTHORIZED, "page host required\n");
924 };
925 let uri: DecdnUri = match format!("decdn:{rest}").parse() {
926 Ok(uri) => uri,
927 Err(e) => return text(StatusCode::BAD_REQUEST, &format!("{e}\n")),
928 };
929 loop {
933 let Some(paying) = self.paying_for(&page_host).await else {
934 return text(
935 StatusCode::PAYMENT_REQUIRED,
936 &format!("no capability covers {page_host}\n"),
937 );
938 };
939 let result = match &uri.target {
940 UriTarget::Blob => {
941 self.stream(uri.hash, uri.namespace, &paying, None, None)
942 .await
943 }
944 UriTarget::BundleEntry(path) => {
945 self.entry(uri.hash, uri.namespace, path, &paying).await
946 }
947 UriTarget::Bundle => self.index(uri.hash, uri.namespace, &paying).await,
948 _ => return text(StatusCode::BAD_REQUEST, "unsupported URI target\n"),
949 };
950 match result {
951 Ok(response) => return response,
952 Err(PullError::Funding(e)) => {
953 tracing::info!(error = %e, host = %page_host, "a capability ran out");
954 self.mark_spent(paying.signer.address()).await;
955 }
956 Err(PullError::NotManifest(e)) => {
957 return text(StatusCode::NOT_FOUND, &format!("{e:#}\n"));
958 }
959 Err(PullError::Other(e)) => {
960 return text(StatusCode::BAD_GATEWAY, &format!("{e:#}\n"));
961 }
962 }
963 }
964 }
965
966 async fn entry(
968 self: &Arc<Self>,
969 manifest_hash: [u8; 32],
970 namespace: Option<u64>,
971 path: &str,
972 paying: &Paying,
973 ) -> Result<Response<Body>, PullError> {
974 let manifest = self.manifest(manifest_hash, namespace, paying).await?;
975 let Some(entry) = manifest.entries.iter().find(|e| e.path == path) else {
976 return Ok(text(
977 StatusCode::NOT_FOUND,
978 &format!("the bundle has no entry {path:?}\n"),
979 ));
980 };
981 let hash = parse_hash(&entry.hash)
982 .with_context(|| format!("bundle entry {path:?} has a malformed hash"))
983 .map_err(PullError::Other)?;
984 self.stream(hash, namespace, paying, Some(path), entry.size)
985 .await
986 }
987
988 async fn index(
990 self: &Arc<Self>,
991 manifest_hash: [u8; 32],
992 namespace: Option<u64>,
993 paying: &Paying,
994 ) -> Result<Response<Body>, PullError> {
995 let manifest = self.manifest(manifest_hash, namespace, paying).await?;
996 let mut rows = String::new();
997 for entry in &manifest.entries {
998 let link = DecdnUri::new(
999 manifest_hash,
1000 UriTarget::BundleEntry(entry.path.clone()),
1001 namespace,
1002 )
1003 .to_string();
1004 let size = entry.size.map_or_else(String::new, human_bytes);
1005 let _ = write!(
1006 rows,
1007 "<tr><td><a href=\"{}\">{}</a></td><td>{size}</td></tr>",
1008 escape(&link),
1009 escape(&entry.path),
1010 );
1011 }
1012 Ok(html(
1013 StatusCode::OK,
1014 &format!(
1015 "<h1>Bundle</h1><p><code>{}</code></p><table>{rows}</table>",
1016 escape(&DecdnUri::new(manifest_hash, UriTarget::Bundle, namespace).to_string())
1017 ),
1018 ))
1019 }
1020
1021 fn cached_manifest(&self, hash: &[u8; 32]) -> Option<Arc<BundleManifest>> {
1022 self.manifests
1023 .lock()
1024 .unwrap_or_else(std::sync::PoisonError::into_inner)
1025 .get(hash)
1026 }
1027
1028 async fn manifest(
1031 self: &Arc<Self>,
1032 hash: [u8; 32],
1033 namespace: Option<u64>,
1034 paying: &Paying,
1035 ) -> Result<Arc<BundleManifest>, PullError> {
1036 if let Some(manifest) = self.cached_manifest(&hash) {
1037 return Ok(manifest);
1038 }
1039 let _fill = self.manifest_fill.lock().await;
1040 if let Some(manifest) = self.cached_manifest(&hash) {
1043 return Ok(manifest);
1044 }
1045 let cap_mb = manifest_cap_mb(self.common.max_blob_mb);
1046 let raw = raw_blob_hint(hash, namespace);
1047 let raw_past_cap = (self.common.max_blob_mb == 0 || cap_mb < self.common.max_blob_mb)
1050 .then_some(raw.as_str());
1051 let (first, mut reader, done) = self
1052 .start_pull(hash, namespace, paying, cap_mb)
1053 .await
1054 .map_err(|e| over_manifest_cap(e, cap_mb, raw_past_cap))?;
1055 let mut bytes = first.to_vec();
1056 let mut buf = vec![0u8; CHUNK_BYTES];
1057 loop {
1058 let n = reader
1059 .read(&mut buf)
1060 .await
1061 .map_err(|e| PullError::Other(e.into()))?;
1062 if n == 0 {
1063 break;
1064 }
1065 bytes.extend_from_slice(buf.get(..n).unwrap_or_default());
1066 if bytes.len() > MAX_MANIFEST_BYTES {
1068 return Err(PullError::NotManifest(anyhow::anyhow!(
1069 "the blob is larger than {MAX_MANIFEST_BYTES} bytes, the most this \
1070 gateway reads as a bundle manifest"
1071 )));
1072 }
1073 }
1074 finished(done)
1075 .await
1076 .map_err(|e| over_manifest_cap(e, cap_mb, raw_past_cap))?;
1077 let manifest = Arc::new(
1079 BundleManifest::parse(&bytes)
1080 .with_context(|| format!("not a bundle manifest; {raw}"))
1081 .map_err(PullError::NotManifest)?,
1082 );
1083 self.manifests
1084 .lock()
1085 .unwrap_or_else(std::sync::PoisonError::into_inner)
1086 .insert(hash, Arc::clone(&manifest), bytes.len());
1087 Ok(manifest)
1088 }
1089
1090 async fn stream(
1093 self: &Arc<Self>,
1094 hash: [u8; 32],
1095 namespace: Option<u64>,
1096 paying: &Paying,
1097 path: Option<&str>,
1098 size: Option<u64>,
1099 ) -> Result<Response<Body>, PullError> {
1100 let (first, reader, done) = self
1101 .start_pull(hash, namespace, paying, self.common.max_blob_mb)
1102 .await?;
1103 let state = (Some(first), reader, Some(done));
1104 let gateway = Arc::clone(self);
1105 let signer = paying.signer.address();
1106 let body = futures_util::stream::unfold(state, move |(first, mut reader, done)| {
1107 let gateway = Arc::clone(&gateway);
1108 async move {
1109 if let Some(first) = first {
1110 return Some((Ok(Frame::data(first)), (None, reader, done)));
1111 }
1112 let mut buf = vec![0u8; CHUNK_BYTES];
1113 match reader.read(&mut buf).await {
1114 Ok(0) => match finished(done?).await {
1115 Ok(()) => None,
1116 Err(e) => {
1119 if matches!(e, PullError::Funding(_)) {
1120 gateway.mark_spent(signer).await;
1121 }
1122 Some((
1123 Err(std::io::Error::other(pull_error_text(&e))),
1124 (None, reader, None),
1125 ))
1126 }
1127 },
1128 Ok(n) => {
1129 buf.truncate(n);
1130 Some((Ok(Frame::data(Bytes::from(buf))), (None, reader, done)))
1131 }
1132 Err(e) => Some((Err(e), (None, reader, None))),
1133 }
1134 }
1135 });
1136 let name = path
1137 .and_then(|p| p.rsplit('/').next())
1138 .map_or_else(|| alloy::hex::encode(hash), str::to_owned);
1139 let mut response = Response::builder()
1140 .status(StatusCode::OK)
1141 .header(header::CONTENT_TYPE, content_type(&name))
1142 .header(
1143 header::CONTENT_DISPOSITION,
1144 format!("inline; filename=\"{}\"", name.replace(['"', '\\'], "_")),
1145 )
1146 .header(header::X_CONTENT_TYPE_OPTIONS, "nosniff");
1147 if let Some(size) = size {
1148 response = response.header(header::CONTENT_LENGTH, size);
1149 }
1150 response
1151 .body(StreamBody::new(body).boxed())
1152 .map_err(|e| PullError::Other(e.into()))
1153 }
1154
1155 async fn start_pull(
1159 self: &Arc<Self>,
1160 hash: [u8; 32],
1161 namespace: Option<u64>,
1162 paying: &Paying,
1163 max_blob_mb: u64,
1164 ) -> Result<(Bytes, DuplexStream, oneshot::Receiver<anyhow::Result<()>>), PullError> {
1165 let (writer, mut reader) = tokio::io::duplex(PIPE_BYTES);
1166 let (done_tx, done_rx) = oneshot::channel();
1167 let mut common = self.common.clone();
1168 common.max_blob_mb = max_blob_mb;
1169 self.pulls
1170 .pull(Job {
1171 blob: BlobFetch {
1172 hash,
1173 output: PathBuf::from("-"),
1174 namespace,
1175 common,
1176 },
1177 paying: Credential {
1178 signer: Arc::clone(&paying.signer),
1179 grant: paying.grant.clone(),
1180 },
1181 writer,
1182 done: done_tx,
1183 })
1184 .map_err(PullError::Other)?;
1185 let (first, done_rx) = first_chunk(&mut reader, done_rx).await?;
1186 Ok((first, reader, done_rx))
1187 }
1188}
1189
1190async fn first_chunk(
1196 reader: &mut DuplexStream,
1197 done_rx: oneshot::Receiver<anyhow::Result<()>>,
1198) -> Result<(Bytes, oneshot::Receiver<anyhow::Result<()>>), PullError> {
1199 let mut first = vec![0u8; CHUNK_BYTES];
1200 let n = reader
1201 .read(&mut first)
1202 .await
1203 .map_err(|e| PullError::Other(e.into()))?;
1204 if n > 0 {
1205 first.truncate(n);
1206 return Ok((Bytes::from(first), done_rx));
1207 }
1208 finished(done_rx).await?;
1209 let (spent_tx, spent_rx) = oneshot::channel();
1211 let _ = spent_tx.send(Ok(()));
1212 Ok((Bytes::new(), spent_rx))
1213}
1214
1215async fn finished(done: oneshot::Receiver<anyhow::Result<()>>) -> Result<(), PullError> {
1217 match done.await {
1218 Ok(Ok(())) => Ok(()),
1219 Ok(Err(e)) => Err(classify(e)),
1220 Err(_) => Err(PullError::Other(anyhow::anyhow!(
1223 "the pull ended without a result"
1224 ))),
1225 }
1226}
1227
1228fn classify(e: anyhow::Error) -> PullError {
1231 if e.downcast_ref::<decdn_client::FundingNeeded>().is_some() {
1232 PullError::Funding(e)
1233 } else {
1234 PullError::Other(e)
1235 }
1236}
1237
1238fn over_manifest_cap(e: PullError, cap_mb: u64, raw: Option<&str>) -> PullError {
1242 match e {
1243 PullError::Other(e) if e.downcast_ref::<decdn_client::BlobTooLarge>().is_some() => {
1244 let hint = raw.map_or_else(String::new, |raw| format!("; {raw}"));
1245 PullError::NotManifest(e.context(format!(
1246 "the blob is larger than {cap_mb} MiB, the most this gateway reads as a \
1247 bundle manifest{hint}"
1248 )))
1249 }
1250 e => e,
1251 }
1252}
1253
1254fn raw_blob_hint(hash: [u8; 32], namespace: Option<u64>) -> String {
1256 let uri = DecdnUri::new(hash, UriTarget::Blob, namespace).to_string();
1257 let path = uri.strip_prefix(decdn_client::uri::SCHEME).unwrap_or(&uri);
1258 format!("fetch `/{path}` for the raw blob")
1259}
1260
1261fn pull_error_text(e: &PullError) -> String {
1262 match e {
1263 PullError::Funding(e) | PullError::NotManifest(e) | PullError::Other(e) => {
1264 format!("{e:#}")
1265 }
1266 }
1267}
1268
1269fn preflight(origin: Option<&str>) -> Response<Body> {
1272 let Some(origin) = origin else {
1273 return text(StatusCode::FORBIDDEN, "no Origin\n");
1274 };
1275 let mut response = with_cors(text(StatusCode::NO_CONTENT, ""), Some(origin));
1276 let headers = response.headers_mut();
1277 headers.insert(
1278 header::ACCESS_CONTROL_ALLOW_METHODS,
1279 header::HeaderValue::from_static("GET, PUT"),
1280 );
1281 headers.insert(
1282 header::ACCESS_CONTROL_ALLOW_HEADERS,
1283 header::HeaderValue::from_static("content-type"),
1284 );
1285 headers.insert(
1286 header::ACCESS_CONTROL_MAX_AGE,
1287 header::HeaderValue::from_static("600"),
1288 );
1289 headers.insert(
1291 "access-control-allow-private-network",
1292 header::HeaderValue::from_static("true"),
1293 );
1294 response
1295}
1296
1297fn with_cors(mut response: Response<Body>, origin: Option<&str>) -> Response<Body> {
1299 if let Some(value) = origin.and_then(|o| header::HeaderValue::from_str(o).ok()) {
1300 let headers = response.headers_mut();
1301 headers.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, value);
1302 headers.insert(header::VARY, header::HeaderValue::from_static("Origin"));
1303 }
1304 response
1305}
1306
1307const ACTIVE_TYPES: &[&str] = &[
1310 "application/xhtml+xml",
1311 "image/svg+xml",
1312 "application/xml",
1313 "application/javascript",
1314 "application/ecmascript",
1315];
1316
1317fn content_type(name: &str) -> String {
1321 let guess = mime_guess::from_path(name).first_or_octet_stream();
1322 if guess.type_() == mime_guess::mime::TEXT || ACTIVE_TYPES.contains(&guess.essence_str()) {
1323 "text/plain; charset=utf-8".to_owned()
1324 } else {
1325 guess.essence_str().to_owned()
1326 }
1327}
1328
1329fn full(body: String) -> Body {
1330 Full::new(Bytes::from(body))
1331 .map_err(|never| match never {})
1332 .boxed()
1333}
1334
1335fn text(status: StatusCode, body: &str) -> Response<Body> {
1336 typed(status, body.to_owned(), "text/plain; charset=utf-8")
1337}
1338
1339fn json(status: StatusCode, value: &serde_json::Value) -> Response<Body> {
1340 typed(status, value.to_string(), "application/json")
1341}
1342
1343fn html(status: StatusCode, body: &str) -> Response<Body> {
1344 let page = format!(
1345 "<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" \
1346 content=\"width=device-width\"><title>decdn gateway</title><style>body{{font-family:\
1347 system-ui,sans-serif;max-width:48rem;margin:3rem auto;padding:0 1rem}}td{{padding:\
1348 .2rem 1rem .2rem 0}}</style></head><body>{body}</body></html>"
1349 );
1350 typed(status, page, "text/html; charset=utf-8")
1351}
1352
1353fn typed(status: StatusCode, body: String, content_type: &'static str) -> Response<Body> {
1354 let mut response = Response::new(full(body));
1355 *response.status_mut() = status;
1356 response.headers_mut().insert(
1357 header::CONTENT_TYPE,
1358 header::HeaderValue::from_static(content_type),
1359 );
1360 response
1361}
1362
1363fn escape(s: &str) -> String {
1364 s.replace('&', "&")
1365 .replace('<', "<")
1366 .replace('>', ">")
1367 .replace('"', """)
1368 .replace('\'', "'")
1369}
1370
1371fn human_bytes(n: u64) -> String {
1372 indicatif::HumanBytes(n).to_string()
1373}
1374
1375#[cfg(test)]
1376#[allow(
1377 clippy::unwrap_used,
1378 clippy::expect_used,
1379 clippy::indexing_slicing,
1380 clippy::panic
1381)]
1382mod tests;