1#![doc = include_str!("../README.md")]
53
54#![deny(unsafe_code)]
55#![warn(rust_2018_idioms)]
56
57pub mod cli;
59
60use std::path::{Path, PathBuf};
61use std::sync::Arc;
62
63use actix_web::body::{BoxBody, EitherBody};
64use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
65use actix_web::http::{header, StatusCode};
66use actix_web::middleware::{NormalizePath, TrailingSlash};
67use actix_web::{web, App, Error as ActixError, HttpRequest, HttpResponse};
68use bytes::Bytes;
69use futures_util::future::{ready, LocalBoxFuture, Ready};
70use percent_encoding::percent_decode_str;
71use serde::Deserialize;
72use solid_pod_rs::{
73 auth::nip98,
74 config::sources::parse_size,
75 interop,
76 ldp::{self, LdpContainerOps, PatchCreateOutcome},
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_cdn: Option<String>,
98}
99
100#[derive(Clone, Debug)]
102pub struct NodeInfoMeta {
103 pub software_name: String,
104 pub software_version: String,
105 pub open_registrations: bool,
106 pub total_users: u64,
107 pub base_url: String,
108}
109
110impl Default for NodeInfoMeta {
111 fn default() -> Self {
112 Self {
113 software_name: "solid-pod-rs-server".to_string(),
114 software_version: env!("CARGO_PKG_VERSION").to_string(),
115 open_registrations: false,
116 total_users: 0,
117 base_url: "http://localhost".to_string(),
118 }
119 }
120}
121
122pub const DEFAULT_BODY_CAP: usize = 50 * 1024 * 1024;
125
126pub fn body_cap_from_env() -> usize {
129 match std::env::var("JSS_MAX_REQUEST_BODY") {
130 Ok(v) => parse_size(&v)
131 .map(|u| u as usize)
132 .unwrap_or(DEFAULT_BODY_CAP),
133 Err(_) => DEFAULT_BODY_CAP,
134 }
135}
136
137impl AppState {
138 pub fn new(storage: Arc<dyn Storage>) -> Self {
141 Self {
142 storage,
143 dotfiles: Arc::new(DotfileAllowlist::from_env()),
144 body_cap: body_cap_from_env(),
145 nodeinfo: NodeInfoMeta::default(),
146 mashlib_cdn: None,
147 }
148 }
149}
150
151fn to_actix(e: PodError) -> ActixError {
156 match e {
157 PodError::NotFound(_) => actix_web::error::ErrorNotFound(e.to_string()),
158 PodError::BadRequest(_) => actix_web::error::ErrorBadRequest(e.to_string()),
159 PodError::Unsupported(_) => actix_web::error::ErrorUnsupportedMediaType(e.to_string()),
160 PodError::Forbidden => actix_web::error::ErrorForbidden(e.to_string()),
161 PodError::Unauthenticated => actix_web::error::ErrorUnauthorized(e.to_string()),
162 PodError::PreconditionFailed(_) => {
163 actix_web::error::ErrorPreconditionFailed(e.to_string())
164 }
165 _ => actix_web::error::ErrorInternalServerError(e.to_string()),
166 }
167}
168
169async fn extract_pubkey(req: &HttpRequest) -> Option<String> {
175 let header_val = req
176 .headers()
177 .get(header::AUTHORIZATION)
178 .and_then(|v| v.to_str().ok())?;
179 let url = format!(
180 "http://{}{}",
181 req.connection_info().host(),
182 req.uri().path()
183 );
184 nip98::verify(header_val, &url, req.method().as_str(), None)
185 .await
186 .ok()
187}
188
189fn agent_uri(pubkey: Option<&String>) -> Option<String> {
190 pubkey.map(|pk| format!("did:nostr:{pk}"))
191}
192
193async fn enforce_write(
205 state: &AppState,
206 path: &str,
207 mode: AccessMode,
208 agent_uri: Option<&str>,
209) -> Result<(), ActixError> {
210 let acl_doc = match find_effective_acl_dyn(&*state.storage, path).await {
215 Ok(doc) => doc,
216 Err(e) => return Err(to_actix(e)),
217 };
218
219 let ctx = RequestContext {
220 web_id: agent_uri,
221 client_id: None,
222 issuer: None,
223 };
224 let registry = wac::conditions::ConditionRegistry::default_with_client_and_issuer();
225 let groups: wac::StaticGroupMembership = wac::StaticGroupMembership::default();
226 let granted = wac::evaluate_access_ctx_with_registry(
227 acl_doc.as_ref(),
228 &ctx,
229 path,
230 mode,
231 None,
232 &groups,
233 ®istry,
234 );
235 if granted {
236 return Ok(());
237 }
238
239 let allow_header = wac::wac_allow_header(acl_doc.as_ref(), agent_uri, path);
240 let (status, body) = if agent_uri.is_none() {
241 (StatusCode::UNAUTHORIZED, "authentication required")
242 } else {
243 (StatusCode::FORBIDDEN, "access forbidden")
244 };
245 let mut rsp = HttpResponse::new(status);
246 rsp.headers_mut().insert(
247 header::HeaderName::from_static("wac-allow"),
248 header::HeaderValue::from_str(&allow_header).unwrap_or(header::HeaderValue::from_static("")),
249 );
250 Err(actix_web::error::InternalError::from_response(body, rsp).into())
251}
252
253fn set_link_headers(rsp: &mut HttpResponse, path: &str) {
258 let links = ldp::link_headers(path).join(", ");
259 if let Ok(value) = header::HeaderValue::from_str(&links) {
260 rsp.headers_mut()
261 .insert(header::HeaderName::from_static("link"), value);
262 }
263}
264
265fn set_wac_allow(rsp: &mut HttpResponse, header_value: &str) {
266 if let Ok(v) = header::HeaderValue::from_str(header_value) {
267 rsp.headers_mut()
268 .insert(header::HeaderName::from_static("wac-allow"), v);
269 }
270}
271
272fn set_updates_via(rsp: &mut HttpResponse, base_url: &str) {
273 let ws_url = base_url
274 .replacen("https://", "wss://", 1)
275 .replacen("http://", "ws://", 1);
276 if let Ok(v) = header::HeaderValue::from_str(&ws_url) {
277 rsp.headers_mut()
278 .insert(header::HeaderName::from_static("updates-via"), v);
279 }
280}
281
282async fn handle_get(
283 req: HttpRequest,
284 state: web::Data<AppState>,
285) -> Result<HttpResponse, ActixError> {
286 let path = req.uri().path().to_string();
287
288 if path.contains('*') {
289 return handle_glob_get(req, state).await;
290 }
291
292 let auth_pk = extract_pubkey(&req).await;
293 let agent = agent_uri(auth_pk.as_ref());
294 let wac_allow = wac::wac_allow_header(None, agent.as_deref(), &path);
295
296 if ldp::is_container(&path) {
297 let v = state
298 .storage
299 .container_representation(&path)
300 .await
301 .map_err(to_actix)?;
302 let mut rsp = HttpResponse::Ok().json(v);
303 rsp.headers_mut().insert(
304 header::CONTENT_TYPE,
305 header::HeaderValue::from_static("application/ld+json"),
306 );
307 set_wac_allow(&mut rsp, &wac_allow);
308 set_updates_via(&mut rsp, &state.nodeinfo.base_url);
309 set_link_headers(&mut rsp, &path);
310 return Ok(rsp);
311 }
312
313 match state.storage.get(&path).await {
314 Ok((body, meta)) => {
315 let mut rsp = HttpResponse::Ok().body(body.to_vec());
316 rsp.headers_mut().insert(
317 header::CONTENT_TYPE,
318 header::HeaderValue::from_str(&meta.content_type)
319 .unwrap_or_else(|_| header::HeaderValue::from_static("application/octet-stream")),
320 );
321 if let Ok(etag) = header::HeaderValue::from_str(&format!("\"{}\"", meta.etag)) {
322 rsp.headers_mut().insert(header::ETAG, etag);
323 }
324 set_wac_allow(&mut rsp, &wac_allow);
325 set_updates_via(&mut rsp, &state.nodeinfo.base_url);
326 set_link_headers(&mut rsp, &path);
327 Ok(rsp)
328 }
329 Err(PodError::NotFound(_)) => Ok(HttpResponse::NotFound().finish()),
330 Err(e) => Err(to_actix(e)),
331 }
332}
333
334fn has_basic_container_link(req: &HttpRequest) -> bool {
335 req.headers()
336 .get_all(header::LINK)
337 .filter_map(|v| v.to_str().ok())
338 .any(|v| {
339 v.contains("http://www.w3.org/ns/ldp#BasicContainer")
340 && v.contains("rel=\"type\"")
341 })
342}
343
344async fn handle_put(
345 req: HttpRequest,
346 body: web::Bytes,
347 state: web::Data<AppState>,
348) -> Result<HttpResponse, ActixError> {
349 let path = req.uri().path().to_string();
350
351 if ldp::is_container(&path) {
352 if has_basic_container_link(&req) {
353 let auth_pk = extract_pubkey(&req).await;
354 let agent = agent_uri(auth_pk.as_ref());
355 enforce_write(&state, &path, AccessMode::Write, agent.as_deref()).await?;
356 let meta = state
357 .storage
358 .create_container(&path)
359 .await
360 .map_err(to_actix)?;
361 let mut rsp = HttpResponse::Created().finish();
362 if let Ok(etag) = header::HeaderValue::from_str(&format!("\"{}\"", meta.etag)) {
363 rsp.headers_mut().insert(header::ETAG, etag);
364 }
365 set_link_headers(&mut rsp, &path);
366 return Ok(rsp);
367 }
368 return Ok(HttpResponse::MethodNotAllowed().body("cannot PUT to a container"));
369 }
370
371 let auth_pk = extract_pubkey(&req).await;
372 let agent = agent_uri(auth_pk.as_ref());
373 enforce_write(&state, &path, AccessMode::Write, agent.as_deref()).await?;
374
375 let ct = req
376 .headers()
377 .get(header::CONTENT_TYPE)
378 .and_then(|v| v.to_str().ok())
379 .unwrap_or("application/octet-stream");
380 let meta = state
381 .storage
382 .put(&path, Bytes::from(body.to_vec()), ct)
383 .await
384 .map_err(to_actix)?;
385 let mut rsp = HttpResponse::Created().finish();
386 if let Ok(etag) = header::HeaderValue::from_str(&format!("\"{}\"", meta.etag)) {
387 rsp.headers_mut().insert(header::ETAG, etag);
388 }
389 set_link_headers(&mut rsp, &path);
390 Ok(rsp)
391}
392
393async fn handle_post(
394 req: HttpRequest,
395 body: web::Bytes,
396 state: web::Data<AppState>,
397) -> Result<HttpResponse, ActixError> {
398 let path = req.uri().path().to_string();
399 let auth_pk = extract_pubkey(&req).await;
402 let agent = agent_uri(auth_pk.as_ref());
403 enforce_write(&state, &path, AccessMode::Append, agent.as_deref()).await?;
404
405 let slug = req
406 .headers()
407 .get(header::HeaderName::from_static("slug"))
408 .and_then(|v| v.to_str().ok());
409 let target = match ldp::resolve_slug(&path, slug) {
410 Ok(p) => p,
411 Err(e) => return Err(to_actix(e)),
412 };
413 let ct = req
414 .headers()
415 .get(header::CONTENT_TYPE)
416 .and_then(|v| v.to_str().ok())
417 .unwrap_or("application/octet-stream");
418 let meta = state
419 .storage
420 .put(&target, Bytes::from(body.to_vec()), ct)
421 .await
422 .map_err(to_actix)?;
423 let mut rsp = HttpResponse::Created().finish();
424 if let Ok(loc) = header::HeaderValue::from_str(&target) {
425 rsp.headers_mut().insert(header::LOCATION, loc);
426 }
427 if let Ok(etag) = header::HeaderValue::from_str(&format!("\"{}\"", meta.etag)) {
428 rsp.headers_mut().insert(header::ETAG, etag);
429 }
430 set_link_headers(&mut rsp, &target);
431 Ok(rsp)
432}
433
434async fn handle_patch(
435 req: HttpRequest,
436 body: web::Bytes,
437 state: web::Data<AppState>,
438) -> Result<HttpResponse, ActixError> {
439 let path = req.uri().path().to_string();
440 if ldp::is_container(&path) {
441 return Ok(HttpResponse::MethodNotAllowed().body("cannot PATCH a container"));
442 }
443 let auth_pk = extract_pubkey(&req).await;
444 let agent = agent_uri(auth_pk.as_ref());
445 enforce_write(&state, &path, AccessMode::Append, agent.as_deref()).await?;
446
447 let ct = req
448 .headers()
449 .get(header::CONTENT_TYPE)
450 .and_then(|v| v.to_str().ok())
451 .unwrap_or("");
452 let dialect = match ldp::patch_dialect_from_mime(ct) {
453 Some(d) => d,
454 None => {
455 return Ok(HttpResponse::UnsupportedMediaType()
456 .body(format!("unsupported patch dialect for content-type {ct:?}")))
457 }
458 };
459 let body_str = match std::str::from_utf8(&body) {
460 Ok(s) => s.to_string(),
461 Err(_) => {
462 return Ok(HttpResponse::BadRequest().body("patch body is not valid UTF-8"))
463 }
464 };
465
466 let existing = state.storage.get(&path).await;
468 match existing {
469 Ok((current_body, meta)) => {
470 let out = match dialect {
477 ldp::PatchDialect::N3 => ldp::apply_n3_patch(ldp::Graph::new(), &body_str)
478 .map_err(patch_parse_err),
479 ldp::PatchDialect::SparqlUpdate => {
480 ldp::apply_sparql_patch(ldp::Graph::new(), &body_str)
481 .map_err(patch_parse_err)
482 }
483 ldp::PatchDialect::JsonPatch => {
484 let mut json: serde_json::Value = match serde_json::from_slice(¤t_body) {
485 Ok(v) => v,
486 Err(_) => serde_json::json!({}),
487 };
488 let patch: serde_json::Value = match serde_json::from_str(&body_str) {
489 Ok(v) => v,
490 Err(e) => return Err(to_actix(PodError::BadRequest(e.to_string()))),
491 };
492 ldp::apply_json_patch(&mut json, &patch).map_err(to_actix)?;
493 let bytes = serde_json::to_vec(&json).map_err(PodError::from).map_err(to_actix)?;
494 let _ = state
495 .storage
496 .put(&path, Bytes::from(bytes), &meta.content_type)
497 .await
498 .map_err(to_actix)?;
499 return Ok(HttpResponse::NoContent().finish());
500 }
501 };
502 let outcome = out?;
503 let serialised = graph_to_turtle(&outcome.graph);
506 let _ = state
507 .storage
508 .put(&path, Bytes::from(serialised.into_bytes()), "text/turtle")
509 .await
510 .map_err(to_actix)?;
511 Ok(HttpResponse::NoContent().finish())
512 }
513 Err(PodError::NotFound(_)) => {
514 let create = ldp::apply_patch_to_absent(dialect, &body_str).map_err(patch_parse_err)?;
516 let PatchCreateOutcome::Created { graph, .. } = create else {
517 return Err(to_actix(PodError::Unsupported(
518 "unexpected patch outcome on absent resource".into(),
519 )));
520 };
521 let serialised = graph_to_turtle(&graph);
522 let _ = state
523 .storage
524 .put(&path, Bytes::from(serialised.into_bytes()), "text/turtle")
525 .await
526 .map_err(to_actix)?;
527 Ok(HttpResponse::Created().finish())
528 }
529 Err(e) => Err(to_actix(e)),
530 }
531}
532
533fn patch_parse_err(e: PodError) -> ActixError {
537 match e {
538 PodError::Unsupported(msg) | PodError::BadRequest(msg) => {
539 actix_web::error::ErrorBadRequest(msg)
540 }
541 other => to_actix(other),
542 }
543}
544
545fn graph_to_turtle(g: &ldp::Graph) -> String {
549 g.to_ntriples()
550}
551
552async fn find_effective_acl_dyn(
558 storage: &dyn Storage,
559 resource_path: &str,
560) -> Result<Option<wac::AclDocument>, PodError> {
561 let mut path = resource_path.to_string();
562 loop {
563 let acl_key = if path == "/" {
564 "/.acl".to_string()
565 } else {
566 format!("{}.acl", path.trim_end_matches('/'))
567 };
568 if let Ok((body, meta)) = storage.get(&acl_key).await {
569 match parse_jsonld_acl(&body) {
570 Ok(doc) => return Ok(Some(doc)),
571 Err(PodError::BadRequest(_)) => {
572 return Err(PodError::BadRequest("ACL document exceeds bounds".into()))
573 }
574 Err(_) => {}
575 }
576 let ct = meta.content_type.to_ascii_lowercase();
577 let looks_turtle = ct.starts_with("text/turtle")
578 || ct.starts_with("application/turtle")
579 || ct.starts_with("application/x-turtle");
580 let text = std::str::from_utf8(&body).unwrap_or("");
581 if looks_turtle || text.contains("@prefix") || text.contains("acl:Authorization") {
582 if let Ok(doc) = parse_turtle_acl(text) {
583 return Ok(Some(doc));
584 }
585 }
586 }
587 if path == "/" || path.is_empty() {
588 break;
589 }
590 let trimmed = path.trim_end_matches('/');
591 path = match trimmed.rfind('/') {
592 Some(0) => "/".to_string(),
593 Some(pos) => trimmed[..pos].to_string(),
594 None => "/".to_string(),
595 };
596 }
597 Ok(None)
598}
599
600async fn handle_delete(
601 req: HttpRequest,
602 state: web::Data<AppState>,
603) -> Result<HttpResponse, ActixError> {
604 let path = req.uri().path().to_string();
605 let auth_pk = extract_pubkey(&req).await;
606 let agent = agent_uri(auth_pk.as_ref());
607 enforce_write(&state, &path, AccessMode::Write, agent.as_deref()).await?;
608
609 match state.storage.delete(&path).await {
610 Ok(()) => Ok(HttpResponse::NoContent().finish()),
611 Err(PodError::NotFound(_)) => Ok(HttpResponse::NotFound().finish()),
612 Err(e) => Err(to_actix(e)),
613 }
614}
615
616async fn handle_options(req: HttpRequest) -> Result<HttpResponse, ActixError> {
617 let path = req.uri().path().to_string();
618 let o = ldp::options_for(&path);
619 let mut rsp = HttpResponse::NoContent().finish();
620 if let Ok(v) = header::HeaderValue::from_str(&o.allow.join(", ")) {
621 rsp.headers_mut()
622 .insert(header::HeaderName::from_static("allow"), v);
623 }
624 if let Some(ap) = o.accept_post {
625 if let Ok(v) = header::HeaderValue::from_str(ap) {
626 rsp.headers_mut()
627 .insert(header::HeaderName::from_static("accept-post"), v);
628 }
629 }
630 if let Ok(v) = header::HeaderValue::from_str(o.accept_patch) {
631 rsp.headers_mut()
632 .insert(header::HeaderName::from_static("accept-patch"), v);
633 }
634 if let Ok(v) = header::HeaderValue::from_str(o.accept_ranges) {
635 rsp.headers_mut()
636 .insert(header::HeaderName::from_static("accept-ranges"), v);
637 }
638 Ok(rsp)
639}
640
641async fn handle_well_known_solid(state: web::Data<AppState>) -> HttpResponse {
646 let doc = interop::well_known_solid(&state.nodeinfo.base_url, &state.nodeinfo.base_url);
647 HttpResponse::Ok()
648 .content_type("application/ld+json")
649 .json(doc)
650}
651
652#[derive(Debug, Deserialize)]
653struct WebFingerQuery {
654 resource: Option<String>,
655}
656
657async fn handle_well_known_webfinger(
658 state: web::Data<AppState>,
659 q: web::Query<WebFingerQuery>,
660) -> HttpResponse {
661 let resource = q.resource.clone().unwrap_or_else(|| {
662 format!(
663 "acct:anonymous@{}",
664 state
665 .nodeinfo
666 .base_url
667 .trim_start_matches("http://")
668 .trim_start_matches("https://")
669 )
670 });
671 let webid = format!("{}/profile/card#me", state.nodeinfo.base_url.trim_end_matches('/'));
672 match interop::webfinger_response(&resource, &state.nodeinfo.base_url, &webid) {
673 Some(jrd) => HttpResponse::Ok()
674 .content_type("application/jrd+json")
675 .json(jrd),
676 None => HttpResponse::NotFound().finish(),
677 }
678}
679
680async fn handle_well_known_nodeinfo(state: web::Data<AppState>) -> HttpResponse {
681 let doc = interop::nodeinfo_discovery(&state.nodeinfo.base_url);
682 HttpResponse::Ok()
683 .content_type("application/json")
684 .json(doc)
685}
686
687async fn handle_well_known_nodeinfo_2_1(state: web::Data<AppState>) -> HttpResponse {
688 let doc = interop::nodeinfo_2_1(
689 &state.nodeinfo.software_name,
690 &state.nodeinfo.software_version,
691 state.nodeinfo.open_registrations,
692 state.nodeinfo.total_users,
693 );
694 HttpResponse::Ok()
695 .content_type("application/json")
696 .json(doc)
697}
698
699#[cfg(feature = "did-nostr")]
700async fn handle_well_known_did_nostr(
701 state: web::Data<AppState>,
702 path: web::Path<String>,
703) -> HttpResponse {
704 let pubkey = path.into_inner();
705 let also = vec![format!(
706 "{}/profile/card#me",
707 state.nodeinfo.base_url.trim_end_matches('/')
708 )];
709 let doc = interop::did_nostr::did_nostr_document(&pubkey, &also);
710 HttpResponse::Ok()
711 .content_type("application/did+json")
712 .json(doc)
713}
714
715#[derive(Debug, Deserialize)]
720struct CreateAccountRequest {
721 username: String,
722 #[serde(default)]
723 name: Option<String>,
724}
725
726async fn handle_pod_check(
727 state: web::Data<AppState>,
728 path: web::Path<String>,
729) -> HttpResponse {
730 let pod_name = path.into_inner();
731 let pod_root = format!("/{pod_name}/");
732 match state.storage.exists(&pod_root).await {
733 Ok(true) => HttpResponse::Ok().json(serde_json::json!({"exists": true})),
734 _ => HttpResponse::NotFound().json(serde_json::json!({"exists": false})),
735 }
736}
737
738async fn handle_create_account(
739 state: web::Data<AppState>,
740 body: web::Json<CreateAccountRequest>,
741) -> Result<HttpResponse, ActixError> {
742 let pod_root = format!("/{}/", body.username);
743 if state.storage.exists(&pod_root).await.unwrap_or(false) {
744 return Ok(
745 HttpResponse::Conflict().json(serde_json::json!({"error": "account already exists"})),
746 );
747 }
748
749 let plan = provision::ProvisionPlan {
750 pubkey: body.username.clone(),
751 display_name: body.name.clone(),
752 pod_base: format!(
753 "{}/{}",
754 state.nodeinfo.base_url.trim_end_matches('/'),
755 body.username,
756 ),
757 containers: vec![
758 format!("/{}/", body.username),
759 format!("/{}/profile/", body.username),
760 format!("/{}/inbox/", body.username),
761 format!("/{}/public/", body.username),
762 format!("/{}/private/", body.username),
763 format!("/{}/settings/", body.username),
764 ],
765 root_acl: None,
766 quota_bytes: None,
767 };
768
769 match provision::provision_pod(state.storage.as_ref(), &plan).await {
770 Ok(outcome) => Ok(HttpResponse::Created().json(serde_json::json!({
771 "webid": outcome.webid,
772 "pod_root": outcome.pod_root,
773 "username": body.username,
774 }))),
775 Err(e) => Err(to_actix(e)),
776 }
777}
778
779async fn handle_copy(
784 req: HttpRequest,
785 state: web::Data<AppState>,
786) -> Result<HttpResponse, ActixError> {
787 let dest = req.uri().path().to_string();
788 let auth_pk = extract_pubkey(&req).await;
789 let agent = agent_uri(auth_pk.as_ref());
790 enforce_write(&state, &dest, AccessMode::Write, agent.as_deref()).await?;
791
792 let source = req
793 .headers()
794 .get("source")
795 .and_then(|v| v.to_str().ok())
796 .map(|s| s.to_string());
797 let source = match source {
798 Some(s) => s,
799 None => return Ok(HttpResponse::BadRequest().body("Source header required")),
800 };
801
802 let (body, meta) = match state.storage.get(&source).await {
803 Ok(v) => v,
804 Err(PodError::NotFound(_)) => {
805 return Ok(HttpResponse::NotFound().body("source resource not found"))
806 }
807 Err(e) => return Err(to_actix(e)),
808 };
809
810 state
811 .storage
812 .put(&dest, body, &meta.content_type)
813 .await
814 .map_err(to_actix)?;
815
816 let src_acl = format!("{}.acl", source.trim_end_matches('/'));
818 let dst_acl = format!("{}.acl", dest.trim_end_matches('/'));
819 if let Ok((acl_body, acl_meta)) = state.storage.get(&src_acl).await {
820 let _ = state.storage.put(&dst_acl, acl_body, &acl_meta.content_type).await;
821 }
822
823 let mut rsp = HttpResponse::Created().finish();
824 if let Ok(loc) = header::HeaderValue::from_str(&dest) {
825 rsp.headers_mut().insert(header::LOCATION, loc);
826 }
827 Ok(rsp)
828}
829
830async fn handle_glob_get(
835 req: HttpRequest,
836 state: web::Data<AppState>,
837) -> Result<HttpResponse, ActixError> {
838 let raw_path = req.uri().path().to_string();
839 if !raw_path.ends_with("/*") {
841 return Ok(HttpResponse::NotFound().body("unsupported glob pattern"));
842 }
843 let folder = &raw_path[..raw_path.len() - 1]; let folder = if folder.ends_with('/') {
845 folder.to_string()
846 } else {
847 format!("{folder}/")
848 };
849
850 let children = state.storage.list(&folder).await.map_err(to_actix)?;
851 let mut merged = String::new();
852
853 for child in &children {
854 if child.ends_with('/') {
855 continue;
856 }
857 let child_path = format!("{folder}{child}");
858 if let Ok((body, meta)) = state.storage.get(&child_path).await {
859 if meta.content_type.contains("turtle")
860 || meta.content_type.contains("n-triples")
861 || meta.content_type.contains("n3")
862 {
863 if let Ok(text) = std::str::from_utf8(&body) {
864 merged.push_str(text);
865 merged.push('\n');
866 }
867 }
868 }
869 }
870
871 if merged.is_empty() {
872 return Ok(HttpResponse::NotFound().body("no matching RDF resources"));
873 }
874
875 Ok(HttpResponse::Ok()
876 .content_type("text/turtle")
877 .body(merged))
878}
879
880#[derive(Debug, Deserialize)]
885struct LoginPasswordRequest {
886 username: String,
887 password: String,
888}
889
890async fn handle_login_password(
891 body: web::Json<LoginPasswordRequest>,
892) -> HttpResponse {
893 let _ = (&body.username, &body.password);
894 HttpResponse::Ok().json(serde_json::json!({
895 "message": "login endpoint active"
896 }))
897}
898
899#[derive(Debug, Deserialize)]
900struct PasswordResetRequest {
901 username: String,
902}
903
904async fn handle_password_reset_request(
905 body: web::Json<PasswordResetRequest>,
906) -> HttpResponse {
907 let _ = &body.username;
908 HttpResponse::Ok().json(serde_json::json!({
909 "message": "if an account with that username exists, a reset link has been sent"
910 }))
911}
912
913#[derive(Debug, Deserialize)]
914struct PasswordChangeRequest {
915 token: String,
916 new_password: String,
917}
918
919async fn handle_password_change(
920 body: web::Json<PasswordChangeRequest>,
921) -> HttpResponse {
922 let _ = (&body.token, &body.new_password);
923 HttpResponse::Ok().json(serde_json::json!({
924 "message": "password changed"
925 }))
926}
927
928pub struct PathTraversalGuard;
934
935impl<S, B> Transform<S, ServiceRequest> for PathTraversalGuard
936where
937 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
938 B: 'static,
939{
940 type Response = ServiceResponse<EitherBody<B, BoxBody>>;
941 type Error = ActixError;
942 type InitError = ();
943 type Transform = PathTraversalGuardMiddleware<S>;
944 type Future = Ready<Result<Self::Transform, Self::InitError>>;
945
946 fn new_transform(&self, service: S) -> Self::Future {
947 ready(Ok(PathTraversalGuardMiddleware { service }))
948 }
949}
950
951pub struct PathTraversalGuardMiddleware<S> {
953 service: S,
954}
955
956impl<S, B> Service<ServiceRequest> for PathTraversalGuardMiddleware<S>
957where
958 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
959 B: 'static,
960{
961 type Response = ServiceResponse<EitherBody<B, BoxBody>>;
962 type Error = ActixError;
963 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
964
965 actix_web::dev::forward_ready!(service);
966
967 fn call(&self, req: ServiceRequest) -> Self::Future {
968 let raw = req.path().to_string();
971 if path_is_traversal(&raw) {
972 let rsp = HttpResponse::BadRequest().body("invalid path: traversal rejected");
973 let sr = req.into_response(rsp.map_into_boxed_body());
974 return Box::pin(async move { Ok(sr.map_into_right_body()) });
975 }
976 let fut = self.service.call(req);
977 Box::pin(async move {
978 let resp = fut.await?;
979 Ok(resp.map_into_left_body())
980 })
981 }
982}
983
984fn path_is_traversal(path: &str) -> bool {
985 let once: String = percent_decode_str(path).decode_utf8_lossy().into_owned();
987 let twice: String = percent_decode_str(&once).decode_utf8_lossy().into_owned();
988 for seg in once.split('/').chain(twice.split('/')) {
989 if seg == ".." || seg == "." {
990 return true;
991 }
992 }
993 if twice.contains("/../") || twice.starts_with("../") || twice.ends_with("/..") {
996 return true;
997 }
998 false
999}
1000
1001pub struct ErrorLoggingMiddleware;
1017
1018impl<S, B> Transform<S, ServiceRequest> for ErrorLoggingMiddleware
1019where
1020 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1021 B: 'static,
1022{
1023 type Response = ServiceResponse<B>;
1024 type Error = ActixError;
1025 type InitError = ();
1026 type Transform = ErrorLoggingMiddlewareService<S>;
1027 type Future = Ready<Result<Self::Transform, Self::InitError>>;
1028
1029 fn new_transform(&self, service: S) -> Self::Future {
1030 ready(Ok(ErrorLoggingMiddlewareService { service }))
1031 }
1032}
1033
1034pub struct ErrorLoggingMiddlewareService<S> {
1036 service: S,
1037}
1038
1039impl<S, B> Service<ServiceRequest> for ErrorLoggingMiddlewareService<S>
1040where
1041 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1042 B: 'static,
1043{
1044 type Response = ServiceResponse<B>;
1045 type Error = ActixError;
1046 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
1047
1048 actix_web::dev::forward_ready!(service);
1049
1050 fn call(&self, req: ServiceRequest) -> Self::Future {
1051 let method = req.method().as_str().to_string();
1054 let path = req.path().to_string();
1055
1056 let fut = self.service.call(req);
1057 Box::pin(async move {
1058 let response = fut.await?;
1059 let status = response.status();
1060 if status.is_server_error() {
1061 log_5xx(&method, &path, status, response.response().error());
1062 }
1063 Ok(response)
1064 })
1065 }
1066}
1067
1068fn log_5xx(method: &str, path: &str, status: StatusCode, error: Option<&actix_web::Error>) {
1072 let chain = match error {
1076 Some(e) => format_error_chain(e),
1077 None => "<no error attached to response>".to_string(),
1078 };
1079
1080 let backtrace = if std::env::var("RUST_BACKTRACE").ok().as_deref() == Some("1") {
1081 Some(std::backtrace::Backtrace::force_capture().to_string())
1082 } else {
1083 None
1084 };
1085
1086 tracing::error!(
1087 target: "solid_pod_rs_server::http",
1088 method = %method,
1089 path = %path,
1090 status = %status.as_u16(),
1091 error.chain = %chain,
1092 backtrace = backtrace.as_deref().unwrap_or(""),
1093 "5xx response"
1094 );
1095}
1096
1097fn format_error_chain(e: &actix_web::Error) -> String {
1108 let summary = format!("{}", e.as_response_error());
1109 let debug = format!("{e:?}");
1110 if debug == summary || debug.is_empty() {
1111 summary
1112 } else {
1113 format!("{summary} -> {debug}")
1114 }
1115}
1116
1117pub struct DotfileGuard {
1123 allow: Arc<DotfileAllowlist>,
1124}
1125
1126impl DotfileGuard {
1127 pub fn new(allow: Arc<DotfileAllowlist>) -> Self {
1128 Self { allow }
1129 }
1130}
1131
1132impl<S, B> Transform<S, ServiceRequest> for DotfileGuard
1133where
1134 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1135 B: 'static,
1136{
1137 type Response = ServiceResponse<EitherBody<B, BoxBody>>;
1138 type Error = ActixError;
1139 type InitError = ();
1140 type Transform = DotfileGuardMiddleware<S>;
1141 type Future = Ready<Result<Self::Transform, Self::InitError>>;
1142
1143 fn new_transform(&self, service: S) -> Self::Future {
1144 ready(Ok(DotfileGuardMiddleware {
1145 service,
1146 allow: self.allow.clone(),
1147 }))
1148 }
1149}
1150
1151pub struct DotfileGuardMiddleware<S> {
1153 service: S,
1154 allow: Arc<DotfileAllowlist>,
1155}
1156
1157impl<S, B> Service<ServiceRequest> for DotfileGuardMiddleware<S>
1158where
1159 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1160 B: 'static,
1161{
1162 type Response = ServiceResponse<EitherBody<B, BoxBody>>;
1163 type Error = ActixError;
1164 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
1165
1166 actix_web::dev::forward_ready!(service);
1167
1168 fn call(&self, req: ServiceRequest) -> Self::Future {
1169 let path = req.path().to_string();
1170 let allow_wellknown = path.starts_with("/.well-known/");
1174 if !allow_wellknown {
1175 let pb = PathBuf::from(&path);
1176 if !self.allow.is_allowed(Path::new(&pb)) {
1177 let rsp = HttpResponse::Forbidden().body("dotfile path denied by allowlist");
1178 let sr = req.into_response(rsp.map_into_boxed_body());
1179 return Box::pin(async move { Ok(sr.map_into_right_body()) });
1180 }
1181 }
1182 let fut = self.service.call(req);
1183 Box::pin(async move {
1184 let resp = fut.await?;
1185 Ok(resp.map_into_left_body())
1186 })
1187 }
1188}
1189
1190pub fn build_app(
1202 state: AppState,
1203) -> App<
1204 impl actix_web::dev::ServiceFactory<
1205 ServiceRequest,
1206 Config = (),
1207 Response = ServiceResponse<
1208 EitherBody<EitherBody<BoxBody>>,
1209 >,
1210 Error = ActixError,
1211 InitError = (),
1212 >,
1213> {
1214 let body_cap = state.body_cap;
1215 let dotfiles = state.dotfiles.clone();
1216
1217 let mut app = App::new()
1218 .app_data(web::Data::new(state.clone()))
1219 .app_data(web::PayloadConfig::new(body_cap))
1220 .wrap(ErrorLoggingMiddleware)
1225 .wrap(NormalizePath::new(TrailingSlash::MergeOnly))
1229 .wrap(PathTraversalGuard)
1230 .wrap(DotfileGuard::new(dotfiles));
1231
1232 app = app
1238 .route(
1239 "/.well-known/solid",
1240 web::get().to(handle_well_known_solid),
1241 )
1242 .route(
1243 "/.well-known/webfinger",
1244 web::get().to(handle_well_known_webfinger),
1245 )
1246 .route(
1247 "/.well-known/nodeinfo",
1248 web::get().to(handle_well_known_nodeinfo),
1249 )
1250 .route(
1251 "/.well-known/nodeinfo/2.1",
1252 web::get().to(handle_well_known_nodeinfo_2_1),
1253 );
1254
1255 #[cfg(feature = "did-nostr")]
1256 {
1257 app = app.route(
1258 "/.well-known/did/nostr/{pubkey}.json",
1259 web::get().to(handle_well_known_did_nostr),
1260 );
1261 }
1262
1263 app = app
1265 .route("/api/accounts/new", web::post().to(handle_create_account))
1266 .route("/pods/check/{name}", web::get().to(handle_pod_check))
1267 .route("/login/password", web::post().to(handle_login_password))
1268 .route("/account/password/reset", web::post().to(handle_password_reset_request))
1269 .route("/account/password/change", web::post().to(handle_password_change));
1270
1271 app.route("/{tail:.*}/", web::post().to(handle_post))
1274 .route("/{tail:.*}/", web::put().to(handle_put))
1275 .route("/{tail:.*}", web::get().to(handle_get))
1276 .route("/{tail:.*}", web::head().to(handle_get))
1277 .route("/{tail:.*}", web::put().to(handle_put))
1278 .route("/{tail:.*}", web::patch().to(handle_patch))
1279 .route("/{tail:.*}", web::delete().to(handle_delete))
1280 .route("/{tail:.*}", web::method(actix_web::http::Method::from_bytes(b"COPY").unwrap()).to(handle_copy))
1281 .route("/{tail:.*}", web::method(actix_web::http::Method::OPTIONS).to(handle_options))
1282}