http.rs (10074B)
1 use std::{ 2 collections::BTreeMap, 3 net::SocketAddr, 4 sync::Arc, 5 time::{Instant, SystemTime, UNIX_EPOCH}, 6 }; 7 8 use anyhow::{Context, Result}; 9 use axum::{ 10 Form, Json, Router, 11 body::Body, 12 extract::State, 13 http::Request, 14 middleware, 15 middleware::Next, 16 response::Response, 17 routing::{get, post}, 18 }; 19 use tokio::{net::TcpListener, sync::Mutex}; 20 21 use crate::{ 22 adapters::{config_store, config_store::UiConfig, p2p::GossipNetwork}, 23 app::{SharedNode, SharedPeerBook}, 24 ip_geolocation::IpGeolocation, 25 }; 26 27 mod actions; 28 mod api; 29 mod auth; 30 mod auth_routes; 31 mod consolidation; 32 mod index_html; 33 mod metrics; 34 mod quantum_migration; 35 mod request_auth; 36 mod state; 37 mod static_assets; 38 pub(crate) mod ui; 39 mod wallet; 40 use actions::{ 41 api_address_book_delete_form, api_address_book_form, api_burn_fee_estimate_form, 42 api_burn_per_block_form, api_chain_reset_form, api_metrics_settings_form, 43 api_mine_fee_estimate_form, api_p2p_announce_form, api_p2p_inbound_form, api_peer_delete_form, 44 api_peer_form, api_pow_mining_form, api_recovery_vdf_settings_form, api_stratum_settings_form, 45 api_transfer_fee_estimate_form, api_transfer_form, api_wallet_endpoint_settings_form, 46 api_wallet_generate_form, api_wallet_import_form, apply_config_form, burn_per_block_form, 47 peer_form, transfer_form, 48 }; 49 use api::{ 50 api_blocks, api_config, api_mempool, api_metrics, api_network_health, api_p2p_metrics, 51 api_peers, api_status, api_wallet_selectable_utxos, api_wallet_transactions, api_wallet_utxos, 52 }; 53 use auth_routes::{ 54 api_auth_change_password_form, api_auth_login_form, api_auth_logout_form, api_auth_setup_form, 55 api_auth_status, require_auth_middleware, 56 }; 57 use index_html::INDEX_HTML; 58 use metrics::{metrics_response, network_health, top_mine_proofs}; 59 use request_auth::wallet_password_for_request; 60 pub use state::ServeOptions; 61 use state::{AuthClientKey, AuthSession, HttpState}; 62 use static_assets::{alpine_js, app_js, favicon, index}; 63 use ui::{ 64 add_pending_outputs, add_pending_v2_outputs, populate_wallet_reward_flow, 65 transaction_v2_input_outpoints, ui_blocks_from_indexes, ui_transaction, ui_transaction_v2, 66 wallet_transaction_rows, wallet_transaction_v2_row, wallet_transaction_v2_rows, 67 }; 68 use wallet::{ 69 api_wallet_setup, estimate_burn_fee, estimate_mine_fee, estimate_transfer_fee, 70 fee_estimate_json, import_setup_wallet_seed, replace_setup_wallet_with_generated_seed, 71 required_fee_per_byte_burn, setup_requires_peer, transfer, wallet_setup_json, 72 }; 73 74 pub fn validate_management_password(password: &str) -> Result<()> { 75 auth::validate_password(password) 76 } 77 78 pub fn hash_management_password(password: &str) -> Result<String> { 79 auth::hash_password(password) 80 } 81 82 pub fn verify_management_password(password: &str, encoded: &str) -> Result<bool> { 83 auth::verify_password(password, encoded) 84 } 85 86 const EXPLORER_LIMIT: usize = 50; 87 const EXPLORER_PAGE_LIMIT: usize = 20; 88 const DATASET_LIMIT: usize = 1_000; 89 const DATASET_PAGE_LIMIT: usize = 25; 90 const AUTH_COOKIE_NAME: &str = "iuna_session"; 91 const SETUP_COOKIE_NAME: &str = "iuna_setup"; 92 const SETUP_COOKIE_TTL_SECS: u64 = 10 * 60; 93 const AUTH_SESSION_TTL_MS: u64 = 12 * 60 * 60 * 1_000; 94 const AUTH_MAX_FAILED_ATTEMPTS: u32 = 5; 95 const AUTH_LOCKOUT_MS: u64 = 60 * 1_000; 96 const UNKNOWN_CLIENT_KEY: &str = "unknown"; 97 const PEER_STALE_AFTER_MS: u64 = 20 * 60 * 1_000; 98 const SLOW_UI_REQUEST_LOG_MS: u128 = 250; 99 100 pub(crate) mod types; 101 use types::{ 102 ActionResponse, AuthForm, AuthStatusResponse, BlocksQuery, ChangePasswordForm, ConfigForm, 103 ConfigResponse, MempoolCounts, MetricsQuery, MetricsResponse, NetworkHealthLocalState, 104 NetworkHealthResponse, Page, PageQuery, PeerPresentation, UiBlock, UiTransaction, 105 WalletTransactionContext, WalletTransactionFilters, WalletTransactionRow, 106 WalletTransactionsQuery, WalletUtxoRow, 107 }; 108 109 pub async fn serve( 110 node: SharedNode, 111 peers: SharedPeerBook, 112 gossip: GossipNetwork, 113 ui_config: Arc<Mutex<UiConfig>>, 114 options: ServeOptions, 115 ) -> Result<()> { 116 // Validate the embedded prefix index before the UI can issue its first peer request. 117 IpGeolocation::bundled(); 118 let addr = options.addr; 119 let setup_capability = auth::random_hex(32)?; 120 let state = HttpState { 121 node, 122 peers, 123 gossip, 124 ui_config, 125 config_path: options.config_path, 126 chain_store: options.chain_store, 127 ui_data_store: options.ui_data_store, 128 wallet_path: options.wallet_path, 129 stratum: options.stratum, 130 auth_sessions: Arc::new(Mutex::new(BTreeMap::new())), 131 auth_backoff: Arc::new(Mutex::new(BTreeMap::new())), 132 setup_capability: Arc::new(Mutex::new(Some(setup_capability))), 133 management_port: addr.port(), 134 wallet_endpoint_addr: options.wallet_endpoint_addr, 135 }; 136 let app = Router::new() 137 .route("/", get(index)) 138 .route("/favicon.ico", get(favicon)) 139 .route("/assets/alpine.min.js", get(alpine_js)) 140 .route("/assets/iuna-ui.js", get(app_js)) 141 .route("/api/auth/status", get(api_auth_status)) 142 .route("/api/auth/setup", post(api_auth_setup_form)) 143 .route("/api/auth/login", post(api_auth_login_form)) 144 .route("/api/auth/logout", post(api_auth_logout_form)) 145 .route( 146 "/api/auth/change-password", 147 post(api_auth_change_password_form), 148 ) 149 .route("/api/status", get(api_status)) 150 .route("/api/blocks", get(api_blocks)) 151 .route("/api/config", get(api_config).post(api_config_form)) 152 .route( 153 "/api/address-book", 154 post(api_address_book_form).delete(api_address_book_delete_form), 155 ) 156 .route("/api/wallet/setup", get(api_wallet_setup)) 157 .route("/api/wallet/generate", post(api_wallet_generate_form)) 158 .route("/api/wallet/import", post(api_wallet_import_form)) 159 .route("/api/wallet/transactions", get(api_wallet_transactions)) 160 .route( 161 "/api/wallet/utxos/selectable", 162 get(api_wallet_selectable_utxos), 163 ) 164 .route("/api/wallet/utxos", get(api_wallet_utxos)) 165 .route( 166 "/api/fee-estimate/transfer", 167 post(api_transfer_fee_estimate_form), 168 ) 169 .route("/api/fee-estimate/burn", post(api_burn_fee_estimate_form)) 170 .route("/api/fee-estimate/mine", post(api_mine_fee_estimate_form)) 171 .route("/api/mempool", get(api_mempool)) 172 .route("/api/metrics", get(api_metrics)) 173 .route("/api/network/health", get(api_network_health)) 174 .route( 175 "/api/peers", 176 get(api_peers) 177 .post(api_peer_form) 178 .delete(api_peer_delete_form), 179 ) 180 .route("/api/p2p/metrics", get(api_p2p_metrics)) 181 .route( 182 "/api/settings/burn-per-block", 183 post(api_burn_per_block_form), 184 ) 185 .route("/api/settings/pow-mining", post(api_pow_mining_form)) 186 .route("/api/settings/metrics", post(api_metrics_settings_form)) 187 .route( 188 "/api/settings/recovery-vdf", 189 post(api_recovery_vdf_settings_form), 190 ) 191 .route("/api/settings/chain-reset", post(api_chain_reset_form)) 192 .route("/api/settings/p2p-inbound", post(api_p2p_inbound_form)) 193 .route("/api/settings/p2p-announce", post(api_p2p_announce_form)) 194 .route("/api/settings/stratum", post(api_stratum_settings_form)) 195 .route( 196 "/api/settings/wallet-endpoint", 197 post(api_wallet_endpoint_settings_form), 198 ) 199 .route("/api/transfer", post(api_transfer_form)) 200 .route("/api/wallet/optimize/preview", post(consolidation::preview)) 201 .route("/api/wallet/optimize/submit", post(consolidation::submit)) 202 .route( 203 "/api/wallet/quantum-migration/preview", 204 post(quantum_migration::preview), 205 ) 206 .route( 207 "/api/wallet/quantum-migration/submit", 208 post(quantum_migration::submit), 209 ) 210 .route("/settings/burn-per-block", post(burn_per_block_form)) 211 .route("/transfer", post(transfer_form)) 212 .route("/peers", post(peer_form)) 213 .layer(middleware::from_fn_with_state( 214 state.clone(), 215 require_auth_middleware, 216 )) 217 .layer(middleware::from_fn(log_slow_api_request)) 218 .with_state(state); 219 220 let listener = TcpListener::bind(addr) 221 .await 222 .with_context(|| format!("binding HTTP management UI on {addr}"))?; 223 axum::serve( 224 listener, 225 app.into_make_service_with_connect_info::<SocketAddr>(), 226 ) 227 .await 228 .context("serving HTTP management UI") 229 } 230 231 async fn log_slow_api_request(request: Request<Body>, next: Next) -> Response { 232 let method = request.method().clone(); 233 let path = request.uri().path().to_string(); 234 let started = Instant::now(); 235 let response = next.run(request).await; 236 let elapsed = started.elapsed(); 237 if path.starts_with("/api/") && elapsed.as_millis() >= SLOW_UI_REQUEST_LOG_MS { 238 println!( 239 "slow UI API request: {method} {path} -> {} in {:.2}s", 240 response.status().as_u16(), 241 elapsed.as_secs_f64() 242 ); 243 } 244 response 245 } 246 247 async fn api_config_form( 248 State(state): State<HttpState>, 249 Form(form): Form<ConfigForm>, 250 ) -> Json<ActionResponse> { 251 action_json(apply_config_form(&state, form).await) 252 } 253 254 fn action_json(result: Result<()>) -> Json<ActionResponse> { 255 match result { 256 Ok(_) => Json(ActionResponse { 257 ok: true, 258 error: None, 259 }), 260 Err(error) => Json(ActionResponse { 261 ok: false, 262 error: Some(format!("{error:#}")), 263 }), 264 } 265 } 266 267 fn api_error(error: anyhow::Error) -> Json<ActionResponse> { 268 Json(ActionResponse { 269 ok: false, 270 error: Some(format!("{error:#}")), 271 }) 272 } 273 274 fn now_ms() -> u64 { 275 SystemTime::now() 276 .duration_since(UNIX_EPOCH) 277 .unwrap_or_default() 278 .as_millis() as u64 279 }