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(
137 "/createAction",
138 post(handlers::create_action).layer(DefaultBodyLimit::disable()),
139 )
140 .route(
141 "/internalizeAction",
142 post(handlers::internalize_action).layer(DefaultBodyLimit::disable()),
143 )
144 .route(
146 "/getHeight",
147 get(handlers::get_height).post(handlers::get_height),
148 )
149 .route(
150 "/getNetwork",
151 get(handlers::get_network).post(handlers::get_network),
152 )
153 .route(
154 "/getVersion",
155 get(handlers::get_version).post(handlers::get_version),
156 )
157 .route(
158 "/waitForAuthentication",
159 get(handlers::wait_for_authentication).post(handlers::wait_for_authentication),
160 )
161 .route("/getHeaderForHeight", post(handlers::get_header_for_height))
163 .route("/verifySignature", post(handlers::verify_signature))
165 .route("/encrypt", post(handlers::encrypt))
166 .route("/decrypt", post(handlers::decrypt))
167 .route("/createHmac", post(handlers::create_hmac))
168 .route("/verifyHmac", post(handlers::verify_hmac))
169 .route("/signAction", post(handlers::sign_action))
171 .route("/abortAction", post(handlers::abort_action))
172 .route("/listActions", post(handlers::list_actions))
173 .route("/listOutputs", post(handlers::list_outputs))
174 .route("/relinquishOutput", post(handlers::relinquish_output))
175 .route("/acquireCertificate", post(handlers::acquire_certificate))
177 .route("/listCertificates", post(handlers::list_certificates))
178 .route("/proveCertificate", post(handlers::prove_certificate))
179 .route(
180 "/relinquishCertificate",
181 post(handlers::relinquish_certificate),
182 )
183 .route(
185 "/discoverByIdentityKey",
186 post(handlers::discover_by_identity_key),
187 )
188 .route(
189 "/discoverByAttributes",
190 post(handlers::discover_by_attributes),
191 )
192 .route(
193 "/revealCounterpartyKeyLinkage",
194 post(handlers::reveal_counterparty_key_linkage),
195 )
196 .route(
197 "/revealSpecificKeyLinkage",
198 post(handlers::reveal_specific_key_linkage),
199 )
200 .route("/arc-callback", post(arc_callback))
203 .layer(cors)
207 .layer(middleware::from_fn(lenient_json_body))
208 .layer(middleware::from_fn(auth_middleware))
209 .layer(axum::Extension(cfg))
210 .layer(axum::Extension(spending_lock))
211 .layer(axum::Extension(verifier))
212 .layer(TraceLayer::new_for_http())
213 .layer(DefaultBodyLimit::max(BODY_CAP))
214 .with_state(wallet)
215}
216
217async fn arc_callback(
225 axum::extract::State(wallet): axum::extract::State<WalletState>,
226 request: Request,
227) -> Response {
228 let expected = request
229 .extensions()
230 .get::<ServerConfig>()
231 .and_then(|c| c.callback_token.clone());
232 let Some(expected) = expected else {
233 return (
234 StatusCode::NOT_FOUND,
235 Json(json!({"code": "CALLBACK_DISABLED", "message": "no callback token configured"})),
236 )
237 .into_response();
238 };
239
240 let headers = request.headers().clone();
241 let provided = headers
242 .get("authorization")
243 .and_then(|v| v.to_str().ok())
244 .and_then(|v| v.strip_prefix("Bearer "))
245 .map(str::to_string)
246 .or_else(|| {
247 headers
248 .get("x-callbacktoken")
249 .and_then(|v| v.to_str().ok())
250 .map(str::to_string)
251 });
252
253 if provided.as_deref() != Some(expected.as_str()) {
254 tracing::warn!(
258 has_authorization = headers.contains_key("authorization"),
259 has_x_callbacktoken = headers.contains_key("x-callbacktoken"),
260 "arc-callback: rejected POST with invalid/missing callback token"
261 );
262 return (
263 StatusCode::UNAUTHORIZED,
264 Json(json!({"code": "UNAUTHORIZED", "message": "invalid or missing callback token"})),
265 )
266 .into_response();
267 }
268
269 let bytes = match axum::body::to_bytes(request.into_body(), 1_000_000).await {
270 Ok(b) => b,
271 Err(_) => {
272 return (
273 StatusCode::PAYLOAD_TOO_LARGE,
274 Json(json!({"code": "TOO_LARGE", "message": "payload too large"})),
275 )
276 .into_response();
277 }
278 };
279 let payload: serde_json::Value = match serde_json::from_slice(&bytes) {
280 Ok(v) => v,
281 Err(e) => {
282 return (
283 StatusCode::BAD_REQUEST,
284 Json(json!({"code": "BAD_JSON", "message": e.to_string()})),
285 )
286 .into_response();
287 }
288 };
289
290 match crate::arc_ingest::ingest_arc_payload(wallet.storage(), &payload).await {
291 Ok(action) => (
292 StatusCode::OK,
293 Json(json!({"ok": true, "action": format!("{:?}", action)})),
294 )
295 .into_response(),
296 Err(e) => (
297 StatusCode::BAD_REQUEST,
298 Json(json!({"code": "BAD_PAYLOAD", "message": e.to_string()})),
299 )
300 .into_response(),
301 }
302}
303
304pub fn sanitize_lone_surrogates(input: &[u8]) -> std::borrow::Cow<'_, [u8]> {
312 if !input.windows(2).any(|w| w == b"\\u") {
313 return std::borrow::Cow::Borrowed(input);
314 }
315 let hex4 = |b: &[u8]| -> Option<u32> {
316 if b.len() < 4 {
317 return None;
318 }
319 std::str::from_utf8(&b[..4])
320 .ok()
321 .and_then(|h| u32::from_str_radix(h, 16).ok())
322 };
323 let mut out = Vec::with_capacity(input.len());
324 let mut i = 0;
325 let mut changed = false;
326 while i < input.len() {
327 if input[i] == b'\\' && i + 1 < input.len() && input[i + 1] == b'u' {
328 if let Some(cu) = hex4(&input[i + 2..]) {
329 let is_lead = (0xD800..=0xDBFF).contains(&cu);
330 let is_trail = (0xDC00..=0xDFFF).contains(&cu);
331 if is_lead {
332 let next = &input[i + 6..];
333 let paired = next.len() >= 6
334 && next[0] == b'\\'
335 && next[1] == b'u'
336 && hex4(&next[2..]).is_some_and(|t| (0xDC00..=0xDFFF).contains(&t));
337 if paired {
338 out.extend_from_slice(&input[i..i + 12]);
339 i += 12;
340 continue;
341 }
342 out.extend_from_slice(b"\\ufffd");
343 changed = true;
344 i += 6;
345 continue;
346 }
347 if is_trail {
348 out.extend_from_slice(b"\\ufffd");
349 changed = true;
350 i += 6;
351 continue;
352 }
353 out.extend_from_slice(&input[i..i + 6]);
354 i += 6;
355 continue;
356 }
357 }
358 out.push(input[i]);
359 i += 1;
360 }
361 if changed {
362 std::borrow::Cow::Owned(out)
363 } else {
364 std::borrow::Cow::Borrowed(input)
365 }
366}
367
368const BEEF_DOORS: [&str; 2] = ["/createAction", "/internalizeAction"];
375
376const BODY_CAP: usize = 50 * 1024 * 1024;
378
379async fn lenient_json_body(request: Request, next: Next) -> Response {
382 let is_json = request
383 .headers()
384 .get(axum::http::header::CONTENT_TYPE)
385 .and_then(|v| v.to_str().ok())
386 .is_some_and(|ct| ct.to_ascii_lowercase().contains("json"));
387 if !is_json {
388 return next.run(request).await;
389 }
390 let (parts, body) = request.into_parts();
391 let cap = if BEEF_DOORS.contains(&parts.uri.path()) {
392 usize::MAX
393 } else {
394 BODY_CAP
395 };
396 let bytes = match axum::body::to_bytes(body, cap).await {
397 Ok(b) => b,
398 Err(_) => {
399 return (
400 StatusCode::PAYLOAD_TOO_LARGE,
401 Json(json!({"code": "TOO_LARGE", "message": "payload too large"})),
402 )
403 .into_response();
404 }
405 };
406 let body = match sanitize_lone_surrogates(&bytes) {
407 std::borrow::Cow::Borrowed(_) => axum::body::Body::from(bytes),
408 std::borrow::Cow::Owned(fixed) => {
409 tracing::warn!(
410 path = %parts.uri.path(),
411 "json body carried a lone UTF-16 surrogate escape; substituted U+FFFD"
412 );
413 axum::body::Body::from(fixed)
414 }
415 };
416 next.run(Request::from_parts(parts, body)).await
417}
418
419pub async fn run(wallet: WalletState, port: u16, config: ServerConfig) -> Result<()> {
420 let tls = config.tls.clone();
421 let bind_addr = config.bind_addr;
422 let app = make_router(wallet, config);
423 let addr = SocketAddr::from((bind_addr, port));
424
425 #[cfg(feature = "tls")]
426 if let Some(tls_cfg) = tls {
427 use axum_server::tls_rustls::RustlsConfig;
428 let rustls = RustlsConfig::from_pem_file(&tls_cfg.cert_path, &tls_cfg.key_path).await?;
429 tracing::info!("HTTPS server listening on {addr}");
430 eprintln!("HTTPS server listening on {addr}");
431 axum_server::bind_rustls(addr, rustls)
432 .serve(app.into_make_service())
433 .await?;
434 return Ok(());
435 }
436
437 #[cfg(not(feature = "tls"))]
438 if tls.is_some() {
439 anyhow::bail!("TLS requested but binary was built without `--features tls`");
440 }
441
442 tracing::info!("HTTP server listening on {addr}");
443 eprintln!("HTTP server listening on {addr}");
444
445 let listener = tokio::net::TcpListener::bind(addr).await?;
446 axum::serve(listener, app)
447 .with_graceful_shutdown(shutdown_signal())
448 .await?;
449
450 Ok(())
451}
452
453pub fn make_wallet_state(wallet: Wallet<StorageSqlx, Services>) -> WalletState {
454 Arc::new(wallet)
455}
456
457#[allow(dead_code)] pub fn make_wallet_state_from_arc(wallet: Arc<Wallet<StorageSqlx, Services>>) -> WalletState {
466 wallet
467}
468
469async fn shutdown_signal() {
470 tokio::signal::ctrl_c()
471 .await
472 .expect("failed to listen for ctrl-c");
473 eprintln!("\nShutting down...");
474}
475
476async fn permission_audit() -> Json<serde_json::Value> {
478 let entries = audit::snapshot();
479 Json(json!({ "count": entries.len(), "entries": entries }))
480}
481
482async fn permission_audit_reset() -> Json<serde_json::Value> {
484 audit::reset();
485 Json(json!({ "ok": true }))
486}
487
488#[cfg(test)]
489mod lenient_json_tests {
490 use super::sanitize_lone_surrogates;
491
492 #[test]
493 fn lone_lead_and_lone_trail_become_fffd_and_pairs_survive() {
494 let lead = br#"{"keyID":"ab \ud83d cd"}"#;
495 assert_eq!(
496 &*sanitize_lone_surrogates(lead),
497 br#"{"keyID":"ab \ufffd cd"}"#
498 );
499 let trail = br#"{"keyID":"\udc00"}"#;
500 assert_eq!(&*sanitize_lone_surrogates(trail), br#"{"keyID":"\ufffd"}"#);
501 let pair = br#"{"keyID":"\ud83d\ude00"}"#;
502 assert!(matches!(
503 sanitize_lone_surrogates(pair),
504 std::borrow::Cow::Borrowed(_)
505 ));
506 let plain = br#"{"keyID":"plain \\u0041"}"#;
507 assert!(matches!(
508 sanitize_lone_surrogates(plain),
509 std::borrow::Cow::Borrowed(_)
510 ));
511 let fixed = sanitize_lone_surrogates(lead);
512 let v: serde_json::Value = serde_json::from_slice(&fixed).unwrap();
513 assert_eq!(v["keyID"], "ab \u{fffd} cd");
514 }
515}