1#![doc = include_str!("../README.md")]
53#![deny(unsafe_code)]
54#![warn(rust_2018_idioms)]
55
56pub mod cli;
58
59use std::path::{Path, PathBuf};
60use std::sync::Arc;
61
62use actix_web::body::{BoxBody, EitherBody};
63use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
64use actix_web::http::{header, StatusCode};
65use actix_web::middleware::{NormalizePath, TrailingSlash};
66use actix_web::{web, App, Error as ActixError, HttpRequest, HttpResponse};
67use bytes::Bytes;
68use futures_util::future::{ready, LocalBoxFuture, Ready};
69use percent_encoding::percent_decode_str;
70use serde::Deserialize;
71use solid_pod_rs::{
72 auth::nip98,
73 config::sources::parse_size,
74 interop,
75 ldp::{self, LdpContainerOps, PatchCreateOutcome},
76 mashlib::{self, MashlibConfig},
77 provision,
78 security::DotfileAllowlist,
79 storage::Storage,
80 wac::{
81 self, conditions::RequestContext, parse_jsonld_acl, parser::parse_turtle_acl, AccessMode,
82 },
83 PodError,
84};
85
86#[derive(Clone)]
92pub struct AppState {
93 pub storage: Arc<dyn Storage>,
94 pub dotfiles: Arc<DotfileAllowlist>,
95 pub body_cap: usize,
96 pub nodeinfo: NodeInfoMeta,
97 pub mashlib: MashlibConfig,
98 pub mashlib_cdn: Option<String>,
101 pub pay_config: solid_pod_rs::payments::PayConfig,
104}
105
106#[derive(Clone, Debug)]
108pub struct NodeInfoMeta {
109 pub software_name: String,
110 pub software_version: String,
111 pub open_registrations: bool,
112 pub total_users: u64,
113 pub base_url: String,
114}
115
116impl Default for NodeInfoMeta {
117 fn default() -> Self {
118 Self {
119 software_name: "solid-pod-rs-server".to_string(),
120 software_version: env!("CARGO_PKG_VERSION").to_string(),
121 open_registrations: false,
122 total_users: 0,
123 base_url: "http://localhost".to_string(),
124 }
125 }
126}
127
128pub const DEFAULT_BODY_CAP: usize = 50 * 1024 * 1024;
131
132pub fn body_cap_from_env() -> usize {
135 match std::env::var("JSS_MAX_REQUEST_BODY") {
136 Ok(v) => parse_size(&v)
137 .map(|u| u as usize)
138 .unwrap_or(DEFAULT_BODY_CAP),
139 Err(_) => DEFAULT_BODY_CAP,
140 }
141}
142
143impl AppState {
144 pub fn new(storage: Arc<dyn Storage>) -> Self {
147 Self {
148 storage,
149 dotfiles: Arc::new(DotfileAllowlist::from_env()),
150 body_cap: body_cap_from_env(),
151 nodeinfo: NodeInfoMeta::default(),
152 mashlib: MashlibConfig::default(),
153 mashlib_cdn: None,
154 pay_config: solid_pod_rs::payments::PayConfig::default(),
155 }
156 }
157}
158
159fn to_actix(e: PodError) -> ActixError {
164 match e {
165 PodError::NotFound(_) => actix_web::error::ErrorNotFound(e.to_string()),
166 PodError::BadRequest(_) => actix_web::error::ErrorBadRequest(e.to_string()),
167 PodError::Unsupported(_) => actix_web::error::ErrorUnsupportedMediaType(e.to_string()),
168 PodError::Forbidden => actix_web::error::ErrorForbidden(e.to_string()),
169 PodError::Unauthenticated => actix_web::error::ErrorUnauthorized(e.to_string()),
170 PodError::PreconditionFailed(_) => actix_web::error::ErrorPreconditionFailed(e.to_string()),
171 _ => actix_web::error::ErrorInternalServerError(e.to_string()),
172 }
173}
174
175async fn extract_pubkey(req: &HttpRequest) -> Option<String> {
181 let header_val = req
182 .headers()
183 .get(header::AUTHORIZATION)
184 .and_then(|v| v.to_str().ok())?;
185 let url = format!(
186 "http://{}{}",
187 req.connection_info().host(),
188 req.uri().path()
189 );
190 nip98::verify(header_val, &url, req.method().as_str(), None)
191 .await
192 .ok()
193}
194
195fn agent_uri(pubkey: Option<&String>) -> Option<String> {
196 pubkey.map(|pk| format!("did:nostr:{pk}"))
197}
198
199fn accept_includes_html(accept: &str) -> bool {
207 accept.split(',').any(|entry| {
208 let mime = entry.split(';').next().unwrap_or("").trim();
209 mime.eq_ignore_ascii_case("text/html")
210 })
211}
212
213async fn enforce_write(
225 state: &AppState,
226 path: &str,
227 mode: AccessMode,
228 agent_uri: Option<&str>,
229) -> Result<(), ActixError> {
230 let acl_doc = match find_effective_acl_dyn(&*state.storage, path).await {
235 Ok(doc) => doc,
236 Err(e) => return Err(to_actix(e)),
237 };
238
239 let ctx = RequestContext {
240 web_id: agent_uri,
241 client_id: None,
242 issuer: None,
243 payment_balance_sats: None,
244 };
245 let registry = wac::conditions::ConditionRegistry::default_with_client_and_issuer();
246 let groups: wac::StaticGroupMembership = wac::StaticGroupMembership::default();
247 let granted = wac::evaluate_access_ctx_with_registry(
248 acl_doc.as_ref(),
249 &ctx,
250 path,
251 mode,
252 None,
253 &groups,
254 ®istry,
255 );
256 if granted {
257 return Ok(());
258 }
259
260 let allow_header = wac::wac_allow_header(acl_doc.as_ref(), agent_uri, path);
261 let (status, body) = if agent_uri.is_none() {
262 (StatusCode::UNAUTHORIZED, "authentication required")
263 } else {
264 (StatusCode::FORBIDDEN, "access forbidden")
265 };
266 let mut rsp = HttpResponse::new(status);
267 rsp.headers_mut().insert(
268 header::HeaderName::from_static("wac-allow"),
269 header::HeaderValue::from_str(&allow_header)
270 .unwrap_or(header::HeaderValue::from_static("")),
271 );
272 Err(actix_web::error::InternalError::from_response(body, rsp).into())
273}
274
275fn set_link_headers(rsp: &mut HttpResponse, path: &str) {
280 let links = ldp::link_headers(path).join(", ");
281 if let Ok(value) = header::HeaderValue::from_str(&links) {
282 rsp.headers_mut()
283 .insert(header::HeaderName::from_static("link"), value);
284 }
285}
286
287fn set_wac_allow(rsp: &mut HttpResponse, header_value: &str) {
288 if let Ok(v) = header::HeaderValue::from_str(header_value) {
289 rsp.headers_mut()
290 .insert(header::HeaderName::from_static("wac-allow"), v);
291 }
292}
293
294fn set_updates_via(rsp: &mut HttpResponse, base_url: &str) {
295 let ws_url = base_url
296 .replacen("https://", "wss://", 1)
297 .replacen("http://", "ws://", 1);
298 if let Ok(v) = header::HeaderValue::from_str(&ws_url) {
299 rsp.headers_mut()
300 .insert(header::HeaderName::from_static("updates-via"), v);
301 }
302}
303
304async fn handle_get(
305 req: HttpRequest,
306 state: web::Data<AppState>,
307) -> Result<HttpResponse, ActixError> {
308 let path = req.uri().path().to_string();
309
310 if path.contains('*') {
311 return handle_glob_get(req, state).await;
312 }
313
314 let auth_pk = extract_pubkey(&req).await;
315 let agent = agent_uri(auth_pk.as_ref());
316 let wac_allow = wac::wac_allow_header(None, agent.as_deref(), &path);
317
318 if ldp::is_container(&path) {
319 let accept = req
320 .headers()
321 .get(header::ACCEPT)
322 .and_then(|v| v.to_str().ok())
323 .unwrap_or("");
324
325 if accept_includes_html(accept) {
331 let index_path = format!("{}index.html", &path);
332 if let Ok((body, _meta)) = state.storage.get(&index_path).await {
333 let mut rsp = HttpResponse::Ok()
334 .content_type("text/html; charset=utf-8")
335 .body(body.to_vec());
336 set_wac_allow(&mut rsp, &wac_allow);
337 set_updates_via(&mut rsp, &state.nodeinfo.base_url);
338 set_link_headers(&mut rsp, &path);
339 return Ok(rsp);
340 }
341 }
342
343 let v = state
344 .storage
345 .container_representation(&path)
346 .await
347 .map_err(to_actix)?;
348
349 let sec_fetch_dest = req
351 .headers()
352 .get("sec-fetch-dest")
353 .and_then(|v| v.to_str().ok());
354 if mashlib::should_serve(
355 accept,
356 sec_fetch_dest,
357 "application/ld+json",
358 state.mashlib.enabled,
359 ) {
360 let json_ld = serde_json::to_string(&v).ok();
361 let html = mashlib::generate_html(&path, &state.mashlib, json_ld.as_deref());
362 let mut rsp = HttpResponse::Ok()
363 .content_type("text/html; charset=utf-8")
364 .insert_header(("X-Frame-Options", "DENY"))
365 .insert_header(("Content-Security-Policy", "frame-ancestors 'none'"))
366 .insert_header(("Cache-Control", "no-store"))
367 .body(html);
368 set_wac_allow(&mut rsp, &wac_allow);
369 set_updates_via(&mut rsp, &state.nodeinfo.base_url);
370 set_link_headers(&mut rsp, &path);
371 return Ok(rsp);
372 }
373
374 let mut rsp = HttpResponse::Ok().json(v);
375 rsp.headers_mut().insert(
376 header::CONTENT_TYPE,
377 header::HeaderValue::from_static("application/ld+json"),
378 );
379 set_wac_allow(&mut rsp, &wac_allow);
380 set_updates_via(&mut rsp, &state.nodeinfo.base_url);
381 set_link_headers(&mut rsp, &path);
382 return Ok(rsp);
383 }
384
385 match state.storage.get(&path).await {
386 Ok((body, meta)) => {
387 let accept = req
389 .headers()
390 .get(header::ACCEPT)
391 .and_then(|v| v.to_str().ok())
392 .unwrap_or("");
393 let sec_fetch_dest = req
394 .headers()
395 .get("sec-fetch-dest")
396 .and_then(|v| v.to_str().ok());
397 if mashlib::should_serve(
398 accept,
399 sec_fetch_dest,
400 &meta.content_type,
401 state.mashlib.enabled,
402 ) {
403 let embed = if body.len() <= state.mashlib.data_island_max_bytes {
404 std::str::from_utf8(&body).ok().map(|s| s.to_string())
405 } else {
406 None
407 };
408 let html = mashlib::generate_html(&path, &state.mashlib, embed.as_deref());
409 let mut rsp = HttpResponse::Ok()
410 .content_type("text/html; charset=utf-8")
411 .insert_header(("X-Frame-Options", "DENY"))
412 .insert_header(("Content-Security-Policy", "frame-ancestors 'none'"))
413 .insert_header(("Cache-Control", "no-store"))
414 .body(html);
415 set_wac_allow(&mut rsp, &wac_allow);
416 set_updates_via(&mut rsp, &state.nodeinfo.base_url);
417 set_link_headers(&mut rsp, &path);
418 return Ok(rsp);
419 }
420
421 let mut rsp = HttpResponse::Ok().body(body.to_vec());
422 rsp.headers_mut().insert(
423 header::CONTENT_TYPE,
424 header::HeaderValue::from_str(&meta.content_type).unwrap_or_else(|_| {
425 header::HeaderValue::from_static("application/octet-stream")
426 }),
427 );
428 if let Ok(etag) = header::HeaderValue::from_str(&format!("\"{}\"", meta.etag)) {
429 rsp.headers_mut().insert(header::ETAG, etag);
430 }
431 set_wac_allow(&mut rsp, &wac_allow);
432 set_updates_via(&mut rsp, &state.nodeinfo.base_url);
433 set_link_headers(&mut rsp, &path);
434 Ok(rsp)
435 }
436 Err(PodError::NotFound(_)) => Ok(HttpResponse::NotFound().finish()),
437 Err(e) => Err(to_actix(e)),
438 }
439}
440
441fn has_basic_container_link(req: &HttpRequest) -> bool {
442 req.headers()
443 .get_all(header::LINK)
444 .filter_map(|v| v.to_str().ok())
445 .any(|v| {
446 v.contains("http://www.w3.org/ns/ldp#BasicContainer") && v.contains("rel=\"type\"")
447 })
448}
449
450async fn handle_put(
451 req: HttpRequest,
452 body: web::Bytes,
453 state: web::Data<AppState>,
454) -> Result<HttpResponse, ActixError> {
455 let path = req.uri().path().to_string();
456
457 if ldp::is_container(&path) {
458 if has_basic_container_link(&req) {
459 let auth_pk = extract_pubkey(&req).await;
460 let agent = agent_uri(auth_pk.as_ref());
461 enforce_write(&state, &path, AccessMode::Write, agent.as_deref()).await?;
462 let meta = state
463 .storage
464 .create_container(&path)
465 .await
466 .map_err(to_actix)?;
467 let mut rsp = HttpResponse::Created().finish();
468 if let Ok(etag) = header::HeaderValue::from_str(&format!("\"{}\"", meta.etag)) {
469 rsp.headers_mut().insert(header::ETAG, etag);
470 }
471 set_link_headers(&mut rsp, &path);
472 return Ok(rsp);
473 }
474 return Ok(HttpResponse::MethodNotAllowed().body("cannot PUT to a container"));
475 }
476
477 let auth_pk = extract_pubkey(&req).await;
478 let agent = agent_uri(auth_pk.as_ref());
479 enforce_write(&state, &path, AccessMode::Write, agent.as_deref()).await?;
480
481 let ct = req
482 .headers()
483 .get(header::CONTENT_TYPE)
484 .and_then(|v| v.to_str().ok())
485 .unwrap_or("application/octet-stream");
486 let meta = state
487 .storage
488 .put(&path, Bytes::from(body.to_vec()), ct)
489 .await
490 .map_err(to_actix)?;
491 let mut rsp = HttpResponse::Created().finish();
492 if let Ok(etag) = header::HeaderValue::from_str(&format!("\"{}\"", meta.etag)) {
493 rsp.headers_mut().insert(header::ETAG, etag);
494 }
495 set_link_headers(&mut rsp, &path);
496 Ok(rsp)
497}
498
499async fn handle_post(
500 req: HttpRequest,
501 body: web::Bytes,
502 state: web::Data<AppState>,
503) -> Result<HttpResponse, ActixError> {
504 let path = req.uri().path().to_string();
505 let auth_pk = extract_pubkey(&req).await;
508 let agent = agent_uri(auth_pk.as_ref());
509 enforce_write(&state, &path, AccessMode::Append, agent.as_deref()).await?;
510
511 let slug = req
512 .headers()
513 .get(header::HeaderName::from_static("slug"))
514 .and_then(|v| v.to_str().ok());
515 let target = match ldp::resolve_slug(&path, slug) {
516 Ok(p) => p,
517 Err(e) => return Err(to_actix(e)),
518 };
519 let ct = req
520 .headers()
521 .get(header::CONTENT_TYPE)
522 .and_then(|v| v.to_str().ok())
523 .unwrap_or("application/octet-stream");
524 let meta = state
525 .storage
526 .put(&target, Bytes::from(body.to_vec()), ct)
527 .await
528 .map_err(to_actix)?;
529 let mut rsp = HttpResponse::Created().finish();
530 if let Ok(loc) = header::HeaderValue::from_str(&target) {
531 rsp.headers_mut().insert(header::LOCATION, loc);
532 }
533 if let Ok(etag) = header::HeaderValue::from_str(&format!("\"{}\"", meta.etag)) {
534 rsp.headers_mut().insert(header::ETAG, etag);
535 }
536 set_link_headers(&mut rsp, &target);
537 Ok(rsp)
538}
539
540async fn handle_patch(
541 req: HttpRequest,
542 body: web::Bytes,
543 state: web::Data<AppState>,
544) -> Result<HttpResponse, ActixError> {
545 let path = req.uri().path().to_string();
546 if ldp::is_container(&path) {
547 return Ok(HttpResponse::MethodNotAllowed().body("cannot PATCH a container"));
548 }
549 let auth_pk = extract_pubkey(&req).await;
550 let agent = agent_uri(auth_pk.as_ref());
551 enforce_write(&state, &path, AccessMode::Write, agent.as_deref()).await?;
557
558 let ct = req
559 .headers()
560 .get(header::CONTENT_TYPE)
561 .and_then(|v| v.to_str().ok())
562 .unwrap_or("");
563 let dialect = match ldp::patch_dialect_from_mime(ct) {
564 Some(d) => d,
565 None => {
566 return Ok(HttpResponse::UnsupportedMediaType()
567 .body(format!("unsupported patch dialect for content-type {ct:?}")))
568 }
569 };
570 let body_str = match std::str::from_utf8(&body) {
571 Ok(s) => s.to_string(),
572 Err(_) => return Ok(HttpResponse::BadRequest().body("patch body is not valid UTF-8")),
573 };
574
575 let existing = state.storage.get(&path).await;
577 match existing {
578 Ok((current_body, meta)) => {
579 let out = match dialect {
586 ldp::PatchDialect::N3 => {
587 ldp::apply_n3_patch(ldp::Graph::new(), &body_str).map_err(patch_parse_err)
588 }
589 ldp::PatchDialect::SparqlUpdate => {
590 ldp::apply_sparql_patch(ldp::Graph::new(), &body_str).map_err(patch_parse_err)
591 }
592 ldp::PatchDialect::JsonPatch => {
593 let mut json: serde_json::Value = match serde_json::from_slice(¤t_body) {
594 Ok(v) => v,
595 Err(_) => serde_json::json!({}),
596 };
597 let patch: serde_json::Value = match serde_json::from_str(&body_str) {
598 Ok(v) => v,
599 Err(e) => return Err(to_actix(PodError::BadRequest(e.to_string()))),
600 };
601 ldp::apply_json_patch(&mut json, &patch).map_err(to_actix)?;
602 let bytes = serde_json::to_vec(&json)
603 .map_err(PodError::from)
604 .map_err(to_actix)?;
605 let _ = state
606 .storage
607 .put(&path, Bytes::from(bytes), &meta.content_type)
608 .await
609 .map_err(to_actix)?;
610 return Ok(HttpResponse::NoContent().finish());
611 }
612 };
613 let outcome = out?;
614 let serialised = graph_to_turtle(&outcome.graph);
617 let _ = state
618 .storage
619 .put(&path, Bytes::from(serialised.into_bytes()), "text/turtle")
620 .await
621 .map_err(to_actix)?;
622 Ok(HttpResponse::NoContent().finish())
623 }
624 Err(PodError::NotFound(_)) => {
625 let create = ldp::apply_patch_to_absent(dialect, &body_str).map_err(patch_parse_err)?;
627 let PatchCreateOutcome::Created { graph, .. } = create else {
628 return Err(to_actix(PodError::Unsupported(
629 "unexpected patch outcome on absent resource".into(),
630 )));
631 };
632 let serialised = graph_to_turtle(&graph);
633 let _ = state
634 .storage
635 .put(&path, Bytes::from(serialised.into_bytes()), "text/turtle")
636 .await
637 .map_err(to_actix)?;
638 Ok(HttpResponse::Created().finish())
639 }
640 Err(e) => Err(to_actix(e)),
641 }
642}
643
644fn patch_parse_err(e: PodError) -> ActixError {
648 match e {
649 PodError::Unsupported(msg) | PodError::BadRequest(msg) => {
650 actix_web::error::ErrorBadRequest(msg)
651 }
652 other => to_actix(other),
653 }
654}
655
656fn graph_to_turtle(g: &ldp::Graph) -> String {
660 g.to_ntriples()
661}
662
663async fn find_effective_acl_dyn(
669 storage: &dyn Storage,
670 resource_path: &str,
671) -> Result<Option<wac::AclDocument>, PodError> {
672 let mut path = resource_path.to_string();
673 loop {
674 let acl_key = if path == "/" {
675 "/.acl".to_string()
676 } else {
677 format!("{}.acl", path.trim_end_matches('/'))
678 };
679 if let Ok((body, meta)) = storage.get(&acl_key).await {
680 match parse_jsonld_acl(&body) {
681 Ok(doc) => return Ok(Some(doc)),
682 Err(PodError::BadRequest(_)) => {
683 return Err(PodError::BadRequest("ACL document exceeds bounds".into()))
684 }
685 Err(_) => {}
686 }
687 let ct = meta.content_type.to_ascii_lowercase();
688 let looks_turtle = ct.starts_with("text/turtle")
689 || ct.starts_with("application/turtle")
690 || ct.starts_with("application/x-turtle");
691 let text = std::str::from_utf8(&body).unwrap_or("");
692 if looks_turtle || text.contains("@prefix") || text.contains("acl:Authorization") {
693 if let Ok(doc) = parse_turtle_acl(text) {
694 return Ok(Some(doc));
695 }
696 }
697 }
698 if path == "/" || path.is_empty() {
699 break;
700 }
701 let trimmed = path.trim_end_matches('/');
702 path = match trimmed.rfind('/') {
703 Some(0) => "/".to_string(),
704 Some(pos) => trimmed[..pos].to_string(),
705 None => "/".to_string(),
706 };
707 }
708 Ok(None)
709}
710
711async fn handle_delete(
712 req: HttpRequest,
713 state: web::Data<AppState>,
714) -> Result<HttpResponse, ActixError> {
715 let path = req.uri().path().to_string();
716 let auth_pk = extract_pubkey(&req).await;
717 let agent = agent_uri(auth_pk.as_ref());
718 enforce_write(&state, &path, AccessMode::Write, agent.as_deref()).await?;
719
720 match state.storage.delete(&path).await {
721 Ok(()) => Ok(HttpResponse::NoContent().finish()),
722 Err(PodError::NotFound(_)) => Ok(HttpResponse::NotFound().finish()),
723 Err(e) => Err(to_actix(e)),
724 }
725}
726
727async fn handle_options(req: HttpRequest) -> Result<HttpResponse, ActixError> {
728 let path = req.uri().path().to_string();
729 let o = ldp::options_for(&path);
730 let mut rsp = HttpResponse::NoContent().finish();
731 if let Ok(v) = header::HeaderValue::from_str(&o.allow.join(", ")) {
732 rsp.headers_mut()
733 .insert(header::HeaderName::from_static("allow"), v);
734 }
735 if let Some(ap) = o.accept_post {
736 if let Ok(v) = header::HeaderValue::from_str(ap) {
737 rsp.headers_mut()
738 .insert(header::HeaderName::from_static("accept-post"), v);
739 }
740 }
741 if let Ok(v) = header::HeaderValue::from_str(o.accept_patch) {
742 rsp.headers_mut()
743 .insert(header::HeaderName::from_static("accept-patch"), v);
744 }
745 if let Ok(v) = header::HeaderValue::from_str(o.accept_ranges) {
746 rsp.headers_mut()
747 .insert(header::HeaderName::from_static("accept-ranges"), v);
748 }
749 Ok(rsp)
750}
751
752async fn handle_well_known_solid(state: web::Data<AppState>) -> HttpResponse {
757 let doc = interop::well_known_solid(&state.nodeinfo.base_url, &state.nodeinfo.base_url);
758 HttpResponse::Ok()
759 .content_type("application/ld+json")
760 .json(doc)
761}
762
763#[derive(Debug, Deserialize)]
764struct WebFingerQuery {
765 resource: Option<String>,
766}
767
768async fn handle_well_known_webfinger(
769 state: web::Data<AppState>,
770 q: web::Query<WebFingerQuery>,
771) -> HttpResponse {
772 let resource = q.resource.clone().unwrap_or_else(|| {
773 format!(
774 "acct:anonymous@{}",
775 state
776 .nodeinfo
777 .base_url
778 .trim_start_matches("http://")
779 .trim_start_matches("https://")
780 )
781 });
782 let webid = format!(
783 "{}/profile/card#me",
784 state.nodeinfo.base_url.trim_end_matches('/')
785 );
786 match interop::webfinger_response(&resource, &state.nodeinfo.base_url, &webid) {
787 Some(jrd) => HttpResponse::Ok()
788 .content_type("application/jrd+json")
789 .json(jrd),
790 None => HttpResponse::NotFound().finish(),
791 }
792}
793
794async fn handle_well_known_nodeinfo(state: web::Data<AppState>) -> HttpResponse {
795 let doc = interop::nodeinfo_discovery(&state.nodeinfo.base_url);
796 HttpResponse::Ok()
797 .content_type("application/json")
798 .json(doc)
799}
800
801async fn handle_well_known_nodeinfo_2_1(state: web::Data<AppState>) -> HttpResponse {
802 let doc = interop::nodeinfo_2_1(
803 &state.nodeinfo.software_name,
804 &state.nodeinfo.software_version,
805 state.nodeinfo.open_registrations,
806 state.nodeinfo.total_users,
807 );
808 HttpResponse::Ok()
809 .content_type("application/json")
810 .json(doc)
811}
812
813#[cfg(feature = "did-nostr")]
814async fn handle_well_known_did_nostr(
815 state: web::Data<AppState>,
816 path: web::Path<String>,
817) -> HttpResponse {
818 let pubkey = path.into_inner();
819 let also = vec![format!(
820 "{}/profile/card#me",
821 state.nodeinfo.base_url.trim_end_matches('/')
822 )];
823 let doc = interop::did_nostr::did_nostr_document(&pubkey, &also);
824 HttpResponse::Ok()
825 .content_type("application/did+json")
826 .json(doc)
827}
828
829#[derive(Debug, Deserialize)]
834struct CreateAccountRequest {
835 username: String,
836 #[serde(default)]
837 name: Option<String>,
838}
839
840async fn handle_pod_check(state: web::Data<AppState>, path: web::Path<String>) -> HttpResponse {
841 let pod_name = path.into_inner();
842 let pod_root = format!("/{pod_name}/");
843 match state.storage.exists(&pod_root).await {
844 Ok(true) => HttpResponse::Ok().json(serde_json::json!({"exists": true})),
845 _ => HttpResponse::NotFound().json(serde_json::json!({"exists": false})),
846 }
847}
848
849async fn handle_create_account(
850 state: web::Data<AppState>,
851 body: web::Json<CreateAccountRequest>,
852) -> Result<HttpResponse, ActixError> {
853 let pod_root = format!("/{}/", body.username);
854 if state.storage.exists(&pod_root).await.unwrap_or(false) {
855 return Ok(
856 HttpResponse::Conflict().json(serde_json::json!({"error": "account already exists"}))
857 );
858 }
859
860 let plan = provision::ProvisionPlan {
861 pubkey: body.username.clone(),
862 display_name: body.name.clone(),
863 pod_base: format!(
864 "{}/{}",
865 state.nodeinfo.base_url.trim_end_matches('/'),
866 body.username,
867 ),
868 containers: vec![
869 format!("/{}/", body.username),
870 format!("/{}/profile/", body.username),
871 format!("/{}/inbox/", body.username),
872 format!("/{}/public/", body.username),
873 format!("/{}/private/", body.username),
874 format!("/{}/settings/", body.username),
875 ],
876 root_acl: None,
877 quota_bytes: None,
878 };
879
880 match provision::provision_pod(state.storage.as_ref(), &plan).await {
881 Ok(outcome) => Ok(HttpResponse::Created().json(serde_json::json!({
882 "webid": outcome.webid,
883 "pod_root": outcome.pod_root,
884 "username": body.username,
885 }))),
886 Err(e) => Err(to_actix(e)),
887 }
888}
889
890async fn handle_copy(
895 req: HttpRequest,
896 state: web::Data<AppState>,
897) -> Result<HttpResponse, ActixError> {
898 let dest = req.uri().path().to_string();
899 let auth_pk = extract_pubkey(&req).await;
900 let agent = agent_uri(auth_pk.as_ref());
901 enforce_write(&state, &dest, AccessMode::Write, agent.as_deref()).await?;
902
903 let source = req
904 .headers()
905 .get("source")
906 .and_then(|v| v.to_str().ok())
907 .map(|s| s.to_string());
908 let source = match source {
909 Some(s) => s,
910 None => return Ok(HttpResponse::BadRequest().body("Source header required")),
911 };
912
913 let (body, meta) = match state.storage.get(&source).await {
914 Ok(v) => v,
915 Err(PodError::NotFound(_)) => {
916 return Ok(HttpResponse::NotFound().body("source resource not found"))
917 }
918 Err(e) => return Err(to_actix(e)),
919 };
920
921 state
922 .storage
923 .put(&dest, body, &meta.content_type)
924 .await
925 .map_err(to_actix)?;
926
927 let src_acl = format!("{}.acl", source.trim_end_matches('/'));
929 let dst_acl = format!("{}.acl", dest.trim_end_matches('/'));
930 if let Ok((acl_body, acl_meta)) = state.storage.get(&src_acl).await {
931 let _ = state
932 .storage
933 .put(&dst_acl, acl_body, &acl_meta.content_type)
934 .await;
935 }
936
937 let mut rsp = HttpResponse::Created().finish();
938 if let Ok(loc) = header::HeaderValue::from_str(&dest) {
939 rsp.headers_mut().insert(header::LOCATION, loc);
940 }
941 Ok(rsp)
942}
943
944async fn handle_glob_get(
949 req: HttpRequest,
950 state: web::Data<AppState>,
951) -> Result<HttpResponse, ActixError> {
952 let raw_path = req.uri().path().to_string();
953 if !raw_path.ends_with("/*") {
955 return Ok(HttpResponse::NotFound().body("unsupported glob pattern"));
956 }
957 let folder = &raw_path[..raw_path.len() - 1]; let folder = if folder.ends_with('/') {
959 folder.to_string()
960 } else {
961 format!("{folder}/")
962 };
963
964 let children = state.storage.list(&folder).await.map_err(to_actix)?;
965 let mut merged = String::new();
966
967 for child in &children {
968 if child.ends_with('/') {
969 continue;
970 }
971 let child_path = format!("{folder}{child}");
972 if let Ok((body, meta)) = state.storage.get(&child_path).await {
973 if meta.content_type.contains("turtle")
974 || meta.content_type.contains("n-triples")
975 || meta.content_type.contains("n3")
976 {
977 if let Ok(text) = std::str::from_utf8(&body) {
978 merged.push_str(text);
979 merged.push('\n');
980 }
981 }
982 }
983 }
984
985 if merged.is_empty() {
986 return Ok(HttpResponse::NotFound().body("no matching RDF resources"));
987 }
988
989 Ok(HttpResponse::Ok().content_type("text/turtle").body(merged))
990}
991
992#[derive(Debug, Deserialize)]
997struct LoginPasswordRequest {
998 username: String,
999 password: String,
1000}
1001
1002async fn handle_login_password(body: web::Json<LoginPasswordRequest>) -> HttpResponse {
1003 let _ = (&body.username, &body.password);
1004 HttpResponse::Ok().json(serde_json::json!({
1005 "message": "login endpoint active"
1006 }))
1007}
1008
1009#[derive(Debug, Deserialize)]
1010struct PasswordResetRequest {
1011 username: String,
1012}
1013
1014async fn handle_password_reset_request(body: web::Json<PasswordResetRequest>) -> HttpResponse {
1015 let _ = &body.username;
1016 HttpResponse::Ok().json(serde_json::json!({
1017 "message": "if an account with that username exists, a reset link has been sent"
1018 }))
1019}
1020
1021#[derive(Debug, Deserialize)]
1022struct PasswordChangeRequest {
1023 token: String,
1024 new_password: String,
1025}
1026
1027async fn handle_password_change(body: web::Json<PasswordChangeRequest>) -> HttpResponse {
1028 let _ = (&body.token, &body.new_password);
1029 HttpResponse::Ok().json(serde_json::json!({
1030 "message": "password changed"
1031 }))
1032}
1033
1034async fn handle_pay_info(state: web::Data<AppState>) -> HttpResponse {
1039 let body = solid_pod_rs::payments::pay_info(&state.pay_config);
1040 HttpResponse::Ok()
1041 .content_type("application/json")
1042 .json(body)
1043}
1044
1045pub const DEFAULT_PROXY_BYTE_CAP: usize = 50 * 1024 * 1024;
1060
1061#[derive(Debug, Deserialize)]
1063struct ProxyQuery {
1064 url: String,
1065}
1066
1067const STRIPPED_RESPONSE_HEADERS: &[&str] = &[
1069 "set-cookie",
1070 "set-cookie2",
1071 "authorization",
1072 "www-authenticate",
1073 "proxy-authenticate",
1074 "proxy-authorization",
1075];
1076
1077fn validate_proxy_target(target: &str) -> Result<url::Url, HttpResponse> {
1083 let parsed = match url::Url::parse(target) {
1084 Ok(u) => u,
1085 Err(_) => {
1086 return Err(
1087 HttpResponse::BadRequest().json(serde_json::json!({"error": "invalid target URL"}))
1088 );
1089 }
1090 };
1091
1092 match parsed.scheme() {
1094 "http" | "https" => {}
1095 scheme => {
1096 return Err(HttpResponse::BadRequest()
1097 .json(serde_json::json!({"error": format!("unsupported scheme: {scheme}")})));
1098 }
1099 }
1100
1101 if let Err(_e) = solid_pod_rs::security::is_safe_url(target) {
1103 return Err(HttpResponse::Forbidden()
1104 .json(serde_json::json!({"error": "target URL blocked by SSRF policy"})));
1105 }
1106
1107 if let Some(host) = parsed.host_str() {
1109 let host_lower = host.to_ascii_lowercase();
1110 if host_lower == "localhost"
1112 || host_lower.ends_with(".localhost")
1113 || host_lower == "0.0.0.0"
1114 || host_lower == "[::1]"
1115 || host_lower == "[::0]"
1116 {
1117 return Err(HttpResponse::Forbidden()
1118 .json(serde_json::json!({"error": "target URL blocked by SSRF policy"})));
1119 }
1120 } else {
1121 return Err(
1122 HttpResponse::BadRequest().json(serde_json::json!({"error": "target URL has no host"}))
1123 );
1124 }
1125
1126 Ok(parsed)
1127}
1128
1129async fn handle_proxy(
1130 req: HttpRequest,
1131 _state: web::Data<AppState>,
1132 query: web::Query<ProxyQuery>,
1133) -> Result<HttpResponse, ActixError> {
1134 let auth_pk = extract_pubkey(&req).await;
1136 let agent = agent_uri(auth_pk.as_ref());
1137 if agent.is_none() {
1138 return Ok(HttpResponse::Unauthorized()
1139 .json(serde_json::json!({"error": "authentication required"})));
1140 }
1141
1142 let _target_url = match validate_proxy_target(&query.url) {
1144 Ok(u) => u,
1145 Err(rsp) => return Ok(rsp),
1146 };
1147
1148 let client = reqwest::Client::builder()
1150 .redirect(reqwest::redirect::Policy::none())
1153 .build()
1154 .map_err(|e| actix_web::error::ErrorInternalServerError(format!("proxy client: {e}")))?;
1155
1156 let mut current_url = query.url.clone();
1157 let mut redirect_count = 0u8;
1158 const MAX_REDIRECTS: u8 = 5;
1159
1160 let byte_cap = std::env::var("PROXY_BYTE_CAP")
1161 .ok()
1162 .and_then(|v| {
1163 solid_pod_rs::config::sources::parse_size(&v)
1164 .map(|u| u as usize)
1165 .ok()
1166 })
1167 .unwrap_or(DEFAULT_PROXY_BYTE_CAP);
1168
1169 loop {
1170 if redirect_count > 0 {
1172 match validate_proxy_target(¤t_url) {
1173 Ok(_) => {}
1174 Err(rsp) => return Ok(rsp),
1175 }
1176 }
1177
1178 let mut upstream_req = client.get(¤t_url);
1179
1180 if let Some(auth_val) = req
1182 .headers()
1183 .get("x-upstream-authorization")
1184 .and_then(|v| v.to_str().ok())
1185 {
1186 upstream_req = upstream_req.header("Authorization", auth_val);
1187 }
1188
1189 let response = upstream_req
1190 .send()
1191 .await
1192 .map_err(|e| actix_web::error::ErrorBadGateway(format!("upstream error: {e}")))?;
1193
1194 if response.status().is_redirection() {
1196 if redirect_count >= MAX_REDIRECTS {
1197 return Ok(HttpResponse::BadGateway()
1198 .json(serde_json::json!({"error": "too many redirects"})));
1199 }
1200 if let Some(location) = response.headers().get("location") {
1201 let loc_str = location
1202 .to_str()
1203 .map_err(|_| actix_web::error::ErrorBadGateway("invalid redirect location"))?;
1204 let base = url::Url::parse(¤t_url)
1206 .map_err(|_| actix_web::error::ErrorBadGateway("invalid current URL"))?;
1207 let resolved = base
1208 .join(loc_str)
1209 .map_err(|_| actix_web::error::ErrorBadGateway("invalid redirect URL"))?;
1210 current_url = resolved.to_string();
1211 redirect_count += 1;
1212 continue;
1213 }
1214 return Ok(HttpResponse::BadGateway()
1215 .json(serde_json::json!({"error": "redirect without location"})));
1216 }
1217
1218 let upstream_status = response.status().as_u16();
1220 let upstream_content_type = response
1221 .headers()
1222 .get("content-type")
1223 .and_then(|v| v.to_str().ok())
1224 .unwrap_or("application/octet-stream")
1225 .to_string();
1226
1227 let mut forwarded_headers: Vec<(String, String)> = Vec::new();
1229 for (name, value) in response.headers() {
1230 let name_lower = name.as_str().to_ascii_lowercase();
1231 if STRIPPED_RESPONSE_HEADERS.contains(&name_lower.as_str()) {
1232 continue;
1233 }
1234 if matches!(
1236 name_lower.as_str(),
1237 "transfer-encoding" | "connection" | "keep-alive" | "trailer" | "upgrade"
1238 ) {
1239 continue;
1240 }
1241 if let Ok(val_str) = value.to_str() {
1242 forwarded_headers.push((name_lower, val_str.to_string()));
1243 }
1244 }
1245
1246 let body_bytes = response
1247 .bytes()
1248 .await
1249 .map_err(|e| actix_web::error::ErrorBadGateway(format!("body read: {e}")))?;
1250
1251 if body_bytes.len() > byte_cap {
1252 return Ok(HttpResponse::PayloadTooLarge().json(serde_json::json!({
1253 "error": "proxied response exceeds byte cap",
1254 "limit": byte_cap
1255 })));
1256 }
1257
1258 let mut rsp = HttpResponse::build(
1260 StatusCode::from_u16(upstream_status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
1261 );
1262 rsp.insert_header(("Content-Type", upstream_content_type.as_str()));
1263 rsp.insert_header(("X-Proxy-Status", upstream_status.to_string()));
1264
1265 for (name, value) in &forwarded_headers {
1267 if let Ok(hname) = header::HeaderName::from_bytes(name.as_bytes()) {
1268 if let Ok(hval) = header::HeaderValue::from_str(value) {
1269 rsp.insert_header((hname, hval));
1270 }
1271 }
1272 }
1273
1274 return Ok(rsp.body(body_bytes.to_vec()));
1275 }
1276}
1277
1278pub struct PathTraversalGuard;
1284
1285impl<S, B> Transform<S, ServiceRequest> for PathTraversalGuard
1286where
1287 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1288 B: 'static,
1289{
1290 type Response = ServiceResponse<EitherBody<B, BoxBody>>;
1291 type Error = ActixError;
1292 type InitError = ();
1293 type Transform = PathTraversalGuardMiddleware<S>;
1294 type Future = Ready<Result<Self::Transform, Self::InitError>>;
1295
1296 fn new_transform(&self, service: S) -> Self::Future {
1297 ready(Ok(PathTraversalGuardMiddleware { service }))
1298 }
1299}
1300
1301pub struct PathTraversalGuardMiddleware<S> {
1303 service: S,
1304}
1305
1306impl<S, B> Service<ServiceRequest> for PathTraversalGuardMiddleware<S>
1307where
1308 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1309 B: 'static,
1310{
1311 type Response = ServiceResponse<EitherBody<B, BoxBody>>;
1312 type Error = ActixError;
1313 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
1314
1315 actix_web::dev::forward_ready!(service);
1316
1317 fn call(&self, req: ServiceRequest) -> Self::Future {
1318 let raw = req.path().to_string();
1321 if path_is_traversal(&raw) {
1322 let rsp = HttpResponse::BadRequest().body("invalid path: traversal rejected");
1323 let sr = req.into_response(rsp.map_into_boxed_body());
1324 return Box::pin(async move { Ok(sr.map_into_right_body()) });
1325 }
1326 let fut = self.service.call(req);
1327 Box::pin(async move {
1328 let resp = fut.await?;
1329 Ok(resp.map_into_left_body())
1330 })
1331 }
1332}
1333
1334fn path_is_traversal(path: &str) -> bool {
1335 let once: String = percent_decode_str(path).decode_utf8_lossy().into_owned();
1337 let twice: String = percent_decode_str(&once).decode_utf8_lossy().into_owned();
1338 for seg in once.split('/').chain(twice.split('/')) {
1339 if seg == ".." || seg == "." {
1340 return true;
1341 }
1342 }
1343 if twice.contains("/../") || twice.starts_with("../") || twice.ends_with("/..") {
1346 return true;
1347 }
1348 false
1349}
1350
1351pub struct ErrorLoggingMiddleware;
1367
1368impl<S, B> Transform<S, ServiceRequest> for ErrorLoggingMiddleware
1369where
1370 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1371 B: 'static,
1372{
1373 type Response = ServiceResponse<B>;
1374 type Error = ActixError;
1375 type InitError = ();
1376 type Transform = ErrorLoggingMiddlewareService<S>;
1377 type Future = Ready<Result<Self::Transform, Self::InitError>>;
1378
1379 fn new_transform(&self, service: S) -> Self::Future {
1380 ready(Ok(ErrorLoggingMiddlewareService { service }))
1381 }
1382}
1383
1384pub struct ErrorLoggingMiddlewareService<S> {
1386 service: S,
1387}
1388
1389impl<S, B> Service<ServiceRequest> for ErrorLoggingMiddlewareService<S>
1390where
1391 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1392 B: 'static,
1393{
1394 type Response = ServiceResponse<B>;
1395 type Error = ActixError;
1396 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
1397
1398 actix_web::dev::forward_ready!(service);
1399
1400 fn call(&self, req: ServiceRequest) -> Self::Future {
1401 let method = req.method().as_str().to_string();
1404 let path = req.path().to_string();
1405
1406 let fut = self.service.call(req);
1407 Box::pin(async move {
1408 let response = fut.await?;
1409 let status = response.status();
1410 if status.is_server_error() {
1411 log_5xx(&method, &path, status, response.response().error());
1412 }
1413 Ok(response)
1414 })
1415 }
1416}
1417
1418fn log_5xx(method: &str, path: &str, status: StatusCode, error: Option<&actix_web::Error>) {
1422 let chain = match error {
1426 Some(e) => format_error_chain(e),
1427 None => "<no error attached to response>".to_string(),
1428 };
1429
1430 let backtrace = if std::env::var("RUST_BACKTRACE").ok().as_deref() == Some("1") {
1431 Some(std::backtrace::Backtrace::force_capture().to_string())
1432 } else {
1433 None
1434 };
1435
1436 tracing::error!(
1437 target: "solid_pod_rs_server::http",
1438 method = %method,
1439 path = %path,
1440 status = %status.as_u16(),
1441 error.chain = %chain,
1442 backtrace = backtrace.as_deref().unwrap_or(""),
1443 "5xx response"
1444 );
1445}
1446
1447fn format_error_chain(e: &actix_web::Error) -> String {
1458 let summary = format!("{}", e.as_response_error());
1459 let debug = format!("{e:?}");
1460 if debug == summary || debug.is_empty() {
1461 summary
1462 } else {
1463 format!("{summary} -> {debug}")
1464 }
1465}
1466
1467pub struct DotfileGuard {
1473 allow: Arc<DotfileAllowlist>,
1474}
1475
1476impl DotfileGuard {
1477 pub fn new(allow: Arc<DotfileAllowlist>) -> Self {
1478 Self { allow }
1479 }
1480}
1481
1482impl<S, B> Transform<S, ServiceRequest> for DotfileGuard
1483where
1484 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1485 B: 'static,
1486{
1487 type Response = ServiceResponse<EitherBody<B, BoxBody>>;
1488 type Error = ActixError;
1489 type InitError = ();
1490 type Transform = DotfileGuardMiddleware<S>;
1491 type Future = Ready<Result<Self::Transform, Self::InitError>>;
1492
1493 fn new_transform(&self, service: S) -> Self::Future {
1494 ready(Ok(DotfileGuardMiddleware {
1495 service,
1496 allow: self.allow.clone(),
1497 }))
1498 }
1499}
1500
1501pub struct DotfileGuardMiddleware<S> {
1503 service: S,
1504 allow: Arc<DotfileAllowlist>,
1505}
1506
1507impl<S, B> Service<ServiceRequest> for DotfileGuardMiddleware<S>
1508where
1509 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1510 B: 'static,
1511{
1512 type Response = ServiceResponse<EitherBody<B, BoxBody>>;
1513 type Error = ActixError;
1514 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
1515
1516 actix_web::dev::forward_ready!(service);
1517
1518 fn call(&self, req: ServiceRequest) -> Self::Future {
1519 let path = req.path().to_string();
1520 let allow_wellknown = path.starts_with("/.well-known/");
1524 if !allow_wellknown {
1525 let pb = PathBuf::from(&path);
1526 if !self.allow.is_allowed(Path::new(&pb)) {
1527 let rsp = HttpResponse::Forbidden().body("dotfile path denied by allowlist");
1528 let sr = req.into_response(rsp.map_into_boxed_body());
1529 return Box::pin(async move { Ok(sr.map_into_right_body()) });
1530 }
1531 }
1532 let fut = self.service.call(req);
1533 Box::pin(async move {
1534 let resp = fut.await?;
1535 Ok(resp.map_into_left_body())
1536 })
1537 }
1538}
1539
1540pub fn build_app(
1552 state: AppState,
1553) -> App<
1554 impl actix_web::dev::ServiceFactory<
1555 ServiceRequest,
1556 Config = (),
1557 Response = ServiceResponse<EitherBody<EitherBody<BoxBody>>>,
1558 Error = ActixError,
1559 InitError = (),
1560 >,
1561> {
1562 let body_cap = state.body_cap;
1563 let dotfiles = state.dotfiles.clone();
1564
1565 let mut app = App::new()
1566 .app_data(web::Data::new(state.clone()))
1567 .app_data(web::PayloadConfig::new(body_cap))
1568 .wrap(ErrorLoggingMiddleware)
1573 .wrap(NormalizePath::new(TrailingSlash::MergeOnly))
1577 .wrap(PathTraversalGuard)
1578 .wrap(DotfileGuard::new(dotfiles));
1579
1580 app = app
1586 .route("/.well-known/solid", web::get().to(handle_well_known_solid))
1587 .route(
1588 "/.well-known/webfinger",
1589 web::get().to(handle_well_known_webfinger),
1590 )
1591 .route(
1592 "/.well-known/nodeinfo",
1593 web::get().to(handle_well_known_nodeinfo),
1594 )
1595 .route(
1596 "/.well-known/nodeinfo/2.1",
1597 web::get().to(handle_well_known_nodeinfo_2_1),
1598 );
1599
1600 #[cfg(feature = "did-nostr")]
1601 {
1602 app = app.route(
1603 "/.well-known/did/nostr/{pubkey}.json",
1604 web::get().to(handle_well_known_did_nostr),
1605 );
1606 }
1607
1608 app = app.route("/pay/.info", web::get().to(handle_pay_info));
1610
1611 app = app.route("/proxy", web::get().to(handle_proxy));
1613
1614 app = app
1616 .route("/api/accounts/new", web::post().to(handle_create_account))
1617 .route("/pods/check/{name}", web::get().to(handle_pod_check))
1618 .route("/login/password", web::post().to(handle_login_password))
1619 .route(
1620 "/account/password/reset",
1621 web::post().to(handle_password_reset_request),
1622 )
1623 .route(
1624 "/account/password/change",
1625 web::post().to(handle_password_change),
1626 );
1627
1628 app.route("/{tail:.*}/", web::post().to(handle_post))
1631 .route("/{tail:.*}/", web::put().to(handle_put))
1632 .route("/{tail:.*}", web::get().to(handle_get))
1633 .route("/{tail:.*}", web::head().to(handle_get))
1634 .route("/{tail:.*}", web::put().to(handle_put))
1635 .route("/{tail:.*}", web::patch().to(handle_patch))
1636 .route("/{tail:.*}", web::delete().to(handle_delete))
1637 .route(
1638 "/{tail:.*}",
1639 web::method(actix_web::http::Method::from_bytes(b"COPY").unwrap()).to(handle_copy),
1640 )
1641 .route(
1642 "/{tail:.*}",
1643 web::method(actix_web::http::Method::OPTIONS).to(handle_options),
1644 )
1645}