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
71pub fn bind_addr_from_env() -> std::net::IpAddr {
74 std::env::var("BIND_ADDR")
75 .ok()
76 .and_then(|v| v.parse().ok())
77 .unwrap_or_else(|| std::net::IpAddr::from([127, 0, 0, 1]))
78}
79
80pub fn refuse_open_bind(
86 bind_addr: std::net::IpAddr,
87 auth_token: Option<&str>,
88 allow_no_token: bool,
89) -> Result<()> {
90 let has_token = auth_token.is_some_and(|t| !t.is_empty());
91 if has_token || allow_no_token || bind_addr.to_canonical().is_loopback() {
92 return Ok(());
93 }
94 anyhow::bail!(
95 "refusing to bind {bind_addr}: no bearer token is configured, and a wallet served \
96 beyond loopback without one answers anyone who can reach it. Set AUTH_TOKEN, or pass \
97 --allow-no-token to bind it open deliberately."
98 )
99}
100
101pub fn refuse_open_bind_from_env(allow_no_token: bool) -> Result<()> {
104 let token = std::env::var("AUTH_TOKEN").ok();
105 refuse_open_bind(bind_addr_from_env(), token.as_deref(), allow_no_token)
106}
107
108async fn auth_middleware(headers: HeaderMap, request: Request, next: Next) -> Response {
110 if request.uri().path() == "/arc-callback" {
114 return next.run(request).await;
115 }
116
117 let token = request
119 .extensions()
120 .get::<ServerConfig>()
121 .and_then(|c| c.auth_token.clone());
122
123 if let Some(expected) = token {
124 let provided = headers
125 .get("authorization")
126 .and_then(|v| v.to_str().ok())
127 .and_then(|v| v.strip_prefix("Bearer "));
128
129 match provided {
130 Some(t) if t == expected => {}
131 _ => {
132 return (
133 StatusCode::UNAUTHORIZED,
134 Json(json!({"code": "UNAUTHORIZED", "message": "Invalid or missing bearer token"})),
135 )
136 .into_response();
137 }
138 }
139 }
140
141 next.run(request).await
142}
143
144pub fn make_router(wallet: WalletState, config: ServerConfig) -> Router {
146 let cors = CorsLayer::very_permissive();
147 let cfg = config.clone();
148 let spending_lock: SpendingLock = Arc::new(tokio::sync::Mutex::new(()));
149 let verifier = crate::broadcast_verify::BroadcastVerifier::from_env(config.chain);
153
154 Router::new()
155 .route(
160 "/isAuthenticated",
161 get(handlers::is_authenticated).post(handlers::is_authenticated),
162 )
163 .route("/getPublicKey", post(handlers::get_public_key))
164 .route("/permission-audit", get(permission_audit))
168 .route("/permission-audit/reset", post(permission_audit_reset))
169 .route("/createSignature", post(handlers::create_signature))
170 .route(
174 "/createAction",
175 post(handlers::create_action).layer(DefaultBodyLimit::disable()),
176 )
177 .route(
178 "/internalizeAction",
179 post(handlers::internalize_action).layer(DefaultBodyLimit::disable()),
180 )
181 .route(
183 "/getHeight",
184 get(handlers::get_height).post(handlers::get_height),
185 )
186 .route(
187 "/getNetwork",
188 get(handlers::get_network).post(handlers::get_network),
189 )
190 .route(
191 "/getVersion",
192 get(handlers::get_version).post(handlers::get_version),
193 )
194 .route(
195 "/waitForAuthentication",
196 get(handlers::wait_for_authentication).post(handlers::wait_for_authentication),
197 )
198 .route("/getHeaderForHeight", post(handlers::get_header_for_height))
200 .route("/verifySignature", post(handlers::verify_signature))
202 .route("/encrypt", post(handlers::encrypt))
203 .route("/decrypt", post(handlers::decrypt))
204 .route("/createHmac", post(handlers::create_hmac))
205 .route("/verifyHmac", post(handlers::verify_hmac))
206 .route("/signAction", post(handlers::sign_action))
208 .route("/abortAction", post(handlers::abort_action))
209 .route("/listActions", post(handlers::list_actions))
210 .route("/listOutputs", post(handlers::list_outputs))
211 .route("/relinquishOutput", post(handlers::relinquish_output))
212 .route("/acquireCertificate", post(handlers::acquire_certificate))
214 .route("/listCertificates", post(handlers::list_certificates))
215 .route("/proveCertificate", post(handlers::prove_certificate))
216 .route(
217 "/relinquishCertificate",
218 post(handlers::relinquish_certificate),
219 )
220 .route(
222 "/discoverByIdentityKey",
223 post(handlers::discover_by_identity_key),
224 )
225 .route(
226 "/discoverByAttributes",
227 post(handlers::discover_by_attributes),
228 )
229 .route(
230 "/revealCounterpartyKeyLinkage",
231 post(handlers::reveal_counterparty_key_linkage),
232 )
233 .route(
234 "/revealSpecificKeyLinkage",
235 post(handlers::reveal_specific_key_linkage),
236 )
237 .route("/arc-callback", post(arc_callback))
240 .layer(cors)
244 .layer(middleware::from_fn(lenient_json_body))
245 .layer(middleware::from_fn(auth_middleware))
246 .layer(axum::Extension(cfg))
247 .layer(axum::Extension(spending_lock))
248 .layer(axum::Extension(verifier))
249 .layer(TraceLayer::new_for_http())
250 .layer(DefaultBodyLimit::max(BODY_CAP))
251 .with_state(wallet)
252}
253
254async fn arc_callback(
262 axum::extract::State(wallet): axum::extract::State<WalletState>,
263 request: Request,
264) -> Response {
265 let expected = request
266 .extensions()
267 .get::<ServerConfig>()
268 .and_then(|c| c.callback_token.clone());
269 let Some(expected) = expected else {
270 return (
271 StatusCode::NOT_FOUND,
272 Json(json!({"code": "CALLBACK_DISABLED", "message": "no callback token configured"})),
273 )
274 .into_response();
275 };
276
277 let headers = request.headers().clone();
278 let provided = headers
279 .get("authorization")
280 .and_then(|v| v.to_str().ok())
281 .and_then(|v| v.strip_prefix("Bearer "))
282 .map(str::to_string)
283 .or_else(|| {
284 headers
285 .get("x-callbacktoken")
286 .and_then(|v| v.to_str().ok())
287 .map(str::to_string)
288 });
289
290 if provided.as_deref() != Some(expected.as_str()) {
291 tracing::warn!(
295 has_authorization = headers.contains_key("authorization"),
296 has_x_callbacktoken = headers.contains_key("x-callbacktoken"),
297 "arc-callback: rejected POST with invalid/missing callback token"
298 );
299 return (
300 StatusCode::UNAUTHORIZED,
301 Json(json!({"code": "UNAUTHORIZED", "message": "invalid or missing callback token"})),
302 )
303 .into_response();
304 }
305
306 let bytes = match axum::body::to_bytes(request.into_body(), 1_000_000).await {
307 Ok(b) => b,
308 Err(_) => {
309 return (
310 StatusCode::PAYLOAD_TOO_LARGE,
311 Json(json!({"code": "TOO_LARGE", "message": "payload too large"})),
312 )
313 .into_response();
314 }
315 };
316 let payload: serde_json::Value = match serde_json::from_slice(&bytes) {
317 Ok(v) => v,
318 Err(e) => {
319 return (
320 StatusCode::BAD_REQUEST,
321 Json(json!({"code": "BAD_JSON", "message": e.to_string()})),
322 )
323 .into_response();
324 }
325 };
326
327 match crate::arc_ingest::ingest_arc_payload(wallet.storage(), &payload).await {
328 Ok(action) => (
329 StatusCode::OK,
330 Json(json!({"ok": true, "action": format!("{:?}", action)})),
331 )
332 .into_response(),
333 Err(e) => (
334 StatusCode::BAD_REQUEST,
335 Json(json!({"code": "BAD_PAYLOAD", "message": e.to_string()})),
336 )
337 .into_response(),
338 }
339}
340
341pub fn sanitize_lone_surrogates(input: &[u8]) -> std::borrow::Cow<'_, [u8]> {
349 if !input.windows(2).any(|w| w == b"\\u") {
350 return std::borrow::Cow::Borrowed(input);
351 }
352 let hex4 = |b: &[u8]| -> Option<u32> {
353 if b.len() < 4 {
354 return None;
355 }
356 std::str::from_utf8(&b[..4])
357 .ok()
358 .and_then(|h| u32::from_str_radix(h, 16).ok())
359 };
360 let mut out = Vec::with_capacity(input.len());
361 let mut i = 0;
362 let mut changed = false;
363 while i < input.len() {
364 if input[i] == b'\\' && i + 1 < input.len() && input[i + 1] == b'u' {
365 if let Some(cu) = hex4(&input[i + 2..]) {
366 let is_lead = (0xD800..=0xDBFF).contains(&cu);
367 let is_trail = (0xDC00..=0xDFFF).contains(&cu);
368 if is_lead {
369 let next = &input[i + 6..];
370 let paired = next.len() >= 6
371 && next[0] == b'\\'
372 && next[1] == b'u'
373 && hex4(&next[2..]).is_some_and(|t| (0xDC00..=0xDFFF).contains(&t));
374 if paired {
375 out.extend_from_slice(&input[i..i + 12]);
376 i += 12;
377 continue;
378 }
379 out.extend_from_slice(b"\\ufffd");
380 changed = true;
381 i += 6;
382 continue;
383 }
384 if is_trail {
385 out.extend_from_slice(b"\\ufffd");
386 changed = true;
387 i += 6;
388 continue;
389 }
390 out.extend_from_slice(&input[i..i + 6]);
391 i += 6;
392 continue;
393 }
394 }
395 out.push(input[i]);
396 i += 1;
397 }
398 if changed {
399 std::borrow::Cow::Owned(out)
400 } else {
401 std::borrow::Cow::Borrowed(input)
402 }
403}
404
405const BEEF_DOORS: [&str; 2] = ["/createAction", "/internalizeAction"];
412
413const BODY_CAP: usize = 50 * 1024 * 1024;
415
416async fn lenient_json_body(request: Request, next: Next) -> Response {
419 let is_json = request
420 .headers()
421 .get(axum::http::header::CONTENT_TYPE)
422 .and_then(|v| v.to_str().ok())
423 .is_some_and(|ct| ct.to_ascii_lowercase().contains("json"));
424 if !is_json {
425 return next.run(request).await;
426 }
427 let (parts, body) = request.into_parts();
428 let cap = if BEEF_DOORS.contains(&parts.uri.path()) {
429 usize::MAX
430 } else {
431 BODY_CAP
432 };
433 let bytes = match axum::body::to_bytes(body, cap).await {
434 Ok(b) => b,
435 Err(_) => {
436 return (
437 StatusCode::PAYLOAD_TOO_LARGE,
438 Json(json!({"code": "TOO_LARGE", "message": "payload too large"})),
439 )
440 .into_response();
441 }
442 };
443 let body = match sanitize_lone_surrogates(&bytes) {
444 std::borrow::Cow::Borrowed(_) => axum::body::Body::from(bytes),
445 std::borrow::Cow::Owned(fixed) => {
446 tracing::warn!(
447 path = %parts.uri.path(),
448 "json body carried a lone UTF-16 surrogate escape; substituted U+FFFD"
449 );
450 axum::body::Body::from(fixed)
451 }
452 };
453 next.run(Request::from_parts(parts, body)).await
454}
455
456pub async fn run(wallet: WalletState, port: u16, config: ServerConfig) -> Result<()> {
457 let tls = config.tls.clone();
458 let bind_addr = config.bind_addr;
459 let app = make_router(wallet, config);
460 let addr = SocketAddr::from((bind_addr, port));
461
462 #[cfg(feature = "tls")]
463 if let Some(tls_cfg) = tls {
464 use axum_server::tls_rustls::RustlsConfig;
465 let rustls = RustlsConfig::from_pem_file(&tls_cfg.cert_path, &tls_cfg.key_path).await?;
466 tracing::info!("HTTPS server listening on {addr}");
467 eprintln!("HTTPS server listening on {addr}");
468 axum_server::bind_rustls(addr, rustls)
469 .serve(app.into_make_service())
470 .await?;
471 return Ok(());
472 }
473
474 #[cfg(not(feature = "tls"))]
475 if tls.is_some() {
476 anyhow::bail!("TLS requested but binary was built without `--features tls`");
477 }
478
479 tracing::info!("HTTP server listening on {addr}");
480 eprintln!("HTTP server listening on {addr}");
481
482 let listener = tokio::net::TcpListener::bind(addr).await?;
483 axum::serve(listener, app)
484 .with_graceful_shutdown(shutdown_signal())
485 .await?;
486
487 Ok(())
488}
489
490pub fn make_wallet_state(wallet: Wallet<StorageSqlx, Services>) -> WalletState {
491 Arc::new(wallet)
492}
493
494#[allow(dead_code)] pub fn make_wallet_state_from_arc(wallet: Arc<Wallet<StorageSqlx, Services>>) -> WalletState {
503 wallet
504}
505
506async fn shutdown_signal() {
507 tokio::signal::ctrl_c()
508 .await
509 .expect("failed to listen for ctrl-c");
510 eprintln!("\nShutting down...");
511}
512
513async fn permission_audit() -> Json<serde_json::Value> {
515 let entries = audit::snapshot();
516 Json(json!({ "count": entries.len(), "entries": entries }))
517}
518
519async fn permission_audit_reset() -> Json<serde_json::Value> {
521 audit::reset();
522 Json(json!({ "ok": true }))
523}
524
525#[cfg(test)]
526mod lenient_json_tests {
527 use super::sanitize_lone_surrogates;
528
529 #[test]
530 fn lone_lead_and_lone_trail_become_fffd_and_pairs_survive() {
531 let lead = br#"{"keyID":"ab \ud83d cd"}"#;
532 assert_eq!(
533 &*sanitize_lone_surrogates(lead),
534 br#"{"keyID":"ab \ufffd cd"}"#
535 );
536 let trail = br#"{"keyID":"\udc00"}"#;
537 assert_eq!(&*sanitize_lone_surrogates(trail), br#"{"keyID":"\ufffd"}"#);
538 let pair = br#"{"keyID":"\ud83d\ude00"}"#;
539 assert!(matches!(
540 sanitize_lone_surrogates(pair),
541 std::borrow::Cow::Borrowed(_)
542 ));
543 let plain = br#"{"keyID":"plain \\u0041"}"#;
544 assert!(matches!(
545 sanitize_lone_surrogates(plain),
546 std::borrow::Cow::Borrowed(_)
547 ));
548 let fixed = sanitize_lone_surrogates(lead);
549 let v: serde_json::Value = serde_json::from_slice(&fixed).unwrap();
550 assert_eq!(v["keyID"], "ab \u{fffd} cd");
551 }
552}
553
554#[cfg(test)]
555mod bind_guard_tests {
556 use super::refuse_open_bind;
557 use std::net::IpAddr;
558
559 fn ip(s: &str) -> IpAddr {
560 s.parse().unwrap()
561 }
562
563 #[test]
564 fn beyond_loopback_needs_a_token_or_the_flag() {
565 for open in ["0.0.0.0", "::", "192.168.1.10", "10.0.0.1"] {
566 let err = refuse_open_bind(ip(open), None, false).unwrap_err();
567 assert!(
568 err.to_string().contains("--allow-no-token"),
569 "{open}: {err}"
570 );
571 assert!(
572 refuse_open_bind(ip(open), Some(""), false).is_err(),
573 "{open}"
574 );
575 assert!(
576 refuse_open_bind(ip(open), Some("t"), false).is_ok(),
577 "{open}"
578 );
579 assert!(refuse_open_bind(ip(open), None, true).is_ok(), "{open}");
580 }
581 }
582
583 #[test]
584 fn loopback_is_unchanged() {
585 for lo in ["127.0.0.1", "127.0.0.2", "::1", "::ffff:127.0.0.1"] {
586 assert!(refuse_open_bind(ip(lo), None, false).is_ok(), "{lo}");
587 }
588 }
589}