dwn_server/
lib.rs

1//! DWN HTTP server, using [axum](https://github.com/tokio-rs/axum).
2//!
3//! The DWN spec does not define a standard HTTP API, so this server is simply built
4//! to be compatible with the [dwn](https://github.com/unavi-xyz/dwn/tree/main/crates/dwn)
5//! crate.
6
7use std::{
8    net::SocketAddr,
9    str::FromStr,
10    sync::{Arc, LazyLock},
11};
12
13use axum::{
14    Json, Router,
15    extract::{Path, State},
16    http::StatusCode,
17    routing::put,
18};
19use axum_macros::debug_handler;
20use directories::ProjectDirs;
21use dwn::{Dwn, core::message::Message};
22use tokio::net::TcpListener;
23use tracing::{debug, error, info};
24use xdid::core::did::Did;
25
26pub use dwn::core::reply::Reply;
27
28pub static DIRS: LazyLock<ProjectDirs> = LazyLock::new(|| {
29    let dirs = ProjectDirs::from("", "UNAVI", "dwn-server").expect("project dirs");
30    std::fs::create_dir_all(dirs.data_dir()).expect("data dir");
31    dirs
32});
33
34const DB_FILE: &str = "data.db";
35
36pub async fn run_server(addr: SocketAddr) -> anyhow::Result<()> {
37    let path = {
38        let mut dir = DIRS.data_dir().to_path_buf();
39        dir.push(DB_FILE);
40        dir
41    };
42    let store = Arc::new(dwn_native_db::NativeDbStore::new(path)?);
43    let dwn = Dwn::new(store.clone(), store);
44
45    let listener = TcpListener::bind(addr).await?;
46    let router = create_router(dwn);
47
48    info!("DWN server running on {addr}");
49
50    axum::serve(listener, router).await?;
51
52    Ok(())
53}
54
55pub fn create_router(dwn: Dwn) -> Router {
56    Router::new()
57        .route("/{target}", put(handle_put))
58        .with_state(dwn)
59}
60
61#[debug_handler]
62async fn handle_put(
63    Path(mut target): Path<String>,
64    State(dwn): State<Dwn>,
65    Json(msg): Json<Message>,
66) -> Result<Json<serde_json::Value>, StatusCode> {
67    if target.starts_with("did:web:") {
68        // Axum automatically decodes percent-encoded paths.
69        // However, for did:web if a port is included the colon must remain percent-encoded.
70        let (_, rest) = target
71            .split_once("did:web:")
72            .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
73
74        let mut parts = rest.split(':');
75
76        if let Some(first) = parts.next()
77            && let Some(second) = parts.next()
78            && second.len() <= 5
79            && second.chars().all(|c| c.is_numeric())
80        {
81            // Assume second is a port.
82            let parts_vec = parts.collect::<Vec<_>>();
83            let parts_str = if parts_vec.is_empty() {
84                String::new()
85            } else {
86                format!(":{}", parts_vec.join(":"))
87            };
88            target = format!("did:web:{first}%3A{second}{parts_str}");
89        }
90    }
91
92    // debug!("-> PUT {target}");
93
94    let target = Did::from_str(&target).map_err(|e| {
95        debug!("Failed to parse DID: {:?}", e);
96        StatusCode::BAD_REQUEST
97    })?;
98
99    let reply = dwn.process_message(&target, msg).await?;
100
101    let res = serde_json::to_value(reply).map_err(|e| {
102        error!("Error serializing response: {e:?}");
103        StatusCode::INTERNAL_SERVER_ERROR
104    })?;
105
106    // debug!("<- {res}");
107
108    Ok(Json(res))
109}