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#[cfg(feature = "nip05-endpoint")]
837#[derive(Debug, Deserialize)]
838struct Nip05Query {
839 name: Option<String>,
842}
843
844#[cfg(feature = "nip05-endpoint")]
845fn nip05_name_is_valid(name: &str) -> bool {
846 if name.is_empty() {
849 return false;
850 }
851 name.bytes()
852 .all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'_' || b == b'-')
853}
854
855#[cfg(feature = "nip05-endpoint")]
856async fn handle_well_known_nip05(
857 state: web::Data<AppState>,
858 query: web::Query<Nip05Query>,
859) -> HttpResponse {
860 use solid_pod_rs::webid::extract_nostr_pubkey;
861
862 let name = query.name.clone().unwrap_or_else(|| "_".to_string());
864 if !nip05_name_is_valid(&name) {
865 return HttpResponse::BadRequest().json(serde_json::json!({
866 "error": "invalid NIP-05 local part",
867 }));
868 }
869
870 let profile_path = if name == "_" {
876 "/profile/card".to_string()
877 } else {
878 format!("/{name}/profile/card")
879 };
880
881 let (body, _meta) = match state.storage.get(&profile_path).await {
882 Ok(v) => v,
883 Err(_) => {
884 return nip05_empty_response();
888 }
889 };
890
891 let pubkey_hex = match extract_nostr_pubkey(&body) {
892 Ok(Some(p)) => p,
893 _ => return nip05_empty_response(),
894 };
895
896 let doc = interop::nip05_document([(name, pubkey_hex)]);
897 HttpResponse::Ok()
898 .insert_header(("Access-Control-Allow-Origin", "*"))
899 .content_type("application/json")
900 .json(doc)
901}
902
903#[cfg(feature = "nip05-endpoint")]
904fn nip05_empty_response() -> HttpResponse {
905 HttpResponse::Ok()
906 .insert_header(("Access-Control-Allow-Origin", "*"))
907 .content_type("application/json")
908 .json(serde_json::json!({ "names": {} }))
909}
910
911#[derive(Debug, Deserialize)]
916struct CreateAccountRequest {
917 username: String,
918 #[serde(default)]
919 name: Option<String>,
920}
921
922async fn handle_pod_check(state: web::Data<AppState>, path: web::Path<String>) -> HttpResponse {
923 let pod_name = path.into_inner();
924 let pod_root = format!("/{pod_name}/");
925 match state.storage.exists(&pod_root).await {
926 Ok(true) => HttpResponse::Ok().json(serde_json::json!({"exists": true})),
927 _ => HttpResponse::NotFound().json(serde_json::json!({"exists": false})),
928 }
929}
930
931async fn handle_create_account(
932 state: web::Data<AppState>,
933 body: web::Json<CreateAccountRequest>,
934) -> Result<HttpResponse, ActixError> {
935 let pod_root = format!("/{}/", body.username);
936 if state.storage.exists(&pod_root).await.unwrap_or(false) {
937 return Ok(
938 HttpResponse::Conflict().json(serde_json::json!({"error": "account already exists"}))
939 );
940 }
941
942 let plan = provision::ProvisionPlan {
943 pubkey: body.username.clone(),
944 display_name: body.name.clone(),
945 pod_base: format!(
946 "{}/{}",
947 state.nodeinfo.base_url.trim_end_matches('/'),
948 body.username,
949 ),
950 containers: vec![
951 format!("/{}/", body.username),
952 format!("/{}/profile/", body.username),
953 format!("/{}/inbox/", body.username),
954 format!("/{}/public/", body.username),
955 format!("/{}/private/", body.username),
956 format!("/{}/settings/", body.username),
957 ],
958 root_acl: None,
959 quota_bytes: None,
960 #[cfg(feature = "provision-keys")]
961 provision_keys: false,
962 };
963
964 match provision::provision_pod(state.storage.as_ref(), &plan).await {
965 Ok(outcome) => Ok(HttpResponse::Created().json(serde_json::json!({
966 "webid": outcome.webid,
967 "pod_root": outcome.pod_root,
968 "username": body.username,
969 }))),
970 Err(e) => Err(to_actix(e)),
971 }
972}
973
974async fn handle_copy(
979 req: HttpRequest,
980 state: web::Data<AppState>,
981) -> Result<HttpResponse, ActixError> {
982 let dest = req.uri().path().to_string();
983 let auth_pk = extract_pubkey(&req).await;
984 let agent = agent_uri(auth_pk.as_ref());
985 enforce_write(&state, &dest, AccessMode::Write, agent.as_deref()).await?;
986
987 let source = req
988 .headers()
989 .get("source")
990 .and_then(|v| v.to_str().ok())
991 .map(|s| s.to_string());
992 let source = match source {
993 Some(s) => s,
994 None => return Ok(HttpResponse::BadRequest().body("Source header required")),
995 };
996
997 let (body, meta) = match state.storage.get(&source).await {
998 Ok(v) => v,
999 Err(PodError::NotFound(_)) => {
1000 return Ok(HttpResponse::NotFound().body("source resource not found"))
1001 }
1002 Err(e) => return Err(to_actix(e)),
1003 };
1004
1005 state
1006 .storage
1007 .put(&dest, body, &meta.content_type)
1008 .await
1009 .map_err(to_actix)?;
1010
1011 let src_acl = format!("{}.acl", source.trim_end_matches('/'));
1013 let dst_acl = format!("{}.acl", dest.trim_end_matches('/'));
1014 if let Ok((acl_body, acl_meta)) = state.storage.get(&src_acl).await {
1015 let _ = state
1016 .storage
1017 .put(&dst_acl, acl_body, &acl_meta.content_type)
1018 .await;
1019 }
1020
1021 let mut rsp = HttpResponse::Created().finish();
1022 if let Ok(loc) = header::HeaderValue::from_str(&dest) {
1023 rsp.headers_mut().insert(header::LOCATION, loc);
1024 }
1025 Ok(rsp)
1026}
1027
1028async fn handle_glob_get(
1033 req: HttpRequest,
1034 state: web::Data<AppState>,
1035) -> Result<HttpResponse, ActixError> {
1036 let raw_path = req.uri().path().to_string();
1037 if !raw_path.ends_with("/*") {
1039 return Ok(HttpResponse::NotFound().body("unsupported glob pattern"));
1040 }
1041 let folder = &raw_path[..raw_path.len() - 1]; let folder = if folder.ends_with('/') {
1043 folder.to_string()
1044 } else {
1045 format!("{folder}/")
1046 };
1047
1048 let children = state.storage.list(&folder).await.map_err(to_actix)?;
1049 let mut merged = String::new();
1050
1051 for child in &children {
1052 if child.ends_with('/') {
1053 continue;
1054 }
1055 let child_path = format!("{folder}{child}");
1056 if let Ok((body, meta)) = state.storage.get(&child_path).await {
1057 if meta.content_type.contains("turtle")
1058 || meta.content_type.contains("n-triples")
1059 || meta.content_type.contains("n3")
1060 {
1061 if let Ok(text) = std::str::from_utf8(&body) {
1062 merged.push_str(text);
1063 merged.push('\n');
1064 }
1065 }
1066 }
1067 }
1068
1069 if merged.is_empty() {
1070 return Ok(HttpResponse::NotFound().body("no matching RDF resources"));
1071 }
1072
1073 Ok(HttpResponse::Ok().content_type("text/turtle").body(merged))
1074}
1075
1076#[derive(Debug, Deserialize)]
1081struct LoginPasswordRequest {
1082 username: String,
1083 password: String,
1084}
1085
1086async fn handle_login_password(body: web::Json<LoginPasswordRequest>) -> HttpResponse {
1087 let _ = (&body.username, &body.password);
1088 HttpResponse::Ok().json(serde_json::json!({
1089 "message": "login endpoint active"
1090 }))
1091}
1092
1093#[derive(Debug, Deserialize)]
1094struct PasswordResetRequest {
1095 username: String,
1096}
1097
1098async fn handle_password_reset_request(body: web::Json<PasswordResetRequest>) -> HttpResponse {
1099 let _ = &body.username;
1100 HttpResponse::Ok().json(serde_json::json!({
1101 "message": "if an account with that username exists, a reset link has been sent"
1102 }))
1103}
1104
1105#[derive(Debug, Deserialize)]
1106struct PasswordChangeRequest {
1107 token: String,
1108 new_password: String,
1109}
1110
1111async fn handle_password_change(body: web::Json<PasswordChangeRequest>) -> HttpResponse {
1112 let _ = (&body.token, &body.new_password);
1113 HttpResponse::Ok().json(serde_json::json!({
1114 "message": "password changed"
1115 }))
1116}
1117
1118async fn handle_pay_info(state: web::Data<AppState>) -> HttpResponse {
1123 let body = solid_pod_rs::payments::pay_info(&state.pay_config);
1124 HttpResponse::Ok()
1125 .content_type("application/json")
1126 .json(body)
1127}
1128
1129pub const DEFAULT_PROXY_BYTE_CAP: usize = 50 * 1024 * 1024;
1144
1145#[derive(Debug, Deserialize)]
1147struct ProxyQuery {
1148 url: String,
1149}
1150
1151const STRIPPED_RESPONSE_HEADERS: &[&str] = &[
1153 "set-cookie",
1154 "set-cookie2",
1155 "authorization",
1156 "www-authenticate",
1157 "proxy-authenticate",
1158 "proxy-authorization",
1159];
1160
1161fn validate_proxy_target(target: &str) -> Result<url::Url, HttpResponse> {
1167 let parsed = match url::Url::parse(target) {
1168 Ok(u) => u,
1169 Err(_) => {
1170 return Err(
1171 HttpResponse::BadRequest().json(serde_json::json!({"error": "invalid target URL"}))
1172 );
1173 }
1174 };
1175
1176 match parsed.scheme() {
1178 "http" | "https" => {}
1179 scheme => {
1180 return Err(HttpResponse::BadRequest()
1181 .json(serde_json::json!({"error": format!("unsupported scheme: {scheme}")})));
1182 }
1183 }
1184
1185 if let Err(_e) = solid_pod_rs::security::is_safe_url(target) {
1187 return Err(HttpResponse::Forbidden()
1188 .json(serde_json::json!({"error": "target URL blocked by SSRF policy"})));
1189 }
1190
1191 if let Some(host) = parsed.host_str() {
1193 let host_lower = host.to_ascii_lowercase();
1194 if host_lower == "localhost"
1196 || host_lower.ends_with(".localhost")
1197 || host_lower == "0.0.0.0"
1198 || host_lower == "[::1]"
1199 || host_lower == "[::0]"
1200 {
1201 return Err(HttpResponse::Forbidden()
1202 .json(serde_json::json!({"error": "target URL blocked by SSRF policy"})));
1203 }
1204 } else {
1205 return Err(
1206 HttpResponse::BadRequest().json(serde_json::json!({"error": "target URL has no host"}))
1207 );
1208 }
1209
1210 Ok(parsed)
1211}
1212
1213async fn handle_proxy(
1214 req: HttpRequest,
1215 _state: web::Data<AppState>,
1216 query: web::Query<ProxyQuery>,
1217) -> Result<HttpResponse, ActixError> {
1218 let auth_pk = extract_pubkey(&req).await;
1220 let agent = agent_uri(auth_pk.as_ref());
1221 if agent.is_none() {
1222 return Ok(HttpResponse::Unauthorized()
1223 .json(serde_json::json!({"error": "authentication required"})));
1224 }
1225
1226 let _target_url = match validate_proxy_target(&query.url) {
1228 Ok(u) => u,
1229 Err(rsp) => return Ok(rsp),
1230 };
1231
1232 let client = reqwest::Client::builder()
1234 .redirect(reqwest::redirect::Policy::none())
1237 .build()
1238 .map_err(|e| actix_web::error::ErrorInternalServerError(format!("proxy client: {e}")))?;
1239
1240 let mut current_url = query.url.clone();
1241 let mut redirect_count = 0u8;
1242 const MAX_REDIRECTS: u8 = 5;
1243
1244 let byte_cap = std::env::var("PROXY_BYTE_CAP")
1245 .ok()
1246 .and_then(|v| {
1247 solid_pod_rs::config::sources::parse_size(&v)
1248 .map(|u| u as usize)
1249 .ok()
1250 })
1251 .unwrap_or(DEFAULT_PROXY_BYTE_CAP);
1252
1253 loop {
1254 if redirect_count > 0 {
1256 match validate_proxy_target(¤t_url) {
1257 Ok(_) => {}
1258 Err(rsp) => return Ok(rsp),
1259 }
1260 }
1261
1262 let mut upstream_req = client.get(¤t_url);
1263
1264 if let Some(auth_val) = req
1266 .headers()
1267 .get("x-upstream-authorization")
1268 .and_then(|v| v.to_str().ok())
1269 {
1270 upstream_req = upstream_req.header("Authorization", auth_val);
1271 }
1272
1273 let response = upstream_req
1274 .send()
1275 .await
1276 .map_err(|e| actix_web::error::ErrorBadGateway(format!("upstream error: {e}")))?;
1277
1278 if response.status().is_redirection() {
1280 if redirect_count >= MAX_REDIRECTS {
1281 return Ok(HttpResponse::BadGateway()
1282 .json(serde_json::json!({"error": "too many redirects"})));
1283 }
1284 if let Some(location) = response.headers().get("location") {
1285 let loc_str = location
1286 .to_str()
1287 .map_err(|_| actix_web::error::ErrorBadGateway("invalid redirect location"))?;
1288 let base = url::Url::parse(¤t_url)
1290 .map_err(|_| actix_web::error::ErrorBadGateway("invalid current URL"))?;
1291 let resolved = base
1292 .join(loc_str)
1293 .map_err(|_| actix_web::error::ErrorBadGateway("invalid redirect URL"))?;
1294 current_url = resolved.to_string();
1295 redirect_count += 1;
1296 continue;
1297 }
1298 return Ok(HttpResponse::BadGateway()
1299 .json(serde_json::json!({"error": "redirect without location"})));
1300 }
1301
1302 let upstream_status = response.status().as_u16();
1304 let upstream_content_type = response
1305 .headers()
1306 .get("content-type")
1307 .and_then(|v| v.to_str().ok())
1308 .unwrap_or("application/octet-stream")
1309 .to_string();
1310
1311 let mut forwarded_headers: Vec<(String, String)> = Vec::new();
1313 for (name, value) in response.headers() {
1314 let name_lower = name.as_str().to_ascii_lowercase();
1315 if STRIPPED_RESPONSE_HEADERS.contains(&name_lower.as_str()) {
1316 continue;
1317 }
1318 if matches!(
1320 name_lower.as_str(),
1321 "transfer-encoding" | "connection" | "keep-alive" | "trailer" | "upgrade"
1322 ) {
1323 continue;
1324 }
1325 if let Ok(val_str) = value.to_str() {
1326 forwarded_headers.push((name_lower, val_str.to_string()));
1327 }
1328 }
1329
1330 let body_bytes = response
1331 .bytes()
1332 .await
1333 .map_err(|e| actix_web::error::ErrorBadGateway(format!("body read: {e}")))?;
1334
1335 if body_bytes.len() > byte_cap {
1336 return Ok(HttpResponse::PayloadTooLarge().json(serde_json::json!({
1337 "error": "proxied response exceeds byte cap",
1338 "limit": byte_cap
1339 })));
1340 }
1341
1342 let mut rsp = HttpResponse::build(
1344 StatusCode::from_u16(upstream_status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
1345 );
1346 rsp.insert_header(("Content-Type", upstream_content_type.as_str()));
1347 rsp.insert_header(("X-Proxy-Status", upstream_status.to_string()));
1348
1349 for (name, value) in &forwarded_headers {
1351 if let Ok(hname) = header::HeaderName::from_bytes(name.as_bytes()) {
1352 if let Ok(hval) = header::HeaderValue::from_str(value) {
1353 rsp.insert_header((hname, hval));
1354 }
1355 }
1356 }
1357
1358 return Ok(rsp.body(body_bytes.to_vec()));
1359 }
1360}
1361
1362pub struct PathTraversalGuard;
1368
1369impl<S, B> Transform<S, ServiceRequest> for PathTraversalGuard
1370where
1371 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1372 B: 'static,
1373{
1374 type Response = ServiceResponse<EitherBody<B, BoxBody>>;
1375 type Error = ActixError;
1376 type InitError = ();
1377 type Transform = PathTraversalGuardMiddleware<S>;
1378 type Future = Ready<Result<Self::Transform, Self::InitError>>;
1379
1380 fn new_transform(&self, service: S) -> Self::Future {
1381 ready(Ok(PathTraversalGuardMiddleware { service }))
1382 }
1383}
1384
1385pub struct PathTraversalGuardMiddleware<S> {
1387 service: S,
1388}
1389
1390impl<S, B> Service<ServiceRequest> for PathTraversalGuardMiddleware<S>
1391where
1392 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1393 B: 'static,
1394{
1395 type Response = ServiceResponse<EitherBody<B, BoxBody>>;
1396 type Error = ActixError;
1397 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
1398
1399 actix_web::dev::forward_ready!(service);
1400
1401 fn call(&self, req: ServiceRequest) -> Self::Future {
1402 let raw = req.path().to_string();
1405 if path_is_traversal(&raw) {
1406 let rsp = HttpResponse::BadRequest().body("invalid path: traversal rejected");
1407 let sr = req.into_response(rsp.map_into_boxed_body());
1408 return Box::pin(async move { Ok(sr.map_into_right_body()) });
1409 }
1410 let fut = self.service.call(req);
1411 Box::pin(async move {
1412 let resp = fut.await?;
1413 Ok(resp.map_into_left_body())
1414 })
1415 }
1416}
1417
1418fn path_is_traversal(path: &str) -> bool {
1419 let once: String = percent_decode_str(path).decode_utf8_lossy().into_owned();
1421 let twice: String = percent_decode_str(&once).decode_utf8_lossy().into_owned();
1422 for seg in once.split('/').chain(twice.split('/')) {
1423 if seg == ".." || seg == "." {
1424 return true;
1425 }
1426 }
1427 if twice.contains("/../") || twice.starts_with("../") || twice.ends_with("/..") {
1430 return true;
1431 }
1432 false
1433}
1434
1435pub struct ErrorLoggingMiddleware;
1451
1452impl<S, B> Transform<S, ServiceRequest> for ErrorLoggingMiddleware
1453where
1454 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1455 B: 'static,
1456{
1457 type Response = ServiceResponse<B>;
1458 type Error = ActixError;
1459 type InitError = ();
1460 type Transform = ErrorLoggingMiddlewareService<S>;
1461 type Future = Ready<Result<Self::Transform, Self::InitError>>;
1462
1463 fn new_transform(&self, service: S) -> Self::Future {
1464 ready(Ok(ErrorLoggingMiddlewareService { service }))
1465 }
1466}
1467
1468pub struct ErrorLoggingMiddlewareService<S> {
1470 service: S,
1471}
1472
1473impl<S, B> Service<ServiceRequest> for ErrorLoggingMiddlewareService<S>
1474where
1475 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1476 B: 'static,
1477{
1478 type Response = ServiceResponse<B>;
1479 type Error = ActixError;
1480 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
1481
1482 actix_web::dev::forward_ready!(service);
1483
1484 fn call(&self, req: ServiceRequest) -> Self::Future {
1485 let method = req.method().as_str().to_string();
1488 let path = req.path().to_string();
1489
1490 let fut = self.service.call(req);
1491 Box::pin(async move {
1492 let response = fut.await?;
1493 let status = response.status();
1494 if status.is_server_error() {
1495 log_5xx(&method, &path, status, response.response().error());
1496 }
1497 Ok(response)
1498 })
1499 }
1500}
1501
1502fn log_5xx(method: &str, path: &str, status: StatusCode, error: Option<&actix_web::Error>) {
1506 let chain = match error {
1510 Some(e) => format_error_chain(e),
1511 None => "<no error attached to response>".to_string(),
1512 };
1513
1514 let backtrace = if std::env::var("RUST_BACKTRACE").ok().as_deref() == Some("1") {
1515 Some(std::backtrace::Backtrace::force_capture().to_string())
1516 } else {
1517 None
1518 };
1519
1520 tracing::error!(
1521 target: "solid_pod_rs_server::http",
1522 method = %method,
1523 path = %path,
1524 status = %status.as_u16(),
1525 error.chain = %chain,
1526 backtrace = backtrace.as_deref().unwrap_or(""),
1527 "5xx response"
1528 );
1529}
1530
1531fn format_error_chain(e: &actix_web::Error) -> String {
1542 let summary = format!("{}", e.as_response_error());
1543 let debug = format!("{e:?}");
1544 if debug == summary || debug.is_empty() {
1545 summary
1546 } else {
1547 format!("{summary} -> {debug}")
1548 }
1549}
1550
1551pub struct DotfileGuard {
1557 allow: Arc<DotfileAllowlist>,
1558}
1559
1560impl DotfileGuard {
1561 pub fn new(allow: Arc<DotfileAllowlist>) -> Self {
1562 Self { allow }
1563 }
1564}
1565
1566impl<S, B> Transform<S, ServiceRequest> for DotfileGuard
1567where
1568 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1569 B: 'static,
1570{
1571 type Response = ServiceResponse<EitherBody<B, BoxBody>>;
1572 type Error = ActixError;
1573 type InitError = ();
1574 type Transform = DotfileGuardMiddleware<S>;
1575 type Future = Ready<Result<Self::Transform, Self::InitError>>;
1576
1577 fn new_transform(&self, service: S) -> Self::Future {
1578 ready(Ok(DotfileGuardMiddleware {
1579 service,
1580 allow: self.allow.clone(),
1581 }))
1582 }
1583}
1584
1585pub struct DotfileGuardMiddleware<S> {
1587 service: S,
1588 allow: Arc<DotfileAllowlist>,
1589}
1590
1591impl<S, B> Service<ServiceRequest> for DotfileGuardMiddleware<S>
1592where
1593 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1594 B: 'static,
1595{
1596 type Response = ServiceResponse<EitherBody<B, BoxBody>>;
1597 type Error = ActixError;
1598 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
1599
1600 actix_web::dev::forward_ready!(service);
1601
1602 fn call(&self, req: ServiceRequest) -> Self::Future {
1603 let path = req.path().to_string();
1604 let allow_wellknown = path.starts_with("/.well-known/");
1608 if !allow_wellknown {
1609 let pb = PathBuf::from(&path);
1610 if !self.allow.is_allowed(Path::new(&pb)) {
1611 let rsp = HttpResponse::Forbidden().body("dotfile path denied by allowlist");
1612 let sr = req.into_response(rsp.map_into_boxed_body());
1613 return Box::pin(async move { Ok(sr.map_into_right_body()) });
1614 }
1615 }
1616 let fut = self.service.call(req);
1617 Box::pin(async move {
1618 let resp = fut.await?;
1619 Ok(resp.map_into_left_body())
1620 })
1621 }
1622}
1623
1624pub fn build_app(
1636 state: AppState,
1637) -> App<
1638 impl actix_web::dev::ServiceFactory<
1639 ServiceRequest,
1640 Config = (),
1641 Response = ServiceResponse<EitherBody<EitherBody<BoxBody>>>,
1642 Error = ActixError,
1643 InitError = (),
1644 >,
1645> {
1646 let body_cap = state.body_cap;
1647 let dotfiles = state.dotfiles.clone();
1648
1649 let mut app = App::new()
1650 .app_data(web::Data::new(state.clone()))
1651 .app_data(web::PayloadConfig::new(body_cap))
1652 .wrap(ErrorLoggingMiddleware)
1657 .wrap(NormalizePath::new(TrailingSlash::MergeOnly))
1661 .wrap(PathTraversalGuard)
1662 .wrap(DotfileGuard::new(dotfiles));
1663
1664 app = app
1670 .route("/.well-known/solid", web::get().to(handle_well_known_solid))
1671 .route(
1672 "/.well-known/webfinger",
1673 web::get().to(handle_well_known_webfinger),
1674 )
1675 .route(
1676 "/.well-known/nodeinfo",
1677 web::get().to(handle_well_known_nodeinfo),
1678 )
1679 .route(
1680 "/.well-known/nodeinfo/2.1",
1681 web::get().to(handle_well_known_nodeinfo_2_1),
1682 );
1683
1684 #[cfg(feature = "did-nostr")]
1685 {
1686 app = app.route(
1687 "/.well-known/did/nostr/{pubkey}.json",
1688 web::get().to(handle_well_known_did_nostr),
1689 );
1690 }
1691
1692 #[cfg(feature = "nip05-endpoint")]
1696 {
1697 app = app.route(
1698 "/.well-known/nostr.json",
1699 web::get().to(handle_well_known_nip05),
1700 );
1701 }
1702
1703 app = app.route("/pay/.info", web::get().to(handle_pay_info));
1705
1706 app = app.route("/proxy", web::get().to(handle_proxy));
1708
1709 app = app
1711 .route("/api/accounts/new", web::post().to(handle_create_account))
1712 .route("/pods/check/{name}", web::get().to(handle_pod_check))
1713 .route("/login/password", web::post().to(handle_login_password))
1714 .route(
1715 "/account/password/reset",
1716 web::post().to(handle_password_reset_request),
1717 )
1718 .route(
1719 "/account/password/change",
1720 web::post().to(handle_password_change),
1721 );
1722
1723 app.route("/{tail:.*}/", web::post().to(handle_post))
1726 .route("/{tail:.*}/", web::put().to(handle_put))
1727 .route("/{tail:.*}", web::get().to(handle_get))
1728 .route("/{tail:.*}", web::head().to(handle_get))
1729 .route("/{tail:.*}", web::put().to(handle_put))
1730 .route("/{tail:.*}", web::patch().to(handle_patch))
1731 .route("/{tail:.*}", web::delete().to(handle_delete))
1732 .route(
1733 "/{tail:.*}",
1734 web::method(actix_web::http::Method::from_bytes(b"COPY").unwrap()).to(handle_copy),
1735 )
1736 .route(
1737 "/{tail:.*}",
1738 web::method(actix_web::http::Method::OPTIONS).to(handle_options),
1739 )
1740}