Make using insecure connections opt-in (#1309)

* add insecure url checks

* rename variables

* add feature flags to expose Url properly

* fix test compile error

* fix feature errors

* remove comment

* add url crate and use it for url parsing

* fix compile errors

* satisfy the holy clippy

* fix typos and host loopback

* macro attribute, provide validation function in utils

* fix expected output of ui tests

* remove the success case for --allow-insecure because we cannot establish ws:// connection at the moment.
This commit is contained in:
Tadeo Hepperle
2024-01-09 18:18:23 +01:00
committed by GitHub
parent 5b35a9f849
commit 7f714cbcb9
22 changed files with 562 additions and 413 deletions
Generated
+1
View File
@@ -4365,6 +4365,7 @@ dependencies = [
"tokio-stream",
"tracing",
"tracing-subscriber 0.3.18",
"url",
]
[[package]]
+1
View File
@@ -96,6 +96,7 @@ tracing = "0.1.40"
tracing-wasm = "0.2.1"
tracing-subscriber = "0.3.18"
trybuild = "1.0.86"
url = "2.5.0"
wabt = "0.10.0"
wasm-bindgen-test = "0.3.24"
which = "5.0.0"
+6 -1
View File
@@ -2,7 +2,7 @@
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::utils::FileOrUrl;
use crate::utils::{validate_url_security, FileOrUrl};
use clap::Parser as ClapParser;
use codec::Decode;
use color_eyre::eyre::eyre;
@@ -62,6 +62,9 @@ pub struct Opts {
/// Defaults to `false` (default substitutions are provided).
#[clap(long)]
no_default_substitutions: bool,
/// Allow insecure URLs e.g. URLs starting with ws:// or http:// without SSL encryption
#[clap(long, short)]
allow_insecure: bool,
}
fn derive_for_type_parser(src: &str) -> Result<(String, String), String> {
@@ -89,6 +92,8 @@ fn substitute_type_parser(src: &str) -> Result<(String, String), String> {
}
pub async fn run(opts: Opts, output: &mut impl std::io::Write) -> color_eyre::Result<()> {
validate_url_security(opts.file_or_url.url.as_ref(), opts.allow_insecure)?;
let bytes = opts.file_or_url.fetch().await?;
codegen(
+9
View File
@@ -11,6 +11,8 @@ use std::collections::HashMap;
use subxt_codegen::fetch_metadata::MetadataVersion;
use subxt_metadata::Metadata;
use crate::utils::validate_url_security;
/// Verify metadata compatibility between substrate nodes.
#[derive(Debug, ClapParser)]
pub struct Opts {
@@ -36,9 +38,16 @@ pub struct Opts {
/// Defaults to latest.
#[clap(long = "version", default_value = "latest")]
version: MetadataVersion,
/// Allow insecure URLs e.g. URLs starting with ws:// or http:// without SSL encryption
#[clap(long, short)]
allow_insecure: bool,
}
pub async fn run(opts: Opts, output: &mut impl std::io::Write) -> color_eyre::Result<()> {
for url in opts.nodes.iter() {
validate_url_security(Some(url), opts.allow_insecure)?;
}
match opts.pallet {
Some(pallet) => {
handle_pallet_metadata(opts.nodes.as_slice(), pallet.as_str(), opts.version, output)
+7 -1
View File
@@ -5,7 +5,7 @@ use frame_metadata::RuntimeMetadataPrefixed;
use std::collections::HashMap;
use std::hash::Hash;
use crate::utils::FileOrUrl;
use crate::utils::{validate_url_security, FileOrUrl};
use color_eyre::owo_colors::OwoColorize;
use scale_info::form::PortableForm;
@@ -29,9 +29,15 @@ pub struct Opts {
metadata_or_url_1: FileOrUrl,
/// metadata file or node URL
metadata_or_url_2: FileOrUrl,
/// Allow insecure URLs e.g. URLs starting with ws:// or http:// without SSL encryption
#[clap(long, short)]
allow_insecure: bool,
}
pub async fn run(opts: Opts, output: &mut impl std::io::Write) -> color_eyre::Result<()> {
validate_url_security(opts.metadata_or_url_1.url.as_ref(), opts.allow_insecure)?;
validate_url_security(opts.metadata_or_url_2.url.as_ref(), opts.allow_insecure)?;
let (entry_1_metadata, entry_2_metadata) = get_metadata(&opts).await?;
let metadata_diff = MetadataDiff::construct(&entry_1_metadata, &entry_2_metadata);
+49 -19
View File
@@ -1,4 +1,4 @@
use crate::utils::{print_first_paragraph_with_indent, FileOrUrl};
use crate::utils::{print_first_paragraph_with_indent, validate_url_security, FileOrUrl};
use clap::{Parser as ClapParser, Subcommand};
use std::fmt::Write;
@@ -71,6 +71,9 @@ pub struct Opts {
pallet: Option<String>,
#[command(subcommand)]
pallet_subcommand: Option<PalletSubcommand>,
/// Allow insecure URLs e.g. URLs starting with ws:// or http:// without SSL encryption
#[clap(long, short)]
allow_insecure: bool,
}
#[derive(Debug, Clone, Subcommand)]
@@ -81,6 +84,8 @@ pub enum PalletSubcommand {
}
pub async fn run(opts: Opts, output: &mut impl std::io::Write) -> color_eyre::Result<()> {
validate_url_security(opts.file_or_url.url.as_ref(), opts.allow_insecure)?;
// get the metadata
let bytes = opts.file_or_url.fetch().await?;
let metadata = Metadata::decode(&mut &bytes[..])?;
@@ -160,48 +165,52 @@ fn print_available_pallets(metadata: &Metadata) -> String {
#[cfg(test)]
pub mod tests {
use super::{run, Opts};
use super::Opts;
async fn simulate_run(cli_command: &str) -> color_eyre::Result<String> {
let mut args = vec![
"explore",
"--file=../artifacts/polkadot_metadata_small.scale",
];
async fn run(cli_command: &str) -> color_eyre::Result<String> {
let mut args = vec!["explore"];
let mut split: Vec<&str> = cli_command.split(' ').filter(|e| !e.is_empty()).collect();
args.append(&mut split);
let opts: Opts = clap::Parser::try_parse_from(args)?;
let mut output: Vec<u8> = Vec::new();
run(opts, &mut output)
super::run(opts, &mut output)
.await
.map(|_| String::from_utf8(output).unwrap())
}
async fn run_against_file(cli_command: &str) -> color_eyre::Result<String> {
run(&format!(
"--file=../artifacts/polkadot_metadata_small.scale {cli_command}"
))
.await
}
#[tokio::test]
async fn test_commands() {
// show pallets:
let output = simulate_run("").await;
let output = run_against_file("").await;
assert_eq!(output.unwrap(), "Usage:\n subxt explore <PALLET>\n explore a specific pallet\n\nAvailable <PALLET> values are:\n Balances\n Multisig\n ParaInherent\n System\n Timestamp\n");
// if incorrect pallet, error:
let output = simulate_run("abc123").await;
let output = run_against_file("abc123").await;
assert!(output.is_err());
// if correct pallet, show options (calls, constants, storage)
let output = simulate_run("Balances").await;
let output = run_against_file("Balances").await;
assert_eq!(output.unwrap(), "Usage:\n subxt explore Balances calls\n explore the calls that can be made into this pallet\n subxt explore Balances constants\n explore the constants held in this pallet\n subxt explore Balances storage\n explore the storage values held in this pallet\n");
// check that exploring calls, storage entries and constants is possible:
let output = simulate_run("Balances calls").await;
let output = run_against_file("Balances calls").await;
assert!(output.unwrap().starts_with("Usage:\n subxt explore Balances calls <CALL>\n explore a specific call within this pallet\n\nAvailable <CALL>'s in the \"Balances\" pallet:\n"));
let output = simulate_run("Balances storage").await;
let output = run_against_file("Balances storage").await;
assert!(output.unwrap().starts_with("Usage:\n subxt explore Balances storage <STORAGE_ENTRY>\n view details for a specific storage entry\n\nAvailable <STORAGE_ENTRY>'s in the \"Balances\" pallet:\n"));
let output = simulate_run("Balances constants").await;
let output = run_against_file("Balances constants").await;
assert!(output.unwrap().starts_with("Usage:\n subxt explore Balances constants <CONSTANT>\n explore a specific call within this pallet\n\nAvailable <CONSTANT>'s in the \"Balances\" pallet:\n"));
// check that invalid subcommands don't work:
let output = simulate_run("Balances abc123").await;
let output = run_against_file("Balances abc123").await;
assert!(output.is_err());
// check that we can explore a certain call:
let output = simulate_run("Balances calls transfer_allow_death").await;
let output = run_against_file("Balances calls transfer_allow_death").await;
assert!(output.unwrap().starts_with("Usage:\n subxt explore Balances calls transfer_allow_death <SCALE_VALUE>\n construct the call by providing a valid argument\n\nThe call expect expects a <SCALE_VALUE> with this shape:\n {\n dest: enum MultiAddress"));
// check that unsigned extrinsic can be constructed:
let output = simulate_run(
let output = run_against_file(
"Balances calls transfer_allow_death {\"dest\":v\"Raw\"((255,255, 255)),\"value\":0}",
)
.await;
@@ -210,11 +219,32 @@ pub mod tests {
"Encoded call data:\n 0x24040400020cffffff00\n"
);
// check that we can explore a certain constant:
let output = simulate_run("Balances constants ExistentialDeposit").await;
let output = run_against_file("Balances constants ExistentialDeposit").await;
assert_eq!(output.unwrap(), "Description:\n The minimum amount required to keep an account open. MUST BE GREATER THAN ZERO!\n\nThe constant has the following shape:\n u128\n\nThe value of the constant is:\n 33333333\n");
// check that we can explore a certain storage entry:
let output = simulate_run("System storage Account").await;
let output = run_against_file("System storage Account").await;
assert!(output.unwrap().starts_with("Usage:\n subxt explore System storage Account <KEY_VALUE>\n\nDescription:\n The full account information for a particular account ID."));
// in the future we could also integrate with substrate-testrunner to spawn up a node and send an actual storage query to it: e.g. `subxt explore System storage Digest`
}
#[tokio::test]
async fn insecure_urls_get_denied() {
// Connection should work fine:
run("--url wss://rpc.polkadot.io:443").await.unwrap();
// Errors, because the --allow-insecure is not set:
assert!(run("--url ws://rpc.polkadot.io:443")
.await
.unwrap_err()
.to_string()
.contains("is not secure"));
// This checks, that we never prevent (insecure) requests to localhost, even if the `--allow-insecure` flag is not set.
// It errors, because there is no node running locally, which results in the "Request error".
assert!(run("--url ws://localhost")
.await
.unwrap_err()
.to_string()
.contains("Request error"));
}
}
+5 -1
View File
@@ -2,7 +2,7 @@
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::utils::FileOrUrl;
use crate::utils::{validate_url_security, FileOrUrl};
use clap::Parser as ClapParser;
use codec::{Decode, Encode};
use color_eyre::eyre::{self, bail};
@@ -35,9 +35,13 @@ pub struct Opts {
/// Write the output of the metadata command to the provided file path.
#[clap(long, short, value_parser)]
pub output_file: Option<PathBuf>,
/// Allow insecure URLs e.g. URLs starting with ws:// or http:// without SSL encryption
#[clap(long, short)]
allow_insecure: bool,
}
pub async fn run(opts: Opts, output: &mut impl Write) -> color_eyre::Result<()> {
validate_url_security(opts.file_or_url.url.as_ref(), opts.allow_insecure)?;
let bytes = opts.file_or_url.fetch().await?;
let mut metadata = RuntimeMetadataPrefixed::decode(&mut &bytes[..])?;
+21 -4
View File
@@ -3,7 +3,7 @@
// see LICENSE for license details.
use clap::Args;
use color_eyre::eyre;
use color_eyre::eyre::bail;
use std::str::FromStr;
use std::{fs, io::Read, path::PathBuf};
@@ -87,7 +87,7 @@ impl FileOrUrl {
match (&self.file, &self.url, self.version) {
// Can't provide both --file and --url
(Some(_), Some(_), _) => {
eyre::bail!("specify one of `--url` or `--file` but not both")
bail!("specify one of `--url` or `--file` but not both")
}
// Load from --file path
(Some(PathOrStdIn::Path(path)), None, None) => {
@@ -101,7 +101,7 @@ impl FileOrUrl {
match res {
Ok(bytes) => Ok(bytes),
Err(err) => eyre::bail!("reading bytes from stdin (`--file -`) failed: {err}"),
Err(err) => bail!("reading bytes from stdin (`--file -`) failed: {err}"),
}
}
// Cannot load the metadata from the file and specify a version to fetch.
@@ -110,7 +110,7 @@ impl FileOrUrl {
// but that would be involved because we'd need to convert
// from each metadata to the latest one and from the
// latest one to each metadata version. For now, disable the conversion.
eyre::bail!("`--file` is incompatible with `--version`")
bail!("`--file` is incompatible with `--version`")
}
// Fetch from --url
(None, Some(uri), version) => {
@@ -144,6 +144,23 @@ pub fn with_indent(s: String, indent: usize) -> String {
.join("\n")
}
pub fn validate_url_security(url: Option<&Url>, allow_insecure: bool) -> color_eyre::Result<()> {
let Some(url) = url else {
return Ok(());
};
match subxt::utils::url_is_secure(url.as_str()) {
Ok(is_secure) => {
if !allow_insecure && !is_secure {
bail!("URL {url} is not secure!\nIf you are really want to use this URL, try using --allow-insecure (-a)");
}
}
Err(err) => {
bail!("URL {url} is not valid: {err}")
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::utils::{FileOrUrl, PathOrStdIn};
-1
View File
@@ -15,7 +15,6 @@ use jsonrpsee::{
};
use std::time::Duration;
// Part of the public interface:
pub use jsonrpsee::client_transport::ws::Url;
/// The metadata version that is fetched from the node.
+379 -369
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1,5 +1,4 @@
use anyhow::anyhow;
use futures::StreamExt;
use js_sys::Promise;
use serde::{Deserialize, Serialize};
use serde_json::json;
+8 -5
View File
@@ -32,7 +32,7 @@ struct RuntimeMetadataArgs {
#[darling(default)]
runtime_metadata_path: Option<String>,
#[darling(default)]
runtime_metadata_url: Option<String>,
runtime_metadata_insecure_url: Option<String>,
#[darling(default)]
derive_for_all_types: Option<Punctuated<syn::Path, syn::Token![,]>>,
#[darling(default)]
@@ -146,11 +146,14 @@ pub fn subxt(args: TokenStream, input: TokenStream) -> TokenStream {
// Do we want to fetch unstable metadata? This only works if fetching from a URL.
let unstable_metadata = args.unstable_metadata.is_present();
match (args.runtime_metadata_path, args.runtime_metadata_url) {
match (
args.runtime_metadata_path,
args.runtime_metadata_insecure_url,
) {
(Some(rest_of_path), None) => {
if unstable_metadata {
abort_call_site!(
"The 'unstable_metadata' attribute requires `runtime_metadata_url`"
"The 'unstable_metadata' attribute requires `runtime_metadata_insecure_url`"
)
}
@@ -185,12 +188,12 @@ pub fn subxt(args: TokenStream, input: TokenStream) -> TokenStream {
}
(None, None) => {
abort_call_site!(
"One of 'runtime_metadata_path' or 'runtime_metadata_url' must be provided"
"One of 'runtime_metadata_path' or 'runtime_metadata_insecure_url' must be provided"
)
}
(Some(_), Some(_)) => {
abort_call_site!(
"Only one of 'runtime_metadata_path' or 'runtime_metadata_url' can be provided"
"Only one of 'runtime_metadata_path' or 'runtime_metadata_insecure_url' can be provided"
)
}
}
+3
View File
@@ -94,6 +94,9 @@ subxt-lightclient = { workspace = true, optional = true, default-features = fals
# Light client support:
tokio-stream = { workspace = true, optional = true }
# For parsing urls to disallow insecure schemes
url = { workspace = true }
# Included if "web" feature is enabled, to enable its js feature.
getrandom = { workspace = true, optional = true }
+11
View File
@@ -20,7 +20,18 @@ pub struct RpcClient {
impl RpcClient {
#[cfg(feature = "jsonrpsee")]
/// Create a default RPC client pointed at some URL, currently based on [`jsonrpsee`].
///
/// Errors if an insecure URL is provided. In this case, use [`RpcClient::from_insecure_url`] instead.
pub async fn from_url<U: AsRef<str>>(url: U) -> Result<Self, Error> {
crate::utils::validate_url_is_secure(url.as_ref())?;
RpcClient::from_insecure_url(url).await
}
#[cfg(feature = "jsonrpsee")]
/// Create a default RPC client pointed at some URL, currently based on [`jsonrpsee`].
///
/// Allows insecure URLs without SSL encryption, e.g. (http:// and ws:// URLs).
pub async fn from_insecure_url<U: AsRef<str>>(url: U) -> Result<Self, Error> {
let client = jsonrpsee_helpers::client(url.as_ref())
.await
.map_err(|e| crate::error::RpcError::ClientError(Box::new(e)))?;
+14 -2
View File
@@ -5,6 +5,8 @@
use super::{rpc::LightClientRpc, LightClient, LightClientError};
use crate::backend::rpc::RpcClient;
use crate::client::RawLightClient;
use crate::error::RpcError;
use crate::utils::validate_url_is_secure;
use crate::{config::Config, error::Error, OnlineClient};
use std::num::NonZeroU32;
use subxt_lightclient::{smoldot, AddedChain};
@@ -101,8 +103,19 @@ impl<T: Config> LightClientBuilder<T> {
/// https://docs.rs/wasm-bindgen-futures/latest/wasm_bindgen_futures/fn.future_to_promise.html.
#[cfg(feature = "jsonrpsee")]
pub async fn build_from_url<Url: AsRef<str>>(self, url: Url) -> Result<LightClient<T>, Error> {
let chain_spec = fetch_url(url.as_ref()).await?;
validate_url_is_secure(url.as_ref())?;
self.build_from_insecure_url(url).await
}
/// Build the light client with specified URL to connect to. Allows insecure URLs (no SSL, ws:// or http://).
///
/// For secure connections only, please use [`crate::LightClientBuilder::build_from_url`].
#[cfg(feature = "jsonrpsee")]
pub async fn build_from_insecure_url<Url: AsRef<str>>(
self,
url: Url,
) -> Result<LightClient<T>, Error> {
let chain_spec = fetch_url(url.as_ref()).await?;
self.build_client(chain_spec).await
}
@@ -235,7 +248,6 @@ async fn build_client_from_rpc<T: Config>(
#[cfg(feature = "jsonrpsee")]
async fn fetch_url(url: impl AsRef<str>) -> Result<serde_json::Value, Error> {
use jsonrpsee::core::client::ClientT;
let client = jsonrpsee_helpers::client(url.as_ref()).await?;
client
+9 -1
View File
@@ -66,7 +66,15 @@ impl<T: Config> OnlineClient<T> {
/// Construct a new [`OnlineClient`], providing a URL to connect to.
pub async fn from_url(url: impl AsRef<str>) -> Result<OnlineClient<T>, Error> {
let client = RpcClient::from_url(url).await?;
crate::utils::validate_url_is_secure(url.as_ref())?;
OnlineClient::from_insecure_url(url).await
}
/// Construct a new [`OnlineClient`], providing a URL to connect to.
///
/// Allows insecure URLs without SSL encryption, e.g. (http:// and ws:// URLs).
pub async fn from_insecure_url(url: impl AsRef<str>) -> Result<OnlineClient<T>, Error> {
let client = RpcClient::from_insecure_url(url).await?;
let backend = LegacyBackend::new(client);
OnlineClient::from_backend(Arc::new(backend)).await
}
+3
View File
@@ -115,6 +115,9 @@ pub enum RpcError {
/// The RPC subscription dropped.
#[error("RPC error: subscription dropped.")]
SubscriptionDropped,
/// The requested URL is insecure.
#[error("RPC error: insecure URL: {0}")]
InsecureUrl(String),
}
impl RpcError {
+4 -4
View File
@@ -220,7 +220,7 @@ pub mod ext {
/// mod polkadot {}
/// ```
///
/// ## `runtime_metadata_url = "..."`
/// ## `runtime_metadata_insecure_url = "..."`
///
/// This attribute can be used instead of `runtime_metadata_path` and will tell the macro to download metadata from a node running
/// at the provided URL, rather than a node running locally. This can be useful in CI, but is **not recommended** in production code,
@@ -228,7 +228,7 @@ pub mod ext {
///
/// ```rust,ignore
/// #[subxt::subxt(
/// runtime_metadata_url = "wss://rpc.polkadot.io:443"
/// runtime_metadata_insecure_url = "wss://rpc.polkadot.io:443"
/// )]
/// mod polkadot {}
/// ```
@@ -281,14 +281,14 @@ pub mod ext {
///
/// ## `unstable_metadata`
///
/// This attribute works only in combination with `runtime_metadata_url`. By default, the macro will fetch the latest stable
/// This attribute works only in combination with `runtime_metadata_insecure_url`. By default, the macro will fetch the latest stable
/// version of the metadata from the target node. This attribute makes the codegen attempt to fetch the unstable version of
/// the metadata first. This is **not recommended** in production code, since the unstable metadata a node is providing is likely
/// to be incompatible with Subxt.
///
/// ```rust,ignore
/// #[subxt::subxt(
/// runtime_metadata_url = "wss://rpc.polkadot.io:443",
/// runtime_metadata_insecure_url = "wss://rpc.polkadot.io:443",
/// unstable_metadata
/// )]
/// mod polkadot {}
+28
View File
@@ -13,8 +13,11 @@ mod static_type;
mod unchecked_extrinsic;
mod wrapper_opaque;
use crate::error::RpcError;
use crate::Error;
use codec::{Compact, Decode, Encode};
use derivative::Derivative;
use url::Url;
pub use account_id::AccountId32;
pub use era::Era;
@@ -47,6 +50,31 @@ pub(crate) fn strip_compact_prefix(bytes: &[u8]) -> Result<(u64, &[u8]), codec::
Ok((val.0, *cursor))
}
/// A URL is considered secure if it uses a secure scheme ("https" or "wss") or is referring to localhost.
///
/// Returns an error if the the string could not be parsed into a URL.
pub fn url_is_secure(url: &str) -> Result<bool, Error> {
let url = Url::parse(url).map_err(|e| Error::Rpc(RpcError::ClientError(Box::new(e))))?;
let secure_scheme = url.scheme() == "https" || url.scheme() == "wss";
let is_localhost = url.host().is_some_and(|e| match e {
url::Host::Domain(e) => e == "localhost",
url::Host::Ipv4(e) => e.is_loopback(),
url::Host::Ipv6(e) => e.is_loopback(),
});
Ok(secure_scheme || is_localhost)
}
/// Validates, that the given Url is secure ("https" or "wss" scheme) or is referring to localhost.
pub fn validate_url_is_secure(url: &str) -> Result<(), Error> {
if !url_is_secure(url)? {
Err(Error::Rpc(crate::error::RpcError::InsecureUrl(url.into())))
} else {
Ok(())
}
}
/// A version of [`std::marker::PhantomData`] that is also Send and Sync (which is fine
/// because regardless of the generic param, it is always possible to Send + Sync this
/// 0 size type).
@@ -1,4 +1,4 @@
error: One of 'runtime_metadata_path' or 'runtime_metadata_url' must be provided
error: One of 'runtime_metadata_path' or 'runtime_metadata_insecure_url' must be provided
--> src/incorrect/need_url_or_path.rs:1:1
|
1 | #[subxt::subxt()]
@@ -1,6 +1,6 @@
#[subxt::subxt(
runtime_metadata_path = "../../../../artifacts/polkadot_metadata_tiny.scale",
runtime_metadata_url = "wss://rpc.polkadot.io:443"
runtime_metadata_insecure_url = "wss://rpc.polkadot.io:443"
)]
pub mod node_runtime {}
@@ -1,9 +1,9 @@
error: Only one of 'runtime_metadata_path' or 'runtime_metadata_url' can be provided
error: Only one of 'runtime_metadata_path' or 'runtime_metadata_insecure_url' can be provided
--> src/incorrect/url_and_path_provided.rs:1:1
|
1 | / #[subxt::subxt(
2 | | runtime_metadata_path = "../../../../artifacts/polkadot_metadata_tiny.scale",
3 | | runtime_metadata_url = "wss://rpc.polkadot.io:443"
3 | | runtime_metadata_insecure_url = "wss://rpc.polkadot.io:443"
4 | | )]
| |__^
|