Microservice to bring 2FA to self hosted PDSes
at feature/admin-rbac 297 lines 8.8 kB view raw
1use serde::de::DeserializeOwned; 2use serde::Serialize; 3use std::fmt; 4 5#[derive(Debug)] 6pub enum AdminProxyError { 7 RequestFailed(String), 8 PdsError { 9 status: u16, 10 error: String, 11 message: String, 12 }, 13 ParseError(String), 14} 15 16impl fmt::Display for AdminProxyError { 17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 18 match self { 19 AdminProxyError::RequestFailed(msg) => write!(f, "Request failed: {msg}"), 20 AdminProxyError::PdsError { 21 status, 22 error, 23 message, 24 } => write!(f, "PDS error ({status}): {error} - {message}"), 25 AdminProxyError::ParseError(msg) => write!(f, "Parse error: {msg}"), 26 } 27 } 28} 29 30/// Make an authenticated GET request to a PDS XRPC endpoint. 31pub async fn admin_xrpc_get<R: DeserializeOwned>( 32 pds_base_url: &str, 33 admin_password: &str, 34 endpoint: &str, 35 query_params: &[(&str, &str)], 36) -> Result<R, AdminProxyError> { 37 let url = format!( 38 "{}/xrpc/{}", 39 pds_base_url.trim_end_matches('/'), 40 endpoint 41 ); 42 let client = reqwest::Client::new(); 43 let resp = client 44 .get(&url) 45 .query(query_params) 46 .basic_auth("admin", Some(admin_password)) 47 .send() 48 .await 49 .map_err(|e| AdminProxyError::RequestFailed(e.to_string()))?; 50 51 if !resp.status().is_success() { 52 let status = resp.status().as_u16(); 53 let body = resp.text().await.unwrap_or_default(); 54 if let Ok(err_json) = serde_json::from_str::<serde_json::Value>(&body) { 55 return Err(AdminProxyError::PdsError { 56 status, 57 error: err_json["error"] 58 .as_str() 59 .unwrap_or("Unknown") 60 .to_string(), 61 message: err_json["message"] 62 .as_str() 63 .unwrap_or(&body) 64 .to_string(), 65 }); 66 } 67 return Err(AdminProxyError::PdsError { 68 status, 69 error: "Unknown".to_string(), 70 message: body, 71 }); 72 } 73 74 resp.json::<R>() 75 .await 76 .map_err(|e| AdminProxyError::ParseError(e.to_string())) 77} 78 79/// Make an authenticated POST request to a PDS XRPC endpoint. 80pub async fn admin_xrpc_post<T: Serialize, R: DeserializeOwned>( 81 pds_base_url: &str, 82 admin_password: &str, 83 endpoint: &str, 84 body: &T, 85) -> Result<R, AdminProxyError> { 86 let url = format!( 87 "{}/xrpc/{}", 88 pds_base_url.trim_end_matches('/'), 89 endpoint 90 ); 91 let client = reqwest::Client::new(); 92 let resp = client 93 .post(&url) 94 .json(body) 95 .basic_auth("admin", Some(admin_password)) 96 .send() 97 .await 98 .map_err(|e| AdminProxyError::RequestFailed(e.to_string()))?; 99 100 if !resp.status().is_success() { 101 let status = resp.status().as_u16(); 102 let body = resp.text().await.unwrap_or_default(); 103 if let Ok(err_json) = serde_json::from_str::<serde_json::Value>(&body) { 104 return Err(AdminProxyError::PdsError { 105 status, 106 error: err_json["error"] 107 .as_str() 108 .unwrap_or("Unknown") 109 .to_string(), 110 message: err_json["message"] 111 .as_str() 112 .unwrap_or(&body) 113 .to_string(), 114 }); 115 } 116 return Err(AdminProxyError::PdsError { 117 status, 118 error: "Unknown".to_string(), 119 message: body, 120 }); 121 } 122 123 resp.json::<R>() 124 .await 125 .map_err(|e| AdminProxyError::ParseError(e.to_string())) 126} 127 128/// Make an authenticated POST request that returns no meaningful body (just success/failure). 129pub async fn admin_xrpc_post_no_response<T: Serialize>( 130 pds_base_url: &str, 131 admin_password: &str, 132 endpoint: &str, 133 body: &T, 134) -> Result<(), AdminProxyError> { 135 let url = format!( 136 "{}/xrpc/{}", 137 pds_base_url.trim_end_matches('/'), 138 endpoint 139 ); 140 let client = reqwest::Client::new(); 141 let resp = client 142 .post(&url) 143 .json(body) 144 .basic_auth("admin", Some(admin_password)) 145 .send() 146 .await 147 .map_err(|e| AdminProxyError::RequestFailed(e.to_string()))?; 148 149 if !resp.status().is_success() { 150 let status = resp.status().as_u16(); 151 let body = resp.text().await.unwrap_or_default(); 152 if let Ok(err_json) = serde_json::from_str::<serde_json::Value>(&body) { 153 return Err(AdminProxyError::PdsError { 154 status, 155 error: err_json["error"] 156 .as_str() 157 .unwrap_or("Unknown") 158 .to_string(), 159 message: err_json["message"] 160 .as_str() 161 .unwrap_or(&body) 162 .to_string(), 163 }); 164 } 165 return Err(AdminProxyError::PdsError { 166 status, 167 error: "Unknown".to_string(), 168 message: body, 169 }); 170 } 171 172 Ok(()) 173} 174 175/// Make an unauthenticated GET request that returns text (e.g., _health). 176pub async fn get_text( 177 pds_base_url: &str, 178 endpoint: &str, 179) -> Result<String, AdminProxyError> { 180 let url = format!( 181 "{}/{}", 182 pds_base_url.trim_end_matches('/'), 183 endpoint 184 ); 185 let client = reqwest::Client::new(); 186 let resp = client 187 .get(&url) 188 .send() 189 .await 190 .map_err(|e| AdminProxyError::RequestFailed(e.to_string()))?; 191 192 if !resp.status().is_success() { 193 let status = resp.status().as_u16(); 194 let body = resp.text().await.unwrap_or_default(); 195 return Err(AdminProxyError::PdsError { 196 status, 197 error: "Unknown".to_string(), 198 message: body, 199 }); 200 } 201 202 resp.text() 203 .await 204 .map_err(|e| AdminProxyError::ParseError(e.to_string())) 205} 206 207/// Make an unauthenticated POST request to an arbitrary base URL (e.g., relay). 208pub async fn public_xrpc_post<T: Serialize>( 209 base_url: &str, 210 endpoint: &str, 211 body: &T, 212) -> Result<(), AdminProxyError> { 213 let url = format!( 214 "{}/xrpc/{}", 215 base_url.trim_end_matches('/'), 216 endpoint 217 ); 218 let client = reqwest::Client::new(); 219 let resp = client 220 .post(&url) 221 .json(body) 222 .send() 223 .await 224 .map_err(|e| AdminProxyError::RequestFailed(e.to_string()))?; 225 226 if !resp.status().is_success() { 227 let status = resp.status().as_u16(); 228 let body = resp.text().await.unwrap_or_default(); 229 if let Ok(err_json) = serde_json::from_str::<serde_json::Value>(&body) { 230 return Err(AdminProxyError::PdsError { 231 status, 232 error: err_json["error"] 233 .as_str() 234 .unwrap_or("Unknown") 235 .to_string(), 236 message: err_json["message"] 237 .as_str() 238 .unwrap_or(&body) 239 .to_string(), 240 }); 241 } 242 return Err(AdminProxyError::PdsError { 243 status, 244 error: "Unknown".to_string(), 245 message: body, 246 }); 247 } 248 249 Ok(()) 250} 251 252/// Make an unauthenticated GET request with JSON response parsing. 253pub async fn public_xrpc_get<R: DeserializeOwned>( 254 pds_base_url: &str, 255 endpoint: &str, 256 query_params: &[(&str, &str)], 257) -> Result<R, AdminProxyError> { 258 let url = format!( 259 "{}/xrpc/{}", 260 pds_base_url.trim_end_matches('/'), 261 endpoint 262 ); 263 let client = reqwest::Client::new(); 264 let resp = client 265 .get(&url) 266 .query(query_params) 267 .send() 268 .await 269 .map_err(|e| AdminProxyError::RequestFailed(e.to_string()))?; 270 271 if !resp.status().is_success() { 272 let status = resp.status().as_u16(); 273 let body = resp.text().await.unwrap_or_default(); 274 if let Ok(err_json) = serde_json::from_str::<serde_json::Value>(&body) { 275 return Err(AdminProxyError::PdsError { 276 status, 277 error: err_json["error"] 278 .as_str() 279 .unwrap_or("Unknown") 280 .to_string(), 281 message: err_json["message"] 282 .as_str() 283 .unwrap_or(&body) 284 .to_string(), 285 }); 286 } 287 return Err(AdminProxyError::PdsError { 288 status, 289 error: "Unknown".to_string(), 290 message: body, 291 }); 292 } 293 294 resp.json::<R>() 295 .await 296 .map_err(|e| AdminProxyError::ParseError(e.to_string())) 297}