From 29834dbe506d7ac84deca1ec05a5d681310f2719 Mon Sep 17 00:00:00 2001 From: danda Date: Thu, 16 May 2024 10:16:48 -0700 Subject: [PATCH] feat: get rid of all javascript implements a generic solution to redirect query strings generated by forms to path based URIs that can be handled by existing routes. This eliminates the need for javascript that was doing the equivalent client-side. --- Cargo.lock | 28 +++++++ Cargo.toml | 1 + src/html/page/mod.rs | 1 + src/html/page/redirect_qs_to_path.rs | 116 +++++++++++++++++++++++++++ src/main.rs | 3 + templates/web/html/page/root.html | 27 ++----- 6 files changed, 156 insertions(+), 20 deletions(-) create mode 100644 src/html/page/redirect_qs_to_path.rs diff --git a/Cargo.lock b/Cargo.lock index 532a728..bd856c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1485,6 +1485,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1823,6 +1833,7 @@ dependencies = [ "tower-http", "tracing", "tracing-subscriber", + "url", ] [[package]] @@ -3377,6 +3388,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -3420,6 +3437,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "utf8parse" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index a34afd7..abb68cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ boilerplate = { version = "1.0.0" } html-escaper = "0.2.0" tower-http = { version = "0.5.2", features = ["fs"] } readonly = "0.2.12" +url = "2.5.0" [patch.crates-io] diff --git a/src/html/page/mod.rs b/src/html/page/mod.rs index 765ec18..9384efe 100644 --- a/src/html/page/mod.rs +++ b/src/html/page/mod.rs @@ -1,4 +1,5 @@ pub mod block; pub mod not_found; +pub mod redirect_qs_to_path; pub mod root; pub mod utxo; diff --git a/src/html/page/redirect_qs_to_path.rs b/src/html/page/redirect_qs_to_path.rs new file mode 100644 index 0000000..f3265b8 --- /dev/null +++ b/src/html/page/redirect_qs_to_path.rs @@ -0,0 +1,116 @@ +use axum::extract::RawQuery; +use axum::extract::State; +use axum::response::Redirect; +use axum::response::Response; +use std::sync::Arc; +// use axum::routing::get; +// use axum::routing::Router; +use super::not_found::not_found_html_response; +use axum::response::IntoResponse; +use std::collections::HashSet; +// use super::root::root; +// use super::utxo::utxo_page; +use crate::model::app_state::AppState; +// use neptune_explorer::model::config::Config; + +/// This converts a query string into a path and redirects browser. +/// +/// Purpose: enable a javascript-free website. +/// +/// Problem being solved: +/// +/// Our axum routes are all paths, however an HTML form submits user input as a query-string. +/// We need to convert that query string into a path. +/// +/// We also want the end-user to see a nice path in the browser url +/// for purposes of copy/paste, etc. +/// +/// Browser form submits: We want: +/// /utxo?utxo=5&l=Submit /utxo/5 +/// /block?height=15&l=Submit /block/height/15 +/// +/// Solution: +/// +/// 1. We submit all browser forms to /rqs with method=get. +/// (note: rqs is short for redirect-query-string) +/// 2. /rqs calls this redirect_query_string_to_path() handler. +/// 3. query-string is transformed to path as follows: +/// a) if _ig key is present, the value is split by ',' +/// to obtain a list of query-string keys to ignore. +/// b. each key/val is converted to: +/// key (if val is empty) +/// key/val (if val is not empty) +/// c) any keys in the _ig list are ignored. +/// d) keys and vals are url encoded +/// e) each resulting /key or /key/val is appended to the path. +/// 4. a 301 redirect to the new path is sent to the browser. +/// +/// An html form might look like: +/// +///
+/// +/// +/// +/// Block height or digest: +/// +/// +///
+/// +/// note that the submit with name "l" is ignored because +/// of _ig=l. We could ignore a list of fields also +/// eg _ig=field1,field2,field3,etc. +/// +/// Order of keys in the query-string (and form) is important. +/// +/// Any keys that are not ignored are translated into a +/// path in the order received. Eg: +/// /rqs?block=&height_or_digest=10 --> /block/height_or_digest/10 +/// /rqs?height_or_digest=10&block= --> /height_or_digest/10/block +/// +/// A future enhancement could be to add an optional field for specifying the +/// path order. That would enable re-ordering of inputs in a form without +/// altering the resulting path. For now, our forms are so simple, that is not +/// needed. +#[axum::debug_handler] +pub async fn redirect_query_string_to_path( + RawQuery(raw_query_option): RawQuery, + State(state): State>, +) -> Result { + let not_found = || not_found_html_response(State(state.clone()), None); + + let raw_query = raw_query_option.ok_or_else(not_found)?; + + // note: we construct a fake-url so we can use Url::query_pairs(). + let fake_url = format!("http://127.0.0.1/?{}", raw_query); + let url = url::Url::parse(&fake_url).map_err(|_| not_found())?; + let query_vars: Vec<(String, _)> = url.query_pairs().into_owned().collect(); + + const IGNORE_QS_VAR: &str = "_ig"; + + let ignore_keys: HashSet<_> = match query_vars.iter().find(|(k, _)| k == IGNORE_QS_VAR) { + Some((_k, v)) => v.split(',').collect(), + None => Default::default(), + }; + + let mut new_path: String = Default::default(); + + for (key, val) in query_vars.iter() { + if key == IGNORE_QS_VAR || ignore_keys.contains(key as &str) { + continue; + } + let parts = match val.is_empty() { + false => format!("/{}/{}", url_encode(key), url_encode(val)), + true => format!("/{}", url_encode(key)), + }; + new_path += &parts; + } + + match new_path.is_empty() { + true => Err(not_found()), + false => Ok(Redirect::permanent(&new_path).into_response()), + } +} + +fn url_encode(s: &str) -> String { + url::form_urlencoded::byte_serialize(s.as_bytes()).collect() +} diff --git a/src/main.rs b/src/main.rs index a1e8b11..da3bde3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use neptune_core::rpc_server::RPCClient; use neptune_explorer::html::page::block::block_page; use neptune_explorer::html::page::block::block_page_with_value; use neptune_explorer::html::page::not_found::not_found_html_fallback; +use neptune_explorer::html::page::redirect_qs_to_path::redirect_query_string_to_path; use neptune_explorer::html::page::root::root; use neptune_explorer::html::page::utxo::utxo_page; use neptune_explorer::model::app_state::AppState; @@ -58,6 +59,8 @@ async fn main() -> Result<(), RpcError> { .route("/block/:selector", get(block_page)) .route("/block/:selector/:value", get(block_page_with_value)) .route("/utxo/:value", get(utxo_page)) + // -- Rewrite query-strings to path -- + .route("/rqs", get(redirect_query_string_to_path)) // -- Static files -- .nest_service( "/css", diff --git a/templates/web/html/page/root.html b/templates/web/html/page/root.html index 213c282..9957ee2 100644 --- a/templates/web/html/page/root.html +++ b/templates/web/html/page/root.html @@ -2,22 +2,6 @@ {{self.state.config.site_name}}: (network: {{self.state.network}}) {{ html_escaper::Trusted(include_str!( concat!(env!("CARGO_MANIFEST_DIR"), "/templates/web/html/components/head.html"))) }} -
@@ -35,7 +19,9 @@ The blockchain tip is at height: {{self.tip_height}} Block Lookup -
+ + + 🛈 Provide a numeric block height or hexadecimal digest identifier to lookup any block in the Neptune blockchain. @@ -44,7 +30,7 @@ The blockchain tip is at height: {{self.tip_height}} Block height or digest: - + Quick Lookup: @@ -56,7 +42,8 @@ Quick Lookup:
UTXO Lookup -
+ + 🛈 An Unspent Transaction Output (UTXO) index can be found in the output of neptune-cli wallet-status. Look for the field: aocl_leaf_index @@ -64,7 +51,7 @@ Quick Lookup: UTXO index: - +