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.
This commit is contained in:
danda 2024-05-16 10:16:48 -07:00
parent 22f06180ae
commit 29834dbe50
6 changed files with 156 additions and 20 deletions

28
Cargo.lock generated
View File

@ -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"

View File

@ -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]

View File

@ -1,4 +1,5 @@
pub mod block;
pub mod not_found;
pub mod redirect_qs_to_path;
pub mod root;
pub mod utxo;

View File

@ -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:
///
/// <form action="/rqs" method="get">
/// <input type="hidden" name="block" value=""/>
/// <input type="hidden" name="_ig" value="l"/>
///
/// Block height or digest:
/// <input type="text" size="80" name="height_or_digest"/>
/// <input type="submit" name="l" value="Lookup Block"/>
/// </form>
///
/// 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<Arc<AppState>>,
) -> Result<Response, Response> {
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()
}

View File

@ -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",

View File

@ -2,22 +2,6 @@
<head>
<title>{{self.state.config.site_name}}: (network: {{self.state.network}})</title>
{{ html_escaper::Trusted(include_str!( concat!(env!("CARGO_MANIFEST_DIR"), "/templates/web/html/components/head.html"))) }}
<script>
function handle_submit(form) {
let value = form.height_or_digest.value;
var is_digest = value.length == 80;
var type = is_digest ? "digest" : "height";
var uri = form.action + "/" + type + "/" + value;
window.location.href = uri;
return false;
}
function handle_utxo_submit(form) {
let value = form.utxo.value;
var uri = form.action + "/" + value;
window.location.href = uri;
return false;
}
</script>
</head>
<body>
<header class="container">
@ -35,7 +19,9 @@ The blockchain tip is at height: {{self.tip_height}}
<summary>
Block Lookup
</summary>
<form action="/block" method="get" onsubmit="return handle_submit(this)">
<form action="/rqs" method="get">
<input type="hidden" name="block" value="" />
<input type="hidden" name="_ig" value="l"/>
<span class="tooltip">🛈
<span class="tooltiptext">
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:
<input type="text" size="80" name="height_or_digest" class="mono"/>
<input type="submit" name="height" value="Lookup Block"/>
<input type="submit" name="l" value="Lookup Block"/>
</form>
Quick Lookup:
@ -56,7 +42,8 @@ Quick Lookup:
<article>
<details open>
<summary>UTXO Lookup</summary>
<form action="/utxo" method="get" onsubmit="return handle_utxo_submit(this)">
<form action="/rqs" method="get">
<input type="hidden" name="_ig" value="l" />
<span class="tooltip">🛈
<span class="tooltiptext">
An Unspent Transaction Output (UTXO) index can be found in the output of <i>neptune-cli wallet-status</i>. Look for the field: <b>aocl_leaf_index</b>
@ -64,7 +51,7 @@ Quick Lookup:
</span>
UTXO index:
<input type="text" size="10" name="utxo" />
<input type="submit" name="height" value="Lookup Utxo" />
<input type="submit" name="l" value="Lookup Utxo" />
</form>
</details>
</article>