1pub mod audit;
2pub mod broadcast_follow_up;
3pub mod handlers;
4pub mod types;
5
6use anyhow::Result;
7use axum::extract::{DefaultBodyLimit, Request};
8use axum::http::{HeaderMap, StatusCode};
9use axum::middleware::{self, Next};
10use axum::response::{IntoResponse, Response};
11use axum::routing::{get, post};
12use axum::{Json, Router};
13use bsv_wallet_toolbox::{Chain, Services, StorageSqlx, Wallet};
14use serde_json::json;
15use std::net::SocketAddr;
16use std::sync::Arc;
17use tower_http::cors::CorsLayer;
18use tower_http::trace::TraceLayer;
19
20use handlers::WalletState;
21
22pub type SpendingLock = Arc<tokio::sync::Mutex<()>>;
29
30#[derive(Clone)]
32#[allow(dead_code)]
33pub struct TlsConfig {
34 pub cert_path: String,
35 pub key_path: String,
36}
37
38#[derive(Clone)]
40pub struct ServerConfig {
41 pub auth_token: Option<String>,
44 pub tls: Option<TlsConfig>,
46 pub chain: Chain,
49 pub bind_addr: std::net::IpAddr,
53 pub callback_token: Option<String>,
57}
58
59impl Default for ServerConfig {
60 fn default() -> Self {
61 Self {
62 auth_token: None,
63 tls: None,
64 chain: Chain::Main,
65 bind_addr: std::net::IpAddr::from([127, 0, 0, 1]),
66 callback_token: None,
67 }
68 }
69}
70
71async fn auth_middleware(headers: HeaderMap, request: Request, next: Next) -> Response {
73 if request.uri().path() == "/arc-callback" {
77 return next.run(request).await;
78 }
79
80 let token = request
82 .extensions()
83 .get::<ServerConfig>()
84 .and_then(|c| c.auth_token.clone());
85
86 if let Some(expected) = token {
87 let provided = headers
88 .get("authorization")
89 .and_then(|v| v.to_str().ok())
90 .and_then(|v| v.strip_prefix("Bearer "));
91
92 match provided {
93 Some(t) if t == expected => {}
94 _ => {
95 return (
96 StatusCode::UNAUTHORIZED,
97 Json(json!({"code": "UNAUTHORIZED", "message": "Invalid or missing bearer token"})),
98 )
99 .into_response();
100 }
101 }
102 }
103
104 next.run(request).await
105}
106
107pub fn make_router(wallet: WalletState, config: ServerConfig) -> Router {
109 let cors = CorsLayer::very_permissive();
110 let cfg = config.clone();
111 let spending_lock: SpendingLock = Arc::new(tokio::sync::Mutex::new(()));
112 let verifier = crate::broadcast_verify::BroadcastVerifier::from_env(config.chain);
116
117 Router::new()
118 .route(
123 "/isAuthenticated",
124 get(handlers::is_authenticated).post(handlers::is_authenticated),
125 )
126 .route("/getPublicKey", post(handlers::get_public_key))
127 .route("/permission-audit", get(permission_audit))
131 .route("/permission-audit/reset", post(permission_audit_reset))
132 .route("/createSignature", post(handlers::create_signature))
133 .route("/createAction", post(handlers::create_action))
134 .route("/internalizeAction", post(handlers::internalize_action))
135 .route(
137 "/getHeight",
138 get(handlers::get_height).post(handlers::get_height),
139 )
140 .route(
141 "/getNetwork",
142 get(handlers::get_network).post(handlers::get_network),
143 )
144 .route(
145 "/getVersion",
146 get(handlers::get_version).post(handlers::get_version),
147 )
148 .route(
149 "/waitForAuthentication",
150 get(handlers::wait_for_authentication).post(handlers::wait_for_authentication),
151 )
152 .route("/getHeaderForHeight", post(handlers::get_header_for_height))
154 .route("/verifySignature", post(handlers::verify_signature))
156 .route("/encrypt", post(handlers::encrypt))
157 .route("/decrypt", post(handlers::decrypt))
158 .route("/createHmac", post(handlers::create_hmac))
159 .route("/verifyHmac", post(handlers::verify_hmac))
160 .route("/signAction", post(handlers::sign_action))
162 .route("/abortAction", post(handlers::abort_action))
163 .route("/listActions", post(handlers::list_actions))
164 .route("/listOutputs", post(handlers::list_outputs))
165 .route("/relinquishOutput", post(handlers::relinquish_output))
166 .route("/acquireCertificate", post(handlers::acquire_certificate))
168 .route("/listCertificates", post(handlers::list_certificates))
169 .route("/proveCertificate", post(handlers::prove_certificate))
170 .route(
171 "/relinquishCertificate",
172 post(handlers::relinquish_certificate),
173 )
174 .route(
176 "/discoverByIdentityKey",
177 post(handlers::discover_by_identity_key),
178 )
179 .route(
180 "/discoverByAttributes",
181 post(handlers::discover_by_attributes),
182 )
183 .route(
184 "/revealCounterpartyKeyLinkage",
185 post(handlers::reveal_counterparty_key_linkage),
186 )
187 .route(
188 "/revealSpecificKeyLinkage",
189 post(handlers::reveal_specific_key_linkage),
190 )
191 .route("/arc-callback", post(arc_callback))
194 .layer(cors)
196 .layer(middleware::from_fn(auth_middleware))
197 .layer(middleware::from_fn(lenient_json_body))
198 .layer(axum::Extension(cfg))
199 .layer(axum::Extension(spending_lock))
200 .layer(axum::Extension(verifier))
201 .layer(TraceLayer::new_for_http())
202 .layer(DefaultBodyLimit::max(50 * 1024 * 1024))
203 .with_state(wallet)
204}
205
206async fn arc_callback(
214 axum::extract::State(wallet): axum::extract::State<WalletState>,
215 request: Request,
216) -> Response {
217 let expected = request
218 .extensions()
219 .get::<ServerConfig>()
220 .and_then(|c| c.callback_token.clone());
221 let Some(expected) = expected else {
222 return (
223 StatusCode::NOT_FOUND,
224 Json(json!({"code": "CALLBACK_DISABLED", "message": "no callback token configured"})),
225 )
226 .into_response();
227 };
228
229 let headers = request.headers().clone();
230 let provided = headers
231 .get("authorization")
232 .and_then(|v| v.to_str().ok())
233 .and_then(|v| v.strip_prefix("Bearer "))
234 .map(str::to_string)
235 .or_else(|| {
236 headers
237 .get("x-callbacktoken")
238 .and_then(|v| v.to_str().ok())
239 .map(str::to_string)
240 });
241
242 if provided.as_deref() != Some(expected.as_str()) {
243 tracing::warn!(
247 has_authorization = headers.contains_key("authorization"),
248 has_x_callbacktoken = headers.contains_key("x-callbacktoken"),
249 "arc-callback: rejected POST with invalid/missing callback token"
250 );
251 return (
252 StatusCode::UNAUTHORIZED,
253 Json(json!({"code": "UNAUTHORIZED", "message": "invalid or missing callback token"})),
254 )
255 .into_response();
256 }
257
258 let bytes = match axum::body::to_bytes(request.into_body(), 1_000_000).await {
259 Ok(b) => b,
260 Err(_) => {
261 return (
262 StatusCode::PAYLOAD_TOO_LARGE,
263 Json(json!({"code": "TOO_LARGE", "message": "payload too large"})),
264 )
265 .into_response();
266 }
267 };
268 let payload: serde_json::Value = match serde_json::from_slice(&bytes) {
269 Ok(v) => v,
270 Err(e) => {
271 return (
272 StatusCode::BAD_REQUEST,
273 Json(json!({"code": "BAD_JSON", "message": e.to_string()})),
274 )
275 .into_response();
276 }
277 };
278
279 match crate::arc_ingest::ingest_arc_payload(wallet.storage(), &payload).await {
280 Ok(action) => (
281 StatusCode::OK,
282 Json(json!({"ok": true, "action": format!("{:?}", action)})),
283 )
284 .into_response(),
285 Err(e) => (
286 StatusCode::BAD_REQUEST,
287 Json(json!({"code": "BAD_PAYLOAD", "message": e.to_string()})),
288 )
289 .into_response(),
290 }
291}
292
293pub fn sanitize_lone_surrogates(input: &[u8]) -> std::borrow::Cow<'_, [u8]> {
301 if !input.windows(2).any(|w| w == b"\\u") {
302 return std::borrow::Cow::Borrowed(input);
303 }
304 let hex4 = |b: &[u8]| -> Option<u32> {
305 if b.len() < 4 {
306 return None;
307 }
308 std::str::from_utf8(&b[..4])
309 .ok()
310 .and_then(|h| u32::from_str_radix(h, 16).ok())
311 };
312 let mut out = Vec::with_capacity(input.len());
313 let mut i = 0;
314 let mut changed = false;
315 while i < input.len() {
316 if input[i] == b'\\' && i + 1 < input.len() && input[i + 1] == b'u' {
317 if let Some(cu) = hex4(&input[i + 2..]) {
318 let is_lead = (0xD800..=0xDBFF).contains(&cu);
319 let is_trail = (0xDC00..=0xDFFF).contains(&cu);
320 if is_lead {
321 let next = &input[i + 6..];
322 let paired = next.len() >= 6
323 && next[0] == b'\\'
324 && next[1] == b'u'
325 && hex4(&next[2..]).is_some_and(|t| (0xDC00..=0xDFFF).contains(&t));
326 if paired {
327 out.extend_from_slice(&input[i..i + 12]);
328 i += 12;
329 continue;
330 }
331 out.extend_from_slice(b"\\ufffd");
332 changed = true;
333 i += 6;
334 continue;
335 }
336 if is_trail {
337 out.extend_from_slice(b"\\ufffd");
338 changed = true;
339 i += 6;
340 continue;
341 }
342 out.extend_from_slice(&input[i..i + 6]);
343 i += 6;
344 continue;
345 }
346 }
347 out.push(input[i]);
348 i += 1;
349 }
350 if changed {
351 std::borrow::Cow::Owned(out)
352 } else {
353 std::borrow::Cow::Borrowed(input)
354 }
355}
356
357async fn lenient_json_body(request: Request, next: Next) -> Response {
360 let is_json = request
361 .headers()
362 .get(axum::http::header::CONTENT_TYPE)
363 .and_then(|v| v.to_str().ok())
364 .is_some_and(|ct| ct.to_ascii_lowercase().contains("json"));
365 if !is_json {
366 return next.run(request).await;
367 }
368 let (parts, body) = request.into_parts();
369 let bytes = match axum::body::to_bytes(body, 50 * 1024 * 1024).await {
370 Ok(b) => b,
371 Err(_) => {
372 return (
373 StatusCode::PAYLOAD_TOO_LARGE,
374 Json(json!({"code": "TOO_LARGE", "message": "payload too large"})),
375 )
376 .into_response();
377 }
378 };
379 let body = match sanitize_lone_surrogates(&bytes) {
380 std::borrow::Cow::Borrowed(_) => axum::body::Body::from(bytes),
381 std::borrow::Cow::Owned(fixed) => {
382 tracing::warn!(
383 path = %parts.uri.path(),
384 "json body carried a lone UTF-16 surrogate escape; substituted U+FFFD"
385 );
386 axum::body::Body::from(fixed)
387 }
388 };
389 next.run(Request::from_parts(parts, body)).await
390}
391
392pub async fn run(wallet: WalletState, port: u16, config: ServerConfig) -> Result<()> {
393 let tls = config.tls.clone();
394 let bind_addr = config.bind_addr;
395 let app = make_router(wallet, config);
396 let addr = SocketAddr::from((bind_addr, port));
397
398 #[cfg(feature = "tls")]
399 if let Some(tls_cfg) = tls {
400 use axum_server::tls_rustls::RustlsConfig;
401 let rustls = RustlsConfig::from_pem_file(&tls_cfg.cert_path, &tls_cfg.key_path).await?;
402 tracing::info!("HTTPS server listening on {addr}");
403 eprintln!("HTTPS server listening on {addr}");
404 axum_server::bind_rustls(addr, rustls)
405 .serve(app.into_make_service())
406 .await?;
407 return Ok(());
408 }
409
410 #[cfg(not(feature = "tls"))]
411 if tls.is_some() {
412 anyhow::bail!("TLS requested but binary was built without `--features tls`");
413 }
414
415 tracing::info!("HTTP server listening on {addr}");
416 eprintln!("HTTP server listening on {addr}");
417
418 let listener = tokio::net::TcpListener::bind(addr).await?;
419 axum::serve(listener, app)
420 .with_graceful_shutdown(shutdown_signal())
421 .await?;
422
423 Ok(())
424}
425
426pub fn make_wallet_state(wallet: Wallet<StorageSqlx, Services>) -> WalletState {
427 Arc::new(wallet)
428}
429
430#[allow(dead_code)] pub fn make_wallet_state_from_arc(wallet: Arc<Wallet<StorageSqlx, Services>>) -> WalletState {
439 wallet
440}
441
442async fn shutdown_signal() {
443 tokio::signal::ctrl_c()
444 .await
445 .expect("failed to listen for ctrl-c");
446 eprintln!("\nShutting down...");
447}
448
449async fn permission_audit() -> Json<serde_json::Value> {
451 let entries = audit::snapshot();
452 Json(json!({ "count": entries.len(), "entries": entries }))
453}
454
455async fn permission_audit_reset() -> Json<serde_json::Value> {
457 audit::reset();
458 Json(json!({ "ok": true }))
459}
460
461#[cfg(test)]
462mod lenient_json_tests {
463 use super::sanitize_lone_surrogates;
464
465 #[test]
466 fn lone_lead_and_lone_trail_become_fffd_and_pairs_survive() {
467 let lead = br#"{"keyID":"ab \ud83d cd"}"#;
468 assert_eq!(
469 &*sanitize_lone_surrogates(lead),
470 br#"{"keyID":"ab \ufffd cd"}"#
471 );
472 let trail = br#"{"keyID":"\udc00"}"#;
473 assert_eq!(&*sanitize_lone_surrogates(trail), br#"{"keyID":"\ufffd"}"#);
474 let pair = br#"{"keyID":"\ud83d\ude00"}"#;
475 assert!(matches!(
476 sanitize_lone_surrogates(pair),
477 std::borrow::Cow::Borrowed(_)
478 ));
479 let plain = br#"{"keyID":"plain \\u0041"}"#;
480 assert!(matches!(
481 sanitize_lone_surrogates(plain),
482 std::borrow::Cow::Borrowed(_)
483 ));
484 let fixed = sanitize_lone_surrogates(lead);
485 let v: serde_json::Value = serde_json::from_slice(&fixed).unwrap();
486 assert_eq!(v["keyID"], "ab \u{fffd} cd");
487 }
488}