qualia_client_core/
qdp_http.rs1use crate::front_door::FrontDoorRecord;
16
17#[derive(Debug, Clone)]
21pub struct HttpReply {
22 pub status: u16,
24 pub content_type: String,
26 pub body: Vec<u8>,
28}
29
30pub fn route(record: &FrontDoorRecord, method: &str, path: &str, accept: &str) -> HttpReply {
40 let path = path.split('?').next().unwrap_or(path);
42
43 if method.eq_ignore_ascii_case("GET") && path == crate::qdp_server::WELL_KNOWN_QDP_PATH {
44 match crate::qdp_server::render_profile(record, accept) {
45 Ok(resp) => HttpReply {
46 status: 200,
47 content_type: resp.content_type,
48 body: resp.body,
49 },
50 Err(err) => HttpReply {
51 status: 500,
52 content_type: "text/plain".to_string(),
53 body: err.into_bytes(),
54 },
55 }
56 } else {
57 HttpReply {
58 status: 404,
59 content_type: "text/plain".to_string(),
60 body: b"not found".to_vec(),
61 }
62 }
63}
64
65#[cfg(not(target_arch = "wasm32"))]
72pub fn serve_blocking(record: FrontDoorRecord, bind_addr: &str) -> Result<(), String> {
73 let server = tiny_http::Server::http(bind_addr).map_err(|e| e.to_string())?;
74
75 for req in server.incoming_requests() {
76 let method = req.method().as_str().to_string();
77 let url = req.url().to_string();
78 let accept = req
80 .headers()
81 .iter()
82 .find(|h| h.field.equiv("Accept"))
83 .map(|h| h.value.as_str().to_string())
84 .unwrap_or_else(|| "*/*".to_string());
85
86 let reply = route(&record, &method, &url, &accept);
87
88 let mut resp = tiny_http::Response::from_data(reply.body).with_status_code(reply.status);
89 if let Ok(header) =
90 tiny_http::Header::from_bytes(b"Content-Type", reply.content_type.as_bytes())
91 {
92 resp = resp.with_header(header);
93 }
94 let _ = req.respond(resp);
95 }
96
97 Ok(())
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::front_door::FrontDoorRecord;
104
105 fn minimal_record() -> FrontDoorRecord {
107 FrontDoorRecord {
108 domain: "a.example".into(),
109 agent_type: crate::domains::AgentType::NaturalPerson,
110 front_door_did: "did:qdp:a".into(),
111 name: None,
112 webid: None,
113 services: vec![],
114 identity_pubkey_hex: None,
115 wireguard_pubkey_hex: None,
116 overlay_addr: None,
117 profile_url: None,
118 }
119 }
120
121 #[test]
122 fn get_well_known_qdp_turtle_is_200() {
123 let rec = minimal_record();
124 let reply = route(&rec, "GET", "/.well-known/QDP", "text/turtle");
125 assert_eq!(reply.status, 200);
126 assert!(
127 reply.content_type.contains("turtle"),
128 "content_type was {:?}",
129 reply.content_type
130 );
131 assert!(!reply.body.is_empty());
132 }
133
134 #[test]
135 fn query_string_is_stripped_before_matching() {
136 let rec = minimal_record();
137 let reply = route(&rec, "GET", "/.well-known/QDP?x=1", "application/ld+json");
138 assert_eq!(reply.status, 200);
139 assert_eq!(reply.content_type, "application/ld+json");
140 assert!(!reply.body.is_empty());
141 }
142
143 #[test]
144 fn method_match_is_case_insensitive() {
145 let rec = minimal_record();
146 let reply = route(&rec, "get", "/.well-known/QDP", "*/*");
147 assert_eq!(reply.status, 200);
148 }
149
150 #[test]
151 fn unknown_path_is_404() {
152 let rec = minimal_record();
153 let reply = route(&rec, "GET", "/other", "*/*");
154 assert_eq!(reply.status, 404);
155 assert_eq!(reply.content_type, "text/plain");
156 assert_eq!(reply.body, b"not found");
157 }
158
159 #[test]
160 fn non_get_method_is_404() {
161 let rec = minimal_record();
162 let reply = route(&rec, "POST", "/.well-known/QDP", "*/*");
163 assert_eq!(reply.status, 404);
164 }
165}