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 mashlib::{self, MashlibConfig},
78 provision,
79 security::DotfileAllowlist,
80 storage::Storage,
81 wac::{
82 self, conditions::RequestContext, parse_jsonld_acl, parser::parse_turtle_acl, AccessMode,
83 },
84 PodError,
85};
86
87#[derive(Clone)]
93pub struct AppState {
94 pub storage: Arc<dyn Storage>,
95 pub dotfiles: Arc<DotfileAllowlist>,
96 pub body_cap: usize,
97 pub nodeinfo: NodeInfoMeta,
98 pub mashlib: MashlibConfig,
99 pub mashlib_cdn: Option<String>,
102}
103
104#[derive(Clone, Debug)]
106pub struct NodeInfoMeta {
107 pub software_name: String,
108 pub software_version: String,
109 pub open_registrations: bool,
110 pub total_users: u64,
111 pub base_url: String,
112}
113
114impl Default for NodeInfoMeta {
115 fn default() -> Self {
116 Self {
117 software_name: "solid-pod-rs-server".to_string(),
118 software_version: env!("CARGO_PKG_VERSION").to_string(),
119 open_registrations: false,
120 total_users: 0,
121 base_url: "http://localhost".to_string(),
122 }
123 }
124}
125
126pub const DEFAULT_BODY_CAP: usize = 50 * 1024 * 1024;
129
130pub fn body_cap_from_env() -> usize {
133 match std::env::var("JSS_MAX_REQUEST_BODY") {
134 Ok(v) => parse_size(&v)
135 .map(|u| u as usize)
136 .unwrap_or(DEFAULT_BODY_CAP),
137 Err(_) => DEFAULT_BODY_CAP,
138 }
139}
140
141impl AppState {
142 pub fn new(storage: Arc<dyn Storage>) -> Self {
145 Self {
146 storage,
147 dotfiles: Arc::new(DotfileAllowlist::from_env()),
148 body_cap: body_cap_from_env(),
149 nodeinfo: NodeInfoMeta::default(),
150 mashlib: MashlibConfig::default(),
151 mashlib_cdn: None,
152 }
153 }
154}
155
156fn to_actix(e: PodError) -> ActixError {
161 match e {
162 PodError::NotFound(_) => actix_web::error::ErrorNotFound(e.to_string()),
163 PodError::BadRequest(_) => actix_web::error::ErrorBadRequest(e.to_string()),
164 PodError::Unsupported(_) => actix_web::error::ErrorUnsupportedMediaType(e.to_string()),
165 PodError::Forbidden => actix_web::error::ErrorForbidden(e.to_string()),
166 PodError::Unauthenticated => actix_web::error::ErrorUnauthorized(e.to_string()),
167 PodError::PreconditionFailed(_) => {
168 actix_web::error::ErrorPreconditionFailed(e.to_string())
169 }
170 _ => actix_web::error::ErrorInternalServerError(e.to_string()),
171 }
172}
173
174async fn extract_pubkey(req: &HttpRequest) -> Option<String> {
180 let header_val = req
181 .headers()
182 .get(header::AUTHORIZATION)
183 .and_then(|v| v.to_str().ok())?;
184 let url = format!(
185 "http://{}{}",
186 req.connection_info().host(),
187 req.uri().path()
188 );
189 nip98::verify(header_val, &url, req.method().as_str(), None)
190 .await
191 .ok()
192}
193
194fn agent_uri(pubkey: Option<&String>) -> Option<String> {
195 pubkey.map(|pk| format!("did:nostr:{pk}"))
196}
197
198async fn enforce_write(
210 state: &AppState,
211 path: &str,
212 mode: AccessMode,
213 agent_uri: Option<&str>,
214) -> Result<(), ActixError> {
215 let acl_doc = match find_effective_acl_dyn(&*state.storage, path).await {
220 Ok(doc) => doc,
221 Err(e) => return Err(to_actix(e)),
222 };
223
224 let ctx = RequestContext {
225 web_id: agent_uri,
226 client_id: None,
227 issuer: None,
228 };
229 let registry = wac::conditions::ConditionRegistry::default_with_client_and_issuer();
230 let groups: wac::StaticGroupMembership = wac::StaticGroupMembership::default();
231 let granted = wac::evaluate_access_ctx_with_registry(
232 acl_doc.as_ref(),
233 &ctx,
234 path,
235 mode,
236 None,
237 &groups,
238 ®istry,
239 );
240 if granted {
241 return Ok(());
242 }
243
244 let allow_header = wac::wac_allow_header(acl_doc.as_ref(), agent_uri, path);
245 let (status, body) = if agent_uri.is_none() {
246 (StatusCode::UNAUTHORIZED, "authentication required")
247 } else {
248 (StatusCode::FORBIDDEN, "access forbidden")
249 };
250 let mut rsp = HttpResponse::new(status);
251 rsp.headers_mut().insert(
252 header::HeaderName::from_static("wac-allow"),
253 header::HeaderValue::from_str(&allow_header).unwrap_or(header::HeaderValue::from_static("")),
254 );
255 Err(actix_web::error::InternalError::from_response(body, rsp).into())
256}
257
258fn set_link_headers(rsp: &mut HttpResponse, path: &str) {
263 let links = ldp::link_headers(path).join(", ");
264 if let Ok(value) = header::HeaderValue::from_str(&links) {
265 rsp.headers_mut()
266 .insert(header::HeaderName::from_static("link"), value);
267 }
268}
269
270fn set_wac_allow(rsp: &mut HttpResponse, header_value: &str) {
271 if let Ok(v) = header::HeaderValue::from_str(header_value) {
272 rsp.headers_mut()
273 .insert(header::HeaderName::from_static("wac-allow"), v);
274 }
275}
276
277fn set_updates_via(rsp: &mut HttpResponse, base_url: &str) {
278 let ws_url = base_url
279 .replacen("https://", "wss://", 1)
280 .replacen("http://", "ws://", 1);
281 if let Ok(v) = header::HeaderValue::from_str(&ws_url) {
282 rsp.headers_mut()
283 .insert(header::HeaderName::from_static("updates-via"), v);
284 }
285}
286
287async fn handle_get(
288 req: HttpRequest,
289 state: web::Data<AppState>,
290) -> Result<HttpResponse, ActixError> {
291 let path = req.uri().path().to_string();
292
293 if path.contains('*') {
294 return handle_glob_get(req, state).await;
295 }
296
297 let auth_pk = extract_pubkey(&req).await;
298 let agent = agent_uri(auth_pk.as_ref());
299 let wac_allow = wac::wac_allow_header(None, agent.as_deref(), &path);
300
301 if ldp::is_container(&path) {
302 let v = state
303 .storage
304 .container_representation(&path)
305 .await
306 .map_err(to_actix)?;
307
308 let accept = req.headers().get(header::ACCEPT)
310 .and_then(|v| v.to_str().ok())
311 .unwrap_or("");
312 let sec_fetch_dest = req.headers().get("sec-fetch-dest")
313 .and_then(|v| v.to_str().ok());
314 if mashlib::should_serve(accept, sec_fetch_dest, "application/ld+json", state.mashlib.enabled) {
315 let json_ld = serde_json::to_string(&v).ok();
316 let html = mashlib::generate_html(
317 &path,
318 &state.mashlib,
319 json_ld.as_deref(),
320 );
321 let mut rsp = HttpResponse::Ok()
322 .content_type("text/html; charset=utf-8")
323 .insert_header(("X-Frame-Options", "DENY"))
324 .insert_header(("Content-Security-Policy", "frame-ancestors 'none'"))
325 .insert_header(("Cache-Control", "no-store"))
326 .body(html);
327 set_wac_allow(&mut rsp, &wac_allow);
328 set_updates_via(&mut rsp, &state.nodeinfo.base_url);
329 set_link_headers(&mut rsp, &path);
330 return Ok(rsp);
331 }
332
333 let mut rsp = HttpResponse::Ok().json(v);
334 rsp.headers_mut().insert(
335 header::CONTENT_TYPE,
336 header::HeaderValue::from_static("application/ld+json"),
337 );
338 set_wac_allow(&mut rsp, &wac_allow);
339 set_updates_via(&mut rsp, &state.nodeinfo.base_url);
340 set_link_headers(&mut rsp, &path);
341 return Ok(rsp);
342 }
343
344 match state.storage.get(&path).await {
345 Ok((body, meta)) => {
346 let accept = req.headers().get(header::ACCEPT)
348 .and_then(|v| v.to_str().ok())
349 .unwrap_or("");
350 let sec_fetch_dest = req.headers().get("sec-fetch-dest")
351 .and_then(|v| v.to_str().ok());
352 if mashlib::should_serve(accept, sec_fetch_dest, &meta.content_type, state.mashlib.enabled) {
353 let embed = if body.len() <= state.mashlib.data_island_max_bytes {
354 std::str::from_utf8(&body).ok().map(|s| s.to_string())
355 } else {
356 None
357 };
358 let html = mashlib::generate_html(
359 &path,
360 &state.mashlib,
361 embed.as_deref(),
362 );
363 let mut rsp = HttpResponse::Ok()
364 .content_type("text/html; charset=utf-8")
365 .insert_header(("X-Frame-Options", "DENY"))
366 .insert_header(("Content-Security-Policy", "frame-ancestors 'none'"))
367 .insert_header(("Cache-Control", "no-store"))
368 .body(html);
369 set_wac_allow(&mut rsp, &wac_allow);
370 set_updates_via(&mut rsp, &state.nodeinfo.base_url);
371 set_link_headers(&mut rsp, &path);
372 return Ok(rsp);
373 }
374
375 let mut rsp = HttpResponse::Ok().body(body.to_vec());
376 rsp.headers_mut().insert(
377 header::CONTENT_TYPE,
378 header::HeaderValue::from_str(&meta.content_type)
379 .unwrap_or_else(|_| header::HeaderValue::from_static("application/octet-stream")),
380 );
381 if let Ok(etag) = header::HeaderValue::from_str(&format!("\"{}\"", meta.etag)) {
382 rsp.headers_mut().insert(header::ETAG, etag);
383 }
384 set_wac_allow(&mut rsp, &wac_allow);
385 set_updates_via(&mut rsp, &state.nodeinfo.base_url);
386 set_link_headers(&mut rsp, &path);
387 Ok(rsp)
388 }
389 Err(PodError::NotFound(_)) => Ok(HttpResponse::NotFound().finish()),
390 Err(e) => Err(to_actix(e)),
391 }
392}
393
394fn has_basic_container_link(req: &HttpRequest) -> bool {
395 req.headers()
396 .get_all(header::LINK)
397 .filter_map(|v| v.to_str().ok())
398 .any(|v| {
399 v.contains("http://www.w3.org/ns/ldp#BasicContainer")
400 && v.contains("rel=\"type\"")
401 })
402}
403
404async fn handle_put(
405 req: HttpRequest,
406 body: web::Bytes,
407 state: web::Data<AppState>,
408) -> Result<HttpResponse, ActixError> {
409 let path = req.uri().path().to_string();
410
411 if ldp::is_container(&path) {
412 if has_basic_container_link(&req) {
413 let auth_pk = extract_pubkey(&req).await;
414 let agent = agent_uri(auth_pk.as_ref());
415 enforce_write(&state, &path, AccessMode::Write, agent.as_deref()).await?;
416 let meta = state
417 .storage
418 .create_container(&path)
419 .await
420 .map_err(to_actix)?;
421 let mut rsp = HttpResponse::Created().finish();
422 if let Ok(etag) = header::HeaderValue::from_str(&format!("\"{}\"", meta.etag)) {
423 rsp.headers_mut().insert(header::ETAG, etag);
424 }
425 set_link_headers(&mut rsp, &path);
426 return Ok(rsp);
427 }
428 return Ok(HttpResponse::MethodNotAllowed().body("cannot PUT to a container"));
429 }
430
431 let auth_pk = extract_pubkey(&req).await;
432 let agent = agent_uri(auth_pk.as_ref());
433 enforce_write(&state, &path, AccessMode::Write, agent.as_deref()).await?;
434
435 let ct = req
436 .headers()
437 .get(header::CONTENT_TYPE)
438 .and_then(|v| v.to_str().ok())
439 .unwrap_or("application/octet-stream");
440 let meta = state
441 .storage
442 .put(&path, Bytes::from(body.to_vec()), ct)
443 .await
444 .map_err(to_actix)?;
445 let mut rsp = HttpResponse::Created().finish();
446 if let Ok(etag) = header::HeaderValue::from_str(&format!("\"{}\"", meta.etag)) {
447 rsp.headers_mut().insert(header::ETAG, etag);
448 }
449 set_link_headers(&mut rsp, &path);
450 Ok(rsp)
451}
452
453async fn handle_post(
454 req: HttpRequest,
455 body: web::Bytes,
456 state: web::Data<AppState>,
457) -> Result<HttpResponse, ActixError> {
458 let path = req.uri().path().to_string();
459 let auth_pk = extract_pubkey(&req).await;
462 let agent = agent_uri(auth_pk.as_ref());
463 enforce_write(&state, &path, AccessMode::Append, agent.as_deref()).await?;
464
465 let slug = req
466 .headers()
467 .get(header::HeaderName::from_static("slug"))
468 .and_then(|v| v.to_str().ok());
469 let target = match ldp::resolve_slug(&path, slug) {
470 Ok(p) => p,
471 Err(e) => return Err(to_actix(e)),
472 };
473 let ct = req
474 .headers()
475 .get(header::CONTENT_TYPE)
476 .and_then(|v| v.to_str().ok())
477 .unwrap_or("application/octet-stream");
478 let meta = state
479 .storage
480 .put(&target, Bytes::from(body.to_vec()), ct)
481 .await
482 .map_err(to_actix)?;
483 let mut rsp = HttpResponse::Created().finish();
484 if let Ok(loc) = header::HeaderValue::from_str(&target) {
485 rsp.headers_mut().insert(header::LOCATION, loc);
486 }
487 if let Ok(etag) = header::HeaderValue::from_str(&format!("\"{}\"", meta.etag)) {
488 rsp.headers_mut().insert(header::ETAG, etag);
489 }
490 set_link_headers(&mut rsp, &target);
491 Ok(rsp)
492}
493
494async fn handle_patch(
495 req: HttpRequest,
496 body: web::Bytes,
497 state: web::Data<AppState>,
498) -> Result<HttpResponse, ActixError> {
499 let path = req.uri().path().to_string();
500 if ldp::is_container(&path) {
501 return Ok(HttpResponse::MethodNotAllowed().body("cannot PATCH a container"));
502 }
503 let auth_pk = extract_pubkey(&req).await;
504 let agent = agent_uri(auth_pk.as_ref());
505 enforce_write(&state, &path, AccessMode::Append, agent.as_deref()).await?;
506
507 let ct = req
508 .headers()
509 .get(header::CONTENT_TYPE)
510 .and_then(|v| v.to_str().ok())
511 .unwrap_or("");
512 let dialect = match ldp::patch_dialect_from_mime(ct) {
513 Some(d) => d,
514 None => {
515 return Ok(HttpResponse::UnsupportedMediaType()
516 .body(format!("unsupported patch dialect for content-type {ct:?}")))
517 }
518 };
519 let body_str = match std::str::from_utf8(&body) {
520 Ok(s) => s.to_string(),
521 Err(_) => {
522 return Ok(HttpResponse::BadRequest().body("patch body is not valid UTF-8"))
523 }
524 };
525
526 let existing = state.storage.get(&path).await;
528 match existing {
529 Ok((current_body, meta)) => {
530 let out = match dialect {
537 ldp::PatchDialect::N3 => ldp::apply_n3_patch(ldp::Graph::new(), &body_str)
538 .map_err(patch_parse_err),
539 ldp::PatchDialect::SparqlUpdate => {
540 ldp::apply_sparql_patch(ldp::Graph::new(), &body_str)
541 .map_err(patch_parse_err)
542 }
543 ldp::PatchDialect::JsonPatch => {
544 let mut json: serde_json::Value = match serde_json::from_slice(¤t_body) {
545 Ok(v) => v,
546 Err(_) => serde_json::json!({}),
547 };
548 let patch: serde_json::Value = match serde_json::from_str(&body_str) {
549 Ok(v) => v,
550 Err(e) => return Err(to_actix(PodError::BadRequest(e.to_string()))),
551 };
552 ldp::apply_json_patch(&mut json, &patch).map_err(to_actix)?;
553 let bytes = serde_json::to_vec(&json).map_err(PodError::from).map_err(to_actix)?;
554 let _ = state
555 .storage
556 .put(&path, Bytes::from(bytes), &meta.content_type)
557 .await
558 .map_err(to_actix)?;
559 return Ok(HttpResponse::NoContent().finish());
560 }
561 };
562 let outcome = out?;
563 let serialised = graph_to_turtle(&outcome.graph);
566 let _ = state
567 .storage
568 .put(&path, Bytes::from(serialised.into_bytes()), "text/turtle")
569 .await
570 .map_err(to_actix)?;
571 Ok(HttpResponse::NoContent().finish())
572 }
573 Err(PodError::NotFound(_)) => {
574 let create = ldp::apply_patch_to_absent(dialect, &body_str).map_err(patch_parse_err)?;
576 let PatchCreateOutcome::Created { graph, .. } = create else {
577 return Err(to_actix(PodError::Unsupported(
578 "unexpected patch outcome on absent resource".into(),
579 )));
580 };
581 let serialised = graph_to_turtle(&graph);
582 let _ = state
583 .storage
584 .put(&path, Bytes::from(serialised.into_bytes()), "text/turtle")
585 .await
586 .map_err(to_actix)?;
587 Ok(HttpResponse::Created().finish())
588 }
589 Err(e) => Err(to_actix(e)),
590 }
591}
592
593fn patch_parse_err(e: PodError) -> ActixError {
597 match e {
598 PodError::Unsupported(msg) | PodError::BadRequest(msg) => {
599 actix_web::error::ErrorBadRequest(msg)
600 }
601 other => to_actix(other),
602 }
603}
604
605fn graph_to_turtle(g: &ldp::Graph) -> String {
609 g.to_ntriples()
610}
611
612async fn find_effective_acl_dyn(
618 storage: &dyn Storage,
619 resource_path: &str,
620) -> Result<Option<wac::AclDocument>, PodError> {
621 let mut path = resource_path.to_string();
622 loop {
623 let acl_key = if path == "/" {
624 "/.acl".to_string()
625 } else {
626 format!("{}.acl", path.trim_end_matches('/'))
627 };
628 if let Ok((body, meta)) = storage.get(&acl_key).await {
629 match parse_jsonld_acl(&body) {
630 Ok(doc) => return Ok(Some(doc)),
631 Err(PodError::BadRequest(_)) => {
632 return Err(PodError::BadRequest("ACL document exceeds bounds".into()))
633 }
634 Err(_) => {}
635 }
636 let ct = meta.content_type.to_ascii_lowercase();
637 let looks_turtle = ct.starts_with("text/turtle")
638 || ct.starts_with("application/turtle")
639 || ct.starts_with("application/x-turtle");
640 let text = std::str::from_utf8(&body).unwrap_or("");
641 if looks_turtle || text.contains("@prefix") || text.contains("acl:Authorization") {
642 if let Ok(doc) = parse_turtle_acl(text) {
643 return Ok(Some(doc));
644 }
645 }
646 }
647 if path == "/" || path.is_empty() {
648 break;
649 }
650 let trimmed = path.trim_end_matches('/');
651 path = match trimmed.rfind('/') {
652 Some(0) => "/".to_string(),
653 Some(pos) => trimmed[..pos].to_string(),
654 None => "/".to_string(),
655 };
656 }
657 Ok(None)
658}
659
660async fn handle_delete(
661 req: HttpRequest,
662 state: web::Data<AppState>,
663) -> Result<HttpResponse, ActixError> {
664 let path = req.uri().path().to_string();
665 let auth_pk = extract_pubkey(&req).await;
666 let agent = agent_uri(auth_pk.as_ref());
667 enforce_write(&state, &path, AccessMode::Write, agent.as_deref()).await?;
668
669 match state.storage.delete(&path).await {
670 Ok(()) => Ok(HttpResponse::NoContent().finish()),
671 Err(PodError::NotFound(_)) => Ok(HttpResponse::NotFound().finish()),
672 Err(e) => Err(to_actix(e)),
673 }
674}
675
676async fn handle_options(req: HttpRequest) -> Result<HttpResponse, ActixError> {
677 let path = req.uri().path().to_string();
678 let o = ldp::options_for(&path);
679 let mut rsp = HttpResponse::NoContent().finish();
680 if let Ok(v) = header::HeaderValue::from_str(&o.allow.join(", ")) {
681 rsp.headers_mut()
682 .insert(header::HeaderName::from_static("allow"), v);
683 }
684 if let Some(ap) = o.accept_post {
685 if let Ok(v) = header::HeaderValue::from_str(ap) {
686 rsp.headers_mut()
687 .insert(header::HeaderName::from_static("accept-post"), v);
688 }
689 }
690 if let Ok(v) = header::HeaderValue::from_str(o.accept_patch) {
691 rsp.headers_mut()
692 .insert(header::HeaderName::from_static("accept-patch"), v);
693 }
694 if let Ok(v) = header::HeaderValue::from_str(o.accept_ranges) {
695 rsp.headers_mut()
696 .insert(header::HeaderName::from_static("accept-ranges"), v);
697 }
698 Ok(rsp)
699}
700
701async fn handle_well_known_solid(state: web::Data<AppState>) -> HttpResponse {
706 let doc = interop::well_known_solid(&state.nodeinfo.base_url, &state.nodeinfo.base_url);
707 HttpResponse::Ok()
708 .content_type("application/ld+json")
709 .json(doc)
710}
711
712#[derive(Debug, Deserialize)]
713struct WebFingerQuery {
714 resource: Option<String>,
715}
716
717async fn handle_well_known_webfinger(
718 state: web::Data<AppState>,
719 q: web::Query<WebFingerQuery>,
720) -> HttpResponse {
721 let resource = q.resource.clone().unwrap_or_else(|| {
722 format!(
723 "acct:anonymous@{}",
724 state
725 .nodeinfo
726 .base_url
727 .trim_start_matches("http://")
728 .trim_start_matches("https://")
729 )
730 });
731 let webid = format!("{}/profile/card#me", state.nodeinfo.base_url.trim_end_matches('/'));
732 match interop::webfinger_response(&resource, &state.nodeinfo.base_url, &webid) {
733 Some(jrd) => HttpResponse::Ok()
734 .content_type("application/jrd+json")
735 .json(jrd),
736 None => HttpResponse::NotFound().finish(),
737 }
738}
739
740async fn handle_well_known_nodeinfo(state: web::Data<AppState>) -> HttpResponse {
741 let doc = interop::nodeinfo_discovery(&state.nodeinfo.base_url);
742 HttpResponse::Ok()
743 .content_type("application/json")
744 .json(doc)
745}
746
747async fn handle_well_known_nodeinfo_2_1(state: web::Data<AppState>) -> HttpResponse {
748 let doc = interop::nodeinfo_2_1(
749 &state.nodeinfo.software_name,
750 &state.nodeinfo.software_version,
751 state.nodeinfo.open_registrations,
752 state.nodeinfo.total_users,
753 );
754 HttpResponse::Ok()
755 .content_type("application/json")
756 .json(doc)
757}
758
759#[cfg(feature = "did-nostr")]
760async fn handle_well_known_did_nostr(
761 state: web::Data<AppState>,
762 path: web::Path<String>,
763) -> HttpResponse {
764 let pubkey = path.into_inner();
765 let also = vec![format!(
766 "{}/profile/card#me",
767 state.nodeinfo.base_url.trim_end_matches('/')
768 )];
769 let doc = interop::did_nostr::did_nostr_document(&pubkey, &also);
770 HttpResponse::Ok()
771 .content_type("application/did+json")
772 .json(doc)
773}
774
775#[derive(Debug, Deserialize)]
780struct CreateAccountRequest {
781 username: String,
782 #[serde(default)]
783 name: Option<String>,
784}
785
786async fn handle_pod_check(
787 state: web::Data<AppState>,
788 path: web::Path<String>,
789) -> HttpResponse {
790 let pod_name = path.into_inner();
791 let pod_root = format!("/{pod_name}/");
792 match state.storage.exists(&pod_root).await {
793 Ok(true) => HttpResponse::Ok().json(serde_json::json!({"exists": true})),
794 _ => HttpResponse::NotFound().json(serde_json::json!({"exists": false})),
795 }
796}
797
798async fn handle_create_account(
799 state: web::Data<AppState>,
800 body: web::Json<CreateAccountRequest>,
801) -> Result<HttpResponse, ActixError> {
802 let pod_root = format!("/{}/", body.username);
803 if state.storage.exists(&pod_root).await.unwrap_or(false) {
804 return Ok(
805 HttpResponse::Conflict().json(serde_json::json!({"error": "account already exists"})),
806 );
807 }
808
809 let plan = provision::ProvisionPlan {
810 pubkey: body.username.clone(),
811 display_name: body.name.clone(),
812 pod_base: format!(
813 "{}/{}",
814 state.nodeinfo.base_url.trim_end_matches('/'),
815 body.username,
816 ),
817 containers: vec![
818 format!("/{}/", body.username),
819 format!("/{}/profile/", body.username),
820 format!("/{}/inbox/", body.username),
821 format!("/{}/public/", body.username),
822 format!("/{}/private/", body.username),
823 format!("/{}/settings/", body.username),
824 ],
825 root_acl: None,
826 quota_bytes: None,
827 };
828
829 match provision::provision_pod(state.storage.as_ref(), &plan).await {
830 Ok(outcome) => Ok(HttpResponse::Created().json(serde_json::json!({
831 "webid": outcome.webid,
832 "pod_root": outcome.pod_root,
833 "username": body.username,
834 }))),
835 Err(e) => Err(to_actix(e)),
836 }
837}
838
839async fn handle_copy(
844 req: HttpRequest,
845 state: web::Data<AppState>,
846) -> Result<HttpResponse, ActixError> {
847 let dest = req.uri().path().to_string();
848 let auth_pk = extract_pubkey(&req).await;
849 let agent = agent_uri(auth_pk.as_ref());
850 enforce_write(&state, &dest, AccessMode::Write, agent.as_deref()).await?;
851
852 let source = req
853 .headers()
854 .get("source")
855 .and_then(|v| v.to_str().ok())
856 .map(|s| s.to_string());
857 let source = match source {
858 Some(s) => s,
859 None => return Ok(HttpResponse::BadRequest().body("Source header required")),
860 };
861
862 let (body, meta) = match state.storage.get(&source).await {
863 Ok(v) => v,
864 Err(PodError::NotFound(_)) => {
865 return Ok(HttpResponse::NotFound().body("source resource not found"))
866 }
867 Err(e) => return Err(to_actix(e)),
868 };
869
870 state
871 .storage
872 .put(&dest, body, &meta.content_type)
873 .await
874 .map_err(to_actix)?;
875
876 let src_acl = format!("{}.acl", source.trim_end_matches('/'));
878 let dst_acl = format!("{}.acl", dest.trim_end_matches('/'));
879 if let Ok((acl_body, acl_meta)) = state.storage.get(&src_acl).await {
880 let _ = state.storage.put(&dst_acl, acl_body, &acl_meta.content_type).await;
881 }
882
883 let mut rsp = HttpResponse::Created().finish();
884 if let Ok(loc) = header::HeaderValue::from_str(&dest) {
885 rsp.headers_mut().insert(header::LOCATION, loc);
886 }
887 Ok(rsp)
888}
889
890async fn handle_glob_get(
895 req: HttpRequest,
896 state: web::Data<AppState>,
897) -> Result<HttpResponse, ActixError> {
898 let raw_path = req.uri().path().to_string();
899 if !raw_path.ends_with("/*") {
901 return Ok(HttpResponse::NotFound().body("unsupported glob pattern"));
902 }
903 let folder = &raw_path[..raw_path.len() - 1]; let folder = if folder.ends_with('/') {
905 folder.to_string()
906 } else {
907 format!("{folder}/")
908 };
909
910 let children = state.storage.list(&folder).await.map_err(to_actix)?;
911 let mut merged = String::new();
912
913 for child in &children {
914 if child.ends_with('/') {
915 continue;
916 }
917 let child_path = format!("{folder}{child}");
918 if let Ok((body, meta)) = state.storage.get(&child_path).await {
919 if meta.content_type.contains("turtle")
920 || meta.content_type.contains("n-triples")
921 || meta.content_type.contains("n3")
922 {
923 if let Ok(text) = std::str::from_utf8(&body) {
924 merged.push_str(text);
925 merged.push('\n');
926 }
927 }
928 }
929 }
930
931 if merged.is_empty() {
932 return Ok(HttpResponse::NotFound().body("no matching RDF resources"));
933 }
934
935 Ok(HttpResponse::Ok()
936 .content_type("text/turtle")
937 .body(merged))
938}
939
940#[derive(Debug, Deserialize)]
945struct LoginPasswordRequest {
946 username: String,
947 password: String,
948}
949
950async fn handle_login_password(
951 body: web::Json<LoginPasswordRequest>,
952) -> HttpResponse {
953 let _ = (&body.username, &body.password);
954 HttpResponse::Ok().json(serde_json::json!({
955 "message": "login endpoint active"
956 }))
957}
958
959#[derive(Debug, Deserialize)]
960struct PasswordResetRequest {
961 username: String,
962}
963
964async fn handle_password_reset_request(
965 body: web::Json<PasswordResetRequest>,
966) -> HttpResponse {
967 let _ = &body.username;
968 HttpResponse::Ok().json(serde_json::json!({
969 "message": "if an account with that username exists, a reset link has been sent"
970 }))
971}
972
973#[derive(Debug, Deserialize)]
974struct PasswordChangeRequest {
975 token: String,
976 new_password: String,
977}
978
979async fn handle_password_change(
980 body: web::Json<PasswordChangeRequest>,
981) -> HttpResponse {
982 let _ = (&body.token, &body.new_password);
983 HttpResponse::Ok().json(serde_json::json!({
984 "message": "password changed"
985 }))
986}
987
988pub struct PathTraversalGuard;
994
995impl<S, B> Transform<S, ServiceRequest> for PathTraversalGuard
996where
997 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
998 B: 'static,
999{
1000 type Response = ServiceResponse<EitherBody<B, BoxBody>>;
1001 type Error = ActixError;
1002 type InitError = ();
1003 type Transform = PathTraversalGuardMiddleware<S>;
1004 type Future = Ready<Result<Self::Transform, Self::InitError>>;
1005
1006 fn new_transform(&self, service: S) -> Self::Future {
1007 ready(Ok(PathTraversalGuardMiddleware { service }))
1008 }
1009}
1010
1011pub struct PathTraversalGuardMiddleware<S> {
1013 service: S,
1014}
1015
1016impl<S, B> Service<ServiceRequest> for PathTraversalGuardMiddleware<S>
1017where
1018 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1019 B: 'static,
1020{
1021 type Response = ServiceResponse<EitherBody<B, BoxBody>>;
1022 type Error = ActixError;
1023 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
1024
1025 actix_web::dev::forward_ready!(service);
1026
1027 fn call(&self, req: ServiceRequest) -> Self::Future {
1028 let raw = req.path().to_string();
1031 if path_is_traversal(&raw) {
1032 let rsp = HttpResponse::BadRequest().body("invalid path: traversal rejected");
1033 let sr = req.into_response(rsp.map_into_boxed_body());
1034 return Box::pin(async move { Ok(sr.map_into_right_body()) });
1035 }
1036 let fut = self.service.call(req);
1037 Box::pin(async move {
1038 let resp = fut.await?;
1039 Ok(resp.map_into_left_body())
1040 })
1041 }
1042}
1043
1044fn path_is_traversal(path: &str) -> bool {
1045 let once: String = percent_decode_str(path).decode_utf8_lossy().into_owned();
1047 let twice: String = percent_decode_str(&once).decode_utf8_lossy().into_owned();
1048 for seg in once.split('/').chain(twice.split('/')) {
1049 if seg == ".." || seg == "." {
1050 return true;
1051 }
1052 }
1053 if twice.contains("/../") || twice.starts_with("../") || twice.ends_with("/..") {
1056 return true;
1057 }
1058 false
1059}
1060
1061pub struct ErrorLoggingMiddleware;
1077
1078impl<S, B> Transform<S, ServiceRequest> for ErrorLoggingMiddleware
1079where
1080 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1081 B: 'static,
1082{
1083 type Response = ServiceResponse<B>;
1084 type Error = ActixError;
1085 type InitError = ();
1086 type Transform = ErrorLoggingMiddlewareService<S>;
1087 type Future = Ready<Result<Self::Transform, Self::InitError>>;
1088
1089 fn new_transform(&self, service: S) -> Self::Future {
1090 ready(Ok(ErrorLoggingMiddlewareService { service }))
1091 }
1092}
1093
1094pub struct ErrorLoggingMiddlewareService<S> {
1096 service: S,
1097}
1098
1099impl<S, B> Service<ServiceRequest> for ErrorLoggingMiddlewareService<S>
1100where
1101 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1102 B: 'static,
1103{
1104 type Response = ServiceResponse<B>;
1105 type Error = ActixError;
1106 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
1107
1108 actix_web::dev::forward_ready!(service);
1109
1110 fn call(&self, req: ServiceRequest) -> Self::Future {
1111 let method = req.method().as_str().to_string();
1114 let path = req.path().to_string();
1115
1116 let fut = self.service.call(req);
1117 Box::pin(async move {
1118 let response = fut.await?;
1119 let status = response.status();
1120 if status.is_server_error() {
1121 log_5xx(&method, &path, status, response.response().error());
1122 }
1123 Ok(response)
1124 })
1125 }
1126}
1127
1128fn log_5xx(method: &str, path: &str, status: StatusCode, error: Option<&actix_web::Error>) {
1132 let chain = match error {
1136 Some(e) => format_error_chain(e),
1137 None => "<no error attached to response>".to_string(),
1138 };
1139
1140 let backtrace = if std::env::var("RUST_BACKTRACE").ok().as_deref() == Some("1") {
1141 Some(std::backtrace::Backtrace::force_capture().to_string())
1142 } else {
1143 None
1144 };
1145
1146 tracing::error!(
1147 target: "solid_pod_rs_server::http",
1148 method = %method,
1149 path = %path,
1150 status = %status.as_u16(),
1151 error.chain = %chain,
1152 backtrace = backtrace.as_deref().unwrap_or(""),
1153 "5xx response"
1154 );
1155}
1156
1157fn format_error_chain(e: &actix_web::Error) -> String {
1168 let summary = format!("{}", e.as_response_error());
1169 let debug = format!("{e:?}");
1170 if debug == summary || debug.is_empty() {
1171 summary
1172 } else {
1173 format!("{summary} -> {debug}")
1174 }
1175}
1176
1177pub struct DotfileGuard {
1183 allow: Arc<DotfileAllowlist>,
1184}
1185
1186impl DotfileGuard {
1187 pub fn new(allow: Arc<DotfileAllowlist>) -> Self {
1188 Self { allow }
1189 }
1190}
1191
1192impl<S, B> Transform<S, ServiceRequest> for DotfileGuard
1193where
1194 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1195 B: 'static,
1196{
1197 type Response = ServiceResponse<EitherBody<B, BoxBody>>;
1198 type Error = ActixError;
1199 type InitError = ();
1200 type Transform = DotfileGuardMiddleware<S>;
1201 type Future = Ready<Result<Self::Transform, Self::InitError>>;
1202
1203 fn new_transform(&self, service: S) -> Self::Future {
1204 ready(Ok(DotfileGuardMiddleware {
1205 service,
1206 allow: self.allow.clone(),
1207 }))
1208 }
1209}
1210
1211pub struct DotfileGuardMiddleware<S> {
1213 service: S,
1214 allow: Arc<DotfileAllowlist>,
1215}
1216
1217impl<S, B> Service<ServiceRequest> for DotfileGuardMiddleware<S>
1218where
1219 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError> + 'static,
1220 B: 'static,
1221{
1222 type Response = ServiceResponse<EitherBody<B, BoxBody>>;
1223 type Error = ActixError;
1224 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
1225
1226 actix_web::dev::forward_ready!(service);
1227
1228 fn call(&self, req: ServiceRequest) -> Self::Future {
1229 let path = req.path().to_string();
1230 let allow_wellknown = path.starts_with("/.well-known/");
1234 if !allow_wellknown {
1235 let pb = PathBuf::from(&path);
1236 if !self.allow.is_allowed(Path::new(&pb)) {
1237 let rsp = HttpResponse::Forbidden().body("dotfile path denied by allowlist");
1238 let sr = req.into_response(rsp.map_into_boxed_body());
1239 return Box::pin(async move { Ok(sr.map_into_right_body()) });
1240 }
1241 }
1242 let fut = self.service.call(req);
1243 Box::pin(async move {
1244 let resp = fut.await?;
1245 Ok(resp.map_into_left_body())
1246 })
1247 }
1248}
1249
1250pub fn build_app(
1262 state: AppState,
1263) -> App<
1264 impl actix_web::dev::ServiceFactory<
1265 ServiceRequest,
1266 Config = (),
1267 Response = ServiceResponse<
1268 EitherBody<EitherBody<BoxBody>>,
1269 >,
1270 Error = ActixError,
1271 InitError = (),
1272 >,
1273> {
1274 let body_cap = state.body_cap;
1275 let dotfiles = state.dotfiles.clone();
1276
1277 let mut app = App::new()
1278 .app_data(web::Data::new(state.clone()))
1279 .app_data(web::PayloadConfig::new(body_cap))
1280 .wrap(ErrorLoggingMiddleware)
1285 .wrap(NormalizePath::new(TrailingSlash::MergeOnly))
1289 .wrap(PathTraversalGuard)
1290 .wrap(DotfileGuard::new(dotfiles));
1291
1292 app = app
1298 .route(
1299 "/.well-known/solid",
1300 web::get().to(handle_well_known_solid),
1301 )
1302 .route(
1303 "/.well-known/webfinger",
1304 web::get().to(handle_well_known_webfinger),
1305 )
1306 .route(
1307 "/.well-known/nodeinfo",
1308 web::get().to(handle_well_known_nodeinfo),
1309 )
1310 .route(
1311 "/.well-known/nodeinfo/2.1",
1312 web::get().to(handle_well_known_nodeinfo_2_1),
1313 );
1314
1315 #[cfg(feature = "did-nostr")]
1316 {
1317 app = app.route(
1318 "/.well-known/did/nostr/{pubkey}.json",
1319 web::get().to(handle_well_known_did_nostr),
1320 );
1321 }
1322
1323 app = app
1325 .route("/api/accounts/new", web::post().to(handle_create_account))
1326 .route("/pods/check/{name}", web::get().to(handle_pod_check))
1327 .route("/login/password", web::post().to(handle_login_password))
1328 .route("/account/password/reset", web::post().to(handle_password_reset_request))
1329 .route("/account/password/change", web::post().to(handle_password_change));
1330
1331 app.route("/{tail:.*}/", web::post().to(handle_post))
1334 .route("/{tail:.*}/", web::put().to(handle_put))
1335 .route("/{tail:.*}", web::get().to(handle_get))
1336 .route("/{tail:.*}", web::head().to(handle_get))
1337 .route("/{tail:.*}", web::put().to(handle_put))
1338 .route("/{tail:.*}", web::patch().to(handle_patch))
1339 .route("/{tail:.*}", web::delete().to(handle_delete))
1340 .route("/{tail:.*}", web::method(actix_web::http::Method::from_bytes(b"COPY").unwrap()).to(handle_copy))
1341 .route("/{tail:.*}", web::method(actix_web::http::Method::OPTIONS).to(handle_options))
1342}