iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

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 }