refactor(app-proxy): remove the stable time freeze (#2856)

This commit is contained in:
Hampus
2026-09-20 04:02:47 +02:00
committed by GitHub
parent 86043212f2
commit 3256af8d92
17 changed files with 11 additions and 1469 deletions
@@ -71,7 +71,6 @@ jobs:
BUILD_VERSION: ${{ needs.meta.outputs.build_version }} BUILD_VERSION: ${{ needs.meta.outputs.build_version }}
PUBLIC_ASSET_BASE_URL: "" PUBLIC_ASSET_BASE_URL: ""
BUNDLE_LOCAL_ASSETS: "true" BUNDLE_LOCAL_ASSETS: "true"
FLUXER_APP_PROXY_TIME_FREEZE_ENABLED: "false"
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
env: env:
@@ -155,7 +154,6 @@ jobs:
BUILD_VERSION=${{ needs.meta.outputs.build_version }} BUILD_VERSION=${{ needs.meta.outputs.build_version }}
SOURCE_SHA=${{ github.sha }} SOURCE_SHA=${{ github.sha }}
SOURCE_DATE=${{ steps.source.outputs.date }} SOURCE_DATE=${{ steps.source.outputs.date }}
FLUXER_APP_PROXY_TIME_FREEZE_ENABLED=false
APP_ASSETS_REF=ghcr.io/${{ env.GHCR_OWNER }}/fluxer-app-proxy-self-hosted:${{ needs.meta.outputs.build_version }}-assets APP_ASSETS_REF=ghcr.io/${{ env.GHCR_OWNER }}/fluxer-app-proxy-self-hosted:${{ needs.meta.outputs.build_version }}-assets
APP_ASSETS_PLATFORM=linux/amd64 APP_ASSETS_PLATFORM=linux/amd64
cache-from: type=registry,ref=ghcr.io/${{ env.GHCR_OWNER }}/fluxer-app-proxy-self-hosted:buildcache-${{ matrix.platform }} cache-from: type=registry,ref=ghcr.io/${{ env.GHCR_OWNER }}/fluxer-app-proxy-self-hosted:buildcache-${{ matrix.platform }}
Generated
-10
View File
@@ -1755,16 +1755,6 @@ dependencies = [
"zip", "zip",
] ]
[[package]]
name = "fluxer-content-update-frozen-snapshot"
version = "0.1.0"
dependencies = [
"anyhow",
"base64 0.23.1",
"sha2 0.11.0",
"tempfile",
]
[[package]] [[package]]
name = "fluxer-dev" name = "fluxer-dev"
version = "0.1.0" version = "0.1.0"
-1
View File
@@ -9,7 +9,6 @@ members = [
"fluxer_messages", "fluxer_messages",
"fluxer_snowflakes", "fluxer_snowflakes",
"tools/ci", "tools/ci",
"tools/content/update-frozen-snapshot",
"tools/dev", "tools/dev",
"tools/i18n_auto", "tools/i18n_auto",
"fluxer_users", "fluxer_users",
-3
View File
@@ -23,6 +23,3 @@ tower-http = { version = "0.7.1", features = ["compression-gzip", "trace"] }
tracing = "0.1.44" tracing = "0.1.44"
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
[features]
default = ["time-freeze"]
time-freeze = []
+1 -7
View File
@@ -2,7 +2,6 @@
ARG BUILD_VERSION="" ARG BUILD_VERSION=""
ARG PUBLIC_ASSET_BASE_URL="" ARG PUBLIC_ASSET_BASE_URL=""
ARG FLUXER_APP_PROXY_TIME_FREEZE_ENABLED="true"
ARG BUNDLE_LOCAL_ASSETS="true" ARG BUNDLE_LOCAL_ASSETS="true"
ARG APP_ASSETS_REF=app-assets ARG APP_ASSETS_REF=app-assets
ARG APP_ASSETS_PLATFORM=$BUILDPLATFORM ARG APP_ASSETS_PLATFORM=$BUILDPLATFORM
@@ -111,7 +110,6 @@ FROM --platform=${APP_ASSETS_PLATFORM} ${APP_ASSETS_REF} AS app-static
# ---------- Stage 3: Build the Rust proxy binary ---------- # ---------- Stage 3: Build the Rust proxy binary ----------
FROM rust:1-trixie AS rust-builder FROM rust:1-trixie AS rust-builder
ARG FLUXER_APP_PROXY_TIME_FREEZE_ENABLED="true"
WORKDIR /usr/src/app WORKDIR /usr/src/app
@@ -138,11 +136,7 @@ RUN printf '%s\n' \
'strip = "symbols"' \ 'strip = "symbols"' \
> Cargo.toml > Cargo.toml
RUN if [ "${FLUXER_APP_PROXY_TIME_FREEZE_ENABLED}" = "false" ]; then \ RUN cargo build --release -p fluxer_app_proxy \
cargo build --release -p fluxer_app_proxy --no-default-features; \
else \
cargo build --release -p fluxer_app_proxy; \
fi \
&& cp target/release/fluxer_app_proxy /usr/local/bin/fluxer-app-proxy && cp target/release/fluxer_app_proxy /usr/local/bin/fluxer-app-proxy
# ---------- Stage 4: Runtime ---------- # ---------- Stage 4: Runtime ----------
-2
View File
@@ -1,7 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
variable "BUILD_VERSION" { default = "" } variable "BUILD_VERSION" { default = "" }
variable "PUBLIC_ASSET_BASE_URL" { default = "" } variable "PUBLIC_ASSET_BASE_URL" { default = "" }
variable "FLUXER_APP_PROXY_TIME_FREEZE_ENABLED" { default = "true" }
variable "BUNDLE_LOCAL_ASSETS" { default = "true" } variable "BUNDLE_LOCAL_ASSETS" { default = "true" }
variable "IMAGE_REPO" { default = "" } variable "IMAGE_REPO" { default = "" }
variable "CACHE_FROM" { default = "" } variable "CACHE_FROM" { default = "" }
@@ -26,7 +25,6 @@ target "app-proxy" {
args = { args = {
BUILD_VERSION = BUILD_VERSION BUILD_VERSION = BUILD_VERSION
PUBLIC_ASSET_BASE_URL = PUBLIC_ASSET_BASE_URL PUBLIC_ASSET_BASE_URL = PUBLIC_ASSET_BASE_URL
FLUXER_APP_PROXY_TIME_FREEZE_ENABLED = FLUXER_APP_PROXY_TIME_FREEZE_ENABLED
BUNDLE_LOCAL_ASSETS = BUNDLE_LOCAL_ASSETS BUNDLE_LOCAL_ASSETS = BUNDLE_LOCAL_ASSETS
APP_ASSETS_REF = APP_ASSETS_REF APP_ASSETS_REF = APP_ASSETS_REF
APP_ASSETS_PLATFORM = APP_ASSETS_PLATFORM APP_ASSETS_PLATFORM = APP_ASSETS_PLATFORM
-55
View File
@@ -348,7 +348,6 @@ pub struct AppProxyConfig {
pub discovery_upstream_url: String, pub discovery_upstream_url: String,
pub discovery_refresh_interval_ms: u64, pub discovery_refresh_interval_ms: u64,
pub release_channel: ReleaseChannel, pub release_channel: ReleaseChannel,
pub time_freeze_enabled: bool,
pub build_version: String, pub build_version: String,
pub bootstrap_api_endpoint: String, pub bootstrap_api_endpoint: String,
pub bootstrap_api_public_endpoint: Option<String>, pub bootstrap_api_public_endpoint: Option<String>,
@@ -445,7 +444,6 @@ impl AppProxyConfig {
&["RELEASE_CHANNEL"], &["RELEASE_CHANNEL"],
"stable", "stable",
)); ));
let time_freeze_enabled = resolve_time_freeze_enabled_from_env();
let geoip_source = cfg::parse_geoip_source_config( let geoip_source = cfg::parse_geoip_source_config(
&cfg::read_first_env(&["FLUXER_GEOIP_DB_PATH", "MAXMIND_DB_PATH"], ""), &cfg::read_first_env(&["FLUXER_GEOIP_DB_PATH", "MAXMIND_DB_PATH"], ""),
"app_proxy", "app_proxy",
@@ -489,7 +487,6 @@ impl AppProxyConfig {
60_000u64, 60_000u64,
), ),
release_channel, release_channel,
time_freeze_enabled,
build_version: cfg::read_env_preferred( build_version: cfg::read_env_preferred(
&["BUILD_VERSION", "FLUXER_BUILD_VERSION"], &["BUILD_VERSION", "FLUXER_BUILD_VERSION"],
env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_VERSION"),
@@ -522,10 +519,6 @@ fn resolve_discovery_upstream_url_from_env() -> String {
resolve_discovery_upstream_url(|name| env::var(name).ok()) resolve_discovery_upstream_url(|name| env::var(name).ok())
} }
fn resolve_time_freeze_enabled_from_env() -> bool {
resolve_time_freeze_enabled(|name| env::var(name).ok())
}
fn resolve_bootstrap_api_public_endpoint_from_env() -> Option<String> { fn resolve_bootstrap_api_public_endpoint_from_env() -> Option<String> {
resolve_bootstrap_api_public_endpoint(|name| env::var(name).ok()) resolve_bootstrap_api_public_endpoint(|name| env::var(name).ok())
.unwrap_or_else(|error| panic!("{error}")) .unwrap_or_else(|error| panic!("{error}"))
@@ -550,24 +543,6 @@ where
))) )))
} }
fn resolve_time_freeze_enabled<F>(mut read_var: F) -> bool
where
F: FnMut(&str) -> Option<String>,
{
if let Some(value) = read_var("FLUXER_APP_PROXY_TIME_FREEZE_ENABLED") {
return parse_boolish(&value);
}
!read_var("FLUXER_SELF_HOSTED").is_some_and(|value| parse_boolish(&value))
}
fn parse_boolish(value: &str) -> bool {
matches!(
value.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
}
fn resolve_discovery_upstream_url<F>(mut read_var: F) -> String fn resolve_discovery_upstream_url<F>(mut read_var: F) -> String
where where
F: FnMut(&str) -> Option<String>, F: FnMut(&str) -> Option<String>,
@@ -613,11 +588,6 @@ mod tests {
resolve_discovery_upstream_url(|name| env.get(name).map(|value| value.to_string())) resolve_discovery_upstream_url(|name| env.get(name).map(|value| value.to_string()))
} }
fn resolve_time_freeze_from_pairs(pairs: &[(&str, &str)]) -> bool {
let env: HashMap<&str, &str> = pairs.iter().copied().collect();
resolve_time_freeze_enabled(|name| env.get(name).map(|value| value.to_string()))
}
fn resolve_bootstrap_endpoint_from_pairs(pairs: &[(&str, &str)]) -> Option<String> { fn resolve_bootstrap_endpoint_from_pairs(pairs: &[(&str, &str)]) -> Option<String> {
try_resolve_bootstrap_endpoint_from_pairs(pairs).expect("the boot html endpoint resolves") try_resolve_bootstrap_endpoint_from_pairs(pairs).expect("the boot html endpoint resolves")
} }
@@ -817,29 +787,4 @@ mod tests {
DEFAULT_DISCOVERY_UPSTREAM_URL DEFAULT_DISCOVERY_UPSTREAM_URL
); );
} }
#[test]
fn time_freeze_enabled_by_default_for_hosted_runtime() {
assert!(resolve_time_freeze_from_pairs(&[]));
}
#[test]
fn time_freeze_disabled_by_default_for_self_hosted_runtime() {
assert!(!resolve_time_freeze_from_pairs(&[(
"FLUXER_SELF_HOSTED",
"true"
)]));
}
#[test]
fn explicit_time_freeze_setting_overrides_self_hosted_default() {
assert!(resolve_time_freeze_from_pairs(&[
("FLUXER_SELF_HOSTED", "true"),
("FLUXER_APP_PROXY_TIME_FREEZE_ENABLED", "true"),
]));
assert!(!resolve_time_freeze_from_pairs(&[
("FLUXER_SELF_HOSTED", "false"),
("FLUXER_APP_PROXY_TIME_FREEZE_ENABLED", "false"),
]));
}
} }
-371
View File
@@ -1,371 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::time_freeze::FrozenSnapshot;
use base64::Engine;
use std::sync::LazyLock;
const STABLE_SNAPSHOT_SHA: &str = "3250c73cbf94e97e8b9ca5a0ecb03c20bdde3867";
const STABLE_INDEX_HTML: &str = "\
PCFkb2N0eXBlIGh0bWw+PGh0bWwgbGFuZz0iZW4iPjxoZWFkPjxtZXRhIGNoYXJzZXQ9InV0Zi04Ij48dGl0bGU+Rmx1eGVyPC90\
aXRsZT48bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLGluaXRpYWwtc2NhbGU9MSx2aWV3\
cG9ydC1maXQ9Y292ZXIsaW50ZXJhY3RpdmUtd2lkZ2V0PXJlc2l6ZXMtY29udGVudCxtYXhpbXVtLXNjYWxlPTEsdXNlci1zY2Fs\
YWJsZT1ubyI+PG1ldGEgbmFtZT0iZGVzY3JpcHRpb24iIGNvbnRlbnQ9IkZsdXhlciBpcyBhIGZyZWUgYW5kIG9wZW4gc291cmNl\
IGluc3RhbnQgbWVzc2FnaW5nIGFuZCBWb0lQIHBsYXRmb3JtIGJ1aWx0IGZvciBmcmllbmRzLCBncm91cHMsIGFuZCBjb21tdW5p\
dGllcy4iPjxsaW5rIHJlbD0icHJlY29ubmVjdCIgaHJlZj0iaHR0cHM6Ly9mbHV4ZXJzdGF0aWMuY29tIj48bGluayByZWw9InN0\
eWxlc2hlZXQiIGhyZWY9Imh0dHBzOi8vZmx1eGVyc3RhdGljLmNvbS9mb250cy9pYm0tcGxleC5jc3MiPjxsaW5rIHJlbD0ibWFu\
aWZlc3QiIGhyZWY9Ii9tYW5pZmVzdC5qc29uIj48bGluayByZWw9Imljb24iIHR5cGU9ImltYWdlL3BuZyIgc2l6ZXM9IjMyeDMy\
IiBocmVmPSJodHRwczovL2ZsdXhlcnN0YXRpYy5jb20vd2ViL2Zhdmljb24tMzJ4MzIucG5nIj48bGluayByZWw9ImFwcGxlLXRv\
dWNoLWljb24iIHNpemVzPSIxODB4MTgwIiBocmVmPSJodHRwczovL2ZsdXhlcnN0YXRpYy5jb20vd2ViL2FwcGxlLXRvdWNoLWlj\
b24ucG5nIj48bWV0YSBuYW1lPSJhcHBsZS1tb2JpbGUtd2ViLWFwcC1jYXBhYmxlIiBjb250ZW50PSJ5ZXMiPjxtZXRhIG5hbWU9\
ImFwcGxlLW1vYmlsZS13ZWItYXBwLXN0YXR1cy1iYXItc3R5bGUiIGNvbnRlbnQ9ImRlZmF1bHQiPjxtZXRhIG5hbWU9ImFwcGxl\
LW1vYmlsZS13ZWItYXBwLXRpdGxlIiBjb250ZW50PSJGbHV4ZXIiPjxtZXRhIG5hbWU9Im1vYmlsZS13ZWItYXBwLWNhcGFibGUi\
IGNvbnRlbnQ9InllcyI+PG1ldGEgbmFtZT0idGhlbWUtY29sb3IiIGNvbnRlbnQ9IiM0NjQxRDkiPjxtZXRhIG5hbWU9Im1zYXBw\
bGljYXRpb24tbmF2YnV0dG9uLWNvbG9yIiBjb250ZW50PSIjNDY0MUQ5Ij48bWV0YSBuYW1lPSJtc2FwcGxpY2F0aW9uLVRpbGVD\
b2xvciIgY29udGVudD0iIzQ2NDFEOSI+PG1ldGEgbmFtZT0iZm9ybWF0LWRldGVjdGlvbiIgY29udGVudD0idGVsZXBob25lPW5v\
Ij48bWV0YSBuYW1lPSJkaXNwbGF5IiBjb250ZW50PSJzdGFuZGFsb25lIj48c2NyaXB0IG5vbmNlPSJ7e0NTUF9OT05DRV9QTEFD\
RUhPTERFUn19Ij4hZnVuY3Rpb24oKXt0cnl7dmFyIGU9d2luZG93LmxvY2F0aW9uO2lmKCIvIj09PWUucGF0aG5hbWUpe3ZhciBh\
PSIvY2hhbm5lbHMvQG1lIjtlLnNlYXJjaCYmKGErPWUuc2VhcmNoKSxlLmhhc2gmJihhKz1lLmhhc2gpLGUucmVwbGFjZShhKX19\
Y2F0Y2goZSl7fX0oKSxmdW5jdGlvbigpe3RyeXt2YXIgZT1sb2NhbFN0b3JhZ2UuZ2V0SXRlbSgidGhlbWUiKTtlJiZkb2N1bWVu\
dC5kb2N1bWVudEVsZW1lbnQuY2xhc3NMaXN0LmFkZCgidGhlbWUtIitlKX1jYXRjaHt9fSgpPC9zY3JpcHQ+PGxpbmsgaHJlZj0i\
aHR0cHM6Ly9mbHV4ZXJzdGF0aWMuY29tL2Fzc2V0cy9iOTNiNDgxOTU4OTA5N2NlLmNzcyIgcmVsPSJzdHlsZXNoZWV0Ij48bGlu\
ayBocmVmPSJodHRwczovL2ZsdXhlcnN0YXRpYy5jb20vYXNzZXRzL2YzNzMyZTk1YzczYmQ3N2MuY3NzIiByZWw9InN0eWxlc2hl\
ZXQiPjwvaGVhZD48Ym9keT48ZGl2IGlkPSJyb290Ij48L2Rpdj4KPG5vc2NyaXB0PkphdmFTY3JpcHQgaXMgcmVxdWlyZWQgdG8g\
dXNlIHRoaXMgYXBwbGljYXRpb24uPC9ub3NjcmlwdD4KCgo8c2NyaXB0IHNyYz0iaHR0cHM6Ly9mbHV4ZXJzdGF0aWMuY29tL2Fz\
c2V0cy84N2JiZWI3ZDZlMzY2YjNkLmpzIiB0eXBlPSJtb2R1bGUiPjwvc2NyaXB0PjxzY3JpcHQgc3JjPSJodHRwczovL2ZsdXhl\
cnN0YXRpYy5jb20vYXNzZXRzL2JlMWRlM2U0OWI1NWM2ZWEuanMiIHR5cGU9Im1vZHVsZSI+PC9zY3JpcHQ+PHNjcmlwdCBzcmM9\
Imh0dHBzOi8vZmx1eGVyc3RhdGljLmNvbS9hc3NldHMvMWY0OWE0NzEyMTg5OTlhZC5qcyIgdHlwZT0ibW9kdWxlIj48L3Njcmlw\
dD48c2NyaXB0IHNyYz0iaHR0cHM6Ly9mbHV4ZXJzdGF0aWMuY29tL2Fzc2V0cy8wYmY4ZDMwZDM2ZTdjMzk0LmpzIiB0eXBlPSJt\
b2R1bGUiPjwvc2NyaXB0PjxzY3JpcHQgc3JjPSJodHRwczovL2ZsdXhlcnN0YXRpYy5jb20vYXNzZXRzLzY0OTAzOTNlYTEyODc4\
NmYuanMiIHR5cGU9Im1vZHVsZSI+PC9zY3JpcHQ+PHNjcmlwdCBzcmM9Imh0dHBzOi8vZmx1eGVyc3RhdGljLmNvbS9hc3NldHMv\
YTJmYzQ2YTM2OTE2OGMxNy5qcyIgdHlwZT0ibW9kdWxlIj48L3NjcmlwdD48c2NyaXB0IHNyYz0iaHR0cHM6Ly9mbHV4ZXJzdGF0\
aWMuY29tL2Fzc2V0cy82ZGNkMzI5MTkxNjJhMzZiLmpzIiB0eXBlPSJtb2R1bGUiPjwvc2NyaXB0PjxzY3JpcHQgc3JjPSJodHRw\
czovL2ZsdXhlcnN0YXRpYy5jb20vYXNzZXRzLzNiYzNhNThiM2M3MDJhYmEuanMiIHR5cGU9Im1vZHVsZSI+PC9zY3JpcHQ+PHNj\
cmlwdCBzcmM9Imh0dHBzOi8vZmx1eGVyc3RhdGljLmNvbS9hc3NldHMvM2NhYWY1MzA0ODI3ODUwOC5qcyIgdHlwZT0ibW9kdWxl\
Ij48L3NjcmlwdD48c2NyaXB0IHNyYz0iaHR0cHM6Ly9mbHV4ZXJzdGF0aWMuY29tL2Fzc2V0cy8xM2EwZDZjY2I3YzllZjFjLmpz\
IiB0eXBlPSJtb2R1bGUiPjwvc2NyaXB0PjxzY3JpcHQgc3JjPSJodHRwczovL2ZsdXhlcnN0YXRpYy5jb20vYXNzZXRzL2NlZGE5\
MTIyZWMyMDI4OTIuanMiIHR5cGU9Im1vZHVsZSI+PC9zY3JpcHQ+PHNjcmlwdCBzcmM9Imh0dHBzOi8vZmx1eGVyc3RhdGljLmNv\
bS9hc3NldHMvYTcyYTQzNzIyMTljNzNlYi5qcyIgdHlwZT0ibW9kdWxlIj48L3NjcmlwdD48c2NyaXB0IHNyYz0iaHR0cHM6Ly9m\
bHV4ZXJzdGF0aWMuY29tL2Fzc2V0cy80N2JmYzQ5Nzg3Yjc5MDQ4LmpzIiB0eXBlPSJtb2R1bGUiPjwvc2NyaXB0PjxzY3JpcHQg\
c3JjPSJodHRwczovL2ZsdXhlcnN0YXRpYy5jb20vYXNzZXRzL2M2ZjEyY2Q5NjA3NzE5OTUuanMiIHR5cGU9Im1vZHVsZSI+PC9z\
Y3JpcHQ+PHNjcmlwdCBzcmM9Imh0dHBzOi8vZmx1eGVyc3RhdGljLmNvbS9hc3NldHMvMjNlOGZiZTIzMDdjMWFmNS5qcyIgdHlw\
ZT0ibW9kdWxlIj48L3NjcmlwdD48c2NyaXB0IHNyYz0iaHR0cHM6Ly9mbHV4ZXJzdGF0aWMuY29tL2Fzc2V0cy9hMTkxYjQyNmI1\
MWU5MjA4LmpzIiB0eXBlPSJtb2R1bGUiPjwvc2NyaXB0PjxzY3JpcHQgc3JjPSJodHRwczovL2ZsdXhlcnN0YXRpYy5jb20vYXNz\
ZXRzLzRlZjY0NDEzYzhjZTVhMzYuanMiIHR5cGU9Im1vZHVsZSI+PC9zY3JpcHQ+PHNjcmlwdCBzcmM9Imh0dHBzOi8vZmx1eGVy\
c3RhdGljLmNvbS9hc3NldHMvMzI1MGM3M2NiZjk0ZTk3ZS5qcyIgdHlwZT0ibW9kdWxlIj48L3NjcmlwdD48L2JvZHk+PC9odG1s\
Pg==\
";
const STABLE_SW_JS: &str = "\
InVzZSBzdHJpY3QiOwooKCkgPT4gewogIC8vIHNyYy9saWIvU2VydmljZVdvcmtlckxvZ0RCLnRzeAogIHZhciBTV19MT0dfREJf\
TkFNRSA9ICJGbHV4ZXJTd0xvZ3MiOwogIHZhciBTV19MT0dfREJfVkVSU0lPTiA9IDE7CiAgdmFyIExPR1NfU1RPUkUgPSAibG9n\
cyI7CiAgdmFyIE1FVEFfU1RPUkUgPSAibWV0YSI7CiAgdmFyIEVOQUJMRURfS0VZID0gImVuYWJsZWQiOwogIHZhciBNQVhfTE9H\
UyA9IDJlMzsKICB2YXIgVFJJTV9UUklHR0VSID0gMjIwMDsKICB2YXIgaWRiQXZhaWxhYmxlID0gKCkgPT4gdHlwZW9mIGluZGV4\
ZWREQiAhPT0gInVuZGVmaW5lZCI7CiAgdmFyIGRiUHJvbWlzZSA9IG51bGw7CiAgdmFyIG9wZW5EYiA9ICgpID0+IHsKICAgIGlm\
IChkYlByb21pc2UpIHJldHVybiBkYlByb21pc2U7CiAgICBpZiAoIWlkYkF2YWlsYWJsZSgpKSB7CiAgICAgIHJldHVybiBQcm9t\
aXNlLnJlamVjdChuZXcgRXJyb3IoIkluZGV4ZWREQiB1bmF2YWlsYWJsZSIpKTsKICAgIH0KICAgIGRiUHJvbWlzZSA9IG5ldyBQ\
cm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHsKICAgICAgbGV0IHJlcTsKICAgICAgdHJ5IHsKICAgICAgICByZXEgPSBpbmRl\
eGVkREIub3BlbihTV19MT0dfREJfTkFNRSwgU1dfTE9HX0RCX1ZFUlNJT04pOwogICAgICB9IGNhdGNoIChlcnIpIHsKICAgICAg\
ICByZWplY3QoZXJyKTsKICAgICAgICByZXR1cm47CiAgICAgIH0KICAgICAgcmVxLm9udXBncmFkZW5lZWRlZCA9ICgpID0+IHsK\
ICAgICAgICBjb25zdCBkYiA9IHJlcS5yZXN1bHQ7CiAgICAgICAgaWYgKCFkYi5vYmplY3RTdG9yZU5hbWVzLmNvbnRhaW5zKExP\
R1NfU1RPUkUpKSB7CiAgICAgICAgICBjb25zdCBzdG9yZSA9IGRiLmNyZWF0ZU9iamVjdFN0b3JlKExPR1NfU1RPUkUsIHsga2V5\
UGF0aDogImlkIiwgYXV0b0luY3JlbWVudDogdHJ1ZSB9KTsKICAgICAgICAgIHN0b3JlLmNyZWF0ZUluZGV4KCJ0cyIsICJ0cyIp\
OwogICAgICAgIH0KICAgICAgICBpZiAoIWRiLm9iamVjdFN0b3JlTmFtZXMuY29udGFpbnMoTUVUQV9TVE9SRSkpIHsKICAgICAg\
ICAgIGRiLmNyZWF0ZU9iamVjdFN0b3JlKE1FVEFfU1RPUkUpOwogICAgICAgIH0KICAgICAgfTsKICAgICAgcmVxLm9uc3VjY2Vz\
cyA9ICgpID0+IHsKICAgICAgICBjb25zdCBkYiA9IHJlcS5yZXN1bHQ7CiAgICAgICAgZGIub25jbG9zZSA9ICgpID0+IHsKICAg\
ICAgICAgIGRiUHJvbWlzZSA9IG51bGw7CiAgICAgICAgfTsKICAgICAgICBkYi5vbnZlcnNpb25jaGFuZ2UgPSAoKSA9PiB7CiAg\
ICAgICAgICB0cnkgewogICAgICAgICAgICBkYi5jbG9zZSgpOwogICAgICAgICAgfSBjYXRjaCB7CiAgICAgICAgICB9CiAgICAg\
ICAgICBkYlByb21pc2UgPSBudWxsOwogICAgICAgIH07CiAgICAgICAgcmVzb2x2ZShkYik7CiAgICAgIH07CiAgICAgIHJlcS5v\
bmVycm9yID0gKCkgPT4gcmVqZWN0KHJlcS5lcnJvciA/PyBuZXcgRXJyb3IoIkluZGV4ZWREQiBvcGVuIGZhaWxlZCIpKTsKICAg\
ICAgcmVxLm9uYmxvY2tlZCA9ICgpID0+IHJlamVjdChuZXcgRXJyb3IoIkluZGV4ZWREQiBvcGVuIGJsb2NrZWQiKSk7CiAgICB9\
KS5jYXRjaCgoZXJyKSA9PiB7CiAgICAgIGRiUHJvbWlzZSA9IG51bGw7CiAgICAgIHRocm93IGVycjsKICAgIH0pOwogICAgcmV0\
dXJuIGRiUHJvbWlzZTsKICB9OwogIHZhciBzZXJpYWxpemVEYXRhID0gKHZhbHVlLCBkZXB0aCA9IDApID0+IHsKICAgIGlmICh2\
YWx1ZSA9PSBudWxsKSByZXR1cm4gdmFsdWU7CiAgICBjb25zdCB0ID0gdHlwZW9mIHZhbHVlOwogICAgaWYgKHQgPT09ICJzdHJp\
bmciIHx8IHQgPT09ICJudW1iZXIiIHx8IHQgPT09ICJib29sZWFuIikgcmV0dXJuIHZhbHVlOwogICAgaWYgKHQgPT09ICJiaWdp\
bnQiKSByZXR1cm4gdmFsdWUudG9TdHJpbmcoKTsKICAgIGlmICh2YWx1ZSBpbnN0YW5jZW9mIEVycm9yKSB7CiAgICAgIHJldHVy\
biB7IF9fdHlwZTogIkVycm9yIiwgbmFtZTogdmFsdWUubmFtZSwgbWVzc2FnZTogdmFsdWUubWVzc2FnZSwgc3RhY2s6IHZhbHVl\
LnN0YWNrIH07CiAgICB9CiAgICBpZiAodmFsdWUgaW5zdGFuY2VvZiBEYXRlKSByZXR1cm4gdmFsdWUudG9JU09TdHJpbmcoKTsK\
ICAgIGlmIChkZXB0aCA+IDQpIHJldHVybiAiW3RydW5jYXRlZF0iOwogICAgaWYgKEFycmF5LmlzQXJyYXkodmFsdWUpKSB7CiAg\
ICAgIHJldHVybiB2YWx1ZS5zbGljZSgwLCA1MCkubWFwKChpdGVtKSA9PiBzZXJpYWxpemVEYXRhKGl0ZW0sIGRlcHRoICsgMSkp\
OwogICAgfQogICAgaWYgKHQgPT09ICJvYmplY3QiKSB7CiAgICAgIGNvbnN0IG91dCA9IHt9OwogICAgICBsZXQgY291bnQgPSAw\
OwogICAgICBmb3IgKGNvbnN0IGtleSBvZiBPYmplY3Qua2V5cyh2YWx1ZSkpIHsKICAgICAgICBpZiAoY291bnQgPj0gNTApIHsK\
ICAgICAgICAgIG91dFsiX190cnVuY2F0ZWQiXSA9IHRydWU7CiAgICAgICAgICBicmVhazsKICAgICAgICB9CiAgICAgICAgdHJ5\
IHsKICAgICAgICAgIG91dFtrZXldID0gc2VyaWFsaXplRGF0YSh2YWx1ZVtrZXldLCBkZXB0aCArIDEpOwogICAgICAgIH0gY2F0\
Y2ggewogICAgICAgICAgb3V0W2tleV0gPSAiW3Vuc2VyaWFsaXphYmxlXSI7CiAgICAgICAgfQogICAgICAgIGNvdW50ICs9IDE7\
CiAgICAgIH0KICAgICAgcmV0dXJuIG91dDsKICAgIH0KICAgIHJldHVybiBgWyR7dH1dYDsKICB9OwogIGFzeW5jIGZ1bmN0aW9u\
IGFwcGVuZFN3TG9nKGxldmVsLCBzb3VyY2UsIG1lc3NhZ2UsIGRhdGEpIHsKICAgIGlmICghaWRiQXZhaWxhYmxlKCkpIHJldHVy\
bjsKICAgIGxldCBkYjsKICAgIHRyeSB7CiAgICAgIGRiID0gYXdhaXQgb3BlbkRiKCk7CiAgICB9IGNhdGNoIHsKICAgICAgcmV0\
dXJuOwogICAgfQogICAgY29uc3QgcmVjb3JkID0gewogICAgICB0czogRGF0ZS5ub3coKSwKICAgICAgbGV2ZWwsCiAgICAgIHNv\
dXJjZSwKICAgICAgbWVzc2FnZSwKICAgICAgZGF0YTogZGF0YSA9PT0gdm9pZCAwID8gdm9pZCAwIDogc2VyaWFsaXplRGF0YShk\
YXRhKQogICAgfTsKICAgIHRyeSB7CiAgICAgIGF3YWl0IG5ldyBQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHsKICAgICAg\
ICBjb25zdCB0eCA9IGRiLnRyYW5zYWN0aW9uKExPR1NfU1RPUkUsICJyZWFkd3JpdGUiKTsKICAgICAgICB0eC5vbmNvbXBsZXRl\
ID0gKCkgPT4gcmVzb2x2ZSgpOwogICAgICAgIHR4Lm9uZXJyb3IgPSAoKSA9PiByZWplY3QodHguZXJyb3IpOwogICAgICAgIHR4\
Lm9uYWJvcnQgPSAoKSA9PiByZWplY3QodHguZXJyb3IpOwogICAgICAgIHR4Lm9iamVjdFN0b3JlKExPR1NfU1RPUkUpLmFkZChy\
ZWNvcmQpOwogICAgICB9KTsKICAgIH0gY2F0Y2ggewogICAgICByZXR1cm47CiAgICB9CiAgICB2b2lkIHNjaGVkdWxlVHJpbSgp\
OwogIH0KICB2YXIgdHJpbVNjaGVkdWxlZCA9IGZhbHNlOwogIHZhciBzY2hlZHVsZVRyaW0gPSBhc3luYyAoKSA9PiB7CiAgICBp\
ZiAodHJpbVNjaGVkdWxlZCkgcmV0dXJuOwogICAgdHJpbVNjaGVkdWxlZCA9IHRydWU7CiAgICB0cnkgewogICAgICBhd2FpdCB0\
cmltSWZOZWVkZWQoKTsKICAgIH0gZmluYWxseSB7CiAgICAgIHRyaW1TY2hlZHVsZWQgPSBmYWxzZTsKICAgIH0KICB9OwogIGFz\
eW5jIGZ1bmN0aW9uIHRyaW1JZk5lZWRlZCgpIHsKICAgIGxldCBkYjsKICAgIHRyeSB7CiAgICAgIGRiID0gYXdhaXQgb3BlbkRi\
KCk7CiAgICB9IGNhdGNoIHsKICAgICAgcmV0dXJuOwogICAgfQogICAgbGV0IGNvdW50ID0gMDsKICAgIHRyeSB7CiAgICAgIGNv\
dW50ID0gYXdhaXQgbmV3IFByb21pc2UoKHJlc29sdmUsIHJlamVjdCkgPT4gewogICAgICAgIGNvbnN0IHR4ID0gZGIudHJhbnNh\
Y3Rpb24oTE9HU19TVE9SRSwgInJlYWRvbmx5Iik7CiAgICAgICAgY29uc3QgcmVxID0gdHgub2JqZWN0U3RvcmUoTE9HU19TVE9S\
RSkuY291bnQoKTsKICAgICAgICByZXEub25zdWNjZXNzID0gKCkgPT4gcmVzb2x2ZShyZXEucmVzdWx0KTsKICAgICAgICByZXEu\
b25lcnJvciA9ICgpID0+IHJlamVjdChyZXEuZXJyb3IpOwogICAgICB9KTsKICAgIH0gY2F0Y2ggewogICAgICByZXR1cm47CiAg\
ICB9CiAgICBpZiAoY291bnQgPCBUUklNX1RSSUdHRVIpIHJldHVybjsKICAgIGNvbnN0IHRvRGVsZXRlID0gY291bnQgLSBNQVhf\
TE9HUzsKICAgIHRyeSB7CiAgICAgIGF3YWl0IG5ldyBQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHsKICAgICAgICBjb25z\
dCB0eCA9IGRiLnRyYW5zYWN0aW9uKExPR1NfU1RPUkUsICJyZWFkd3JpdGUiKTsKICAgICAgICB0eC5vbmNvbXBsZXRlID0gKCkg\
PT4gcmVzb2x2ZSgpOwogICAgICAgIHR4Lm9uZXJyb3IgPSAoKSA9PiByZWplY3QodHguZXJyb3IpOwogICAgICAgIHR4Lm9uYWJv\
cnQgPSAoKSA9PiByZWplY3QodHguZXJyb3IpOwogICAgICAgIGNvbnN0IHN0b3JlID0gdHgub2JqZWN0U3RvcmUoTE9HU19TVE9S\
RSk7CiAgICAgICAgY29uc3QgY3Vyc29yUmVxID0gc3RvcmUuaW5kZXgoInRzIikub3BlbkN1cnNvcigpOwogICAgICAgIGxldCBk\
ZWxldGVkID0gMDsKICAgICAgICBjdXJzb3JSZXEub25zdWNjZXNzID0gKCkgPT4gewogICAgICAgICAgY29uc3QgY3Vyc29yID0g\
Y3Vyc29yUmVxLnJlc3VsdDsKICAgICAgICAgIGlmICghY3Vyc29yIHx8IGRlbGV0ZWQgPj0gdG9EZWxldGUpIHJldHVybjsKICAg\
ICAgICAgIGN1cnNvci5kZWxldGUoKTsKICAgICAgICAgIGRlbGV0ZWQgKz0gMTsKICAgICAgICAgIGN1cnNvci5jb250aW51ZSgp\
OwogICAgICAgIH07CiAgICAgICAgY3Vyc29yUmVxLm9uZXJyb3IgPSAoKSA9PiByZWplY3QoY3Vyc29yUmVxLmVycm9yKTsKICAg\
ICAgfSk7CiAgICB9IGNhdGNoIHsKICAgIH0KICB9CiAgYXN5bmMgZnVuY3Rpb24gaXNTd0xvZ2dpbmdFbmFibGVkKCkgewogICAg\
aWYgKCFpZGJBdmFpbGFibGUoKSkgcmV0dXJuIGZhbHNlOwogICAgbGV0IGRiOwogICAgdHJ5IHsKICAgICAgZGIgPSBhd2FpdCBv\
cGVuRGIoKTsKICAgIH0gY2F0Y2ggewogICAgICByZXR1cm4gZmFsc2U7CiAgICB9CiAgICByZXR1cm4gbmV3IFByb21pc2UoKHJl\
c29sdmUpID0+IHsKICAgICAgY29uc3QgdHggPSBkYi50cmFuc2FjdGlvbihNRVRBX1NUT1JFLCAicmVhZG9ubHkiKTsKICAgICAg\
Y29uc3QgcmVxID0gdHgub2JqZWN0U3RvcmUoTUVUQV9TVE9SRSkuZ2V0KEVOQUJMRURfS0VZKTsKICAgICAgcmVxLm9uc3VjY2Vz\
cyA9ICgpID0+IHJlc29sdmUocmVxLnJlc3VsdCA9PT0gdHJ1ZSk7CiAgICAgIHJlcS5vbmVycm9yID0gKCkgPT4gcmVzb2x2ZShm\
YWxzZSk7CiAgICB9KTsKICB9CiAgdmFyIFNXX0xPR19TRVRfRU5BQkxFRF9NRVNTQUdFID0gIlNXX0xPR19TRVRfRU5BQkxFRCI7\
CgogIC8vIHNyYy9zZXJ2aWNlX3dvcmtlci9Xb3JrZXIudHN4CiAgdmFyIGxvZ2dpbmdFbmFibGVkID0gZmFsc2U7CiAgdmFyIGVu\
c3VyZUxvZ2dpbmdFbmFibGVkTG9hZGVkID0gaXNTd0xvZ2dpbmdFbmFibGVkKCkudGhlbigKICAgICh2KSA9PiB7CiAgICAgIGxv\
Z2dpbmdFbmFibGVkID0gdjsKICAgIH0sCiAgICAoKSA9PiB2b2lkIDAKICApOwogIHZhciBsb2cgPSBhc3luYyAobGV2ZWwsIG1l\
c3NhZ2UsIGRhdGEpID0+IHsKICAgIHRyeSB7CiAgICAgIGlmICh0eXBlb2YgY29uc29sZSAhPT0gInVuZGVmaW5lZCIpIHsKICAg\
ICAgICBjb25zdCBmbiA9IGxldmVsID09PSAiZXJyb3IiID8gY29uc29sZS5lcnJvciA6IGxldmVsID09PSAid2FybiIgPyBjb25z\
b2xlLndhcm4gOiBsZXZlbCA9PT0gImluZm8iID8gY29uc29sZS5pbmZvIDogY29uc29sZS5kZWJ1ZzsKICAgICAgICBpZiAoZGF0\
YSAhPT0gdm9pZCAwKSBmbj8uY2FsbChjb25zb2xlLCAiW1NXXSIsIG1lc3NhZ2UsIGRhdGEpOwogICAgICAgIGVsc2UgZm4/LmNh\
bGwoY29uc29sZSwgIltTV10iLCBtZXNzYWdlKTsKICAgICAgfQogICAgfSBjYXRjaCB7CiAgICB9CiAgICBpZiAoIWxvZ2dpbmdF\
bmFibGVkKSByZXR1cm47CiAgICB0cnkgewogICAgICBhd2FpdCBhcHBlbmRTd0xvZyhsZXZlbCwgInN3IiwgbWVzc2FnZSwgZGF0\
YSk7CiAgICB9IGNhdGNoIHsKICAgIH0KICB9OwogIHNlbGYuYWRkRXZlbnRMaXN0ZW5lcigiaW5zdGFsbCIsIChldmVudCkgPT4g\
ewogICAgZXZlbnQud2FpdFVudGlsKAogICAgICAoYXN5bmMgKCkgPT4gewogICAgICAgIGF3YWl0IGVuc3VyZUxvZ2dpbmdFbmFi\
bGVkTG9hZGVkOwogICAgICAgIGF3YWl0IGxvZygiaW5mbyIsICJpbnN0YWxsIik7CiAgICAgICAgc2VsZi5za2lwV2FpdGluZygp\
OwogICAgICB9KSgpCiAgICApOwogIH0pOwogIHNlbGYuYWRkRXZlbnRMaXN0ZW5lcigiYWN0aXZhdGUiLCAoZXZlbnQpID0+IHsK\
ICAgIGV2ZW50LndhaXRVbnRpbCgKICAgICAgKGFzeW5jICgpID0+IHsKICAgICAgICBhd2FpdCBlbnN1cmVMb2dnaW5nRW5hYmxl\
ZExvYWRlZDsKICAgICAgICBhd2FpdCBsb2coImluZm8iLCAiYWN0aXZhdGUiKTsKICAgICAgICBhd2FpdCBzZWxmLmNsaWVudHMu\
Y2xhaW0oKTsKICAgICAgICBhd2FpdCBsb2coImRlYnVnIiwgImNsaWVudHMgY2xhaW1lZCIpOwogICAgICB9KSgpCiAgICApOwog\
IH0pOwogIHNlbGYuYWRkRXZlbnRMaXN0ZW5lcigibWVzc2FnZSIsIChldmVudCkgPT4gewogICAgY29uc3QgZGF0YSA9IGV2ZW50\
LmRhdGE7CiAgICBjb25zdCB0eXBlID0gZGF0YT8udHlwZTsKICAgIGlmICh0eXBlID09PSAiU0tJUF9XQUlUSU5HIikgewogICAg\
ICBldmVudC53YWl0VW50aWwobG9nKCJpbmZvIiwgIm1lc3NhZ2U6IFNLSVBfV0FJVElORyIpKTsKICAgICAgc2VsZi5za2lwV2Fp\
dGluZygpOwogICAgICByZXR1cm47CiAgICB9CiAgICBpZiAodHlwZSA9PT0gIkFQUF9VUERBVEVfQkFER0UiKSB7CiAgICAgIGNv\
bnN0IHJhd0NvdW50ID0gZGF0YT8uY291bnQ7CiAgICAgIGxldCBiYWRnZUNvdW50ID0gbnVsbDsKICAgICAgaWYgKHR5cGVvZiBy\
YXdDb3VudCA9PT0gIm51bWJlciIgJiYgTnVtYmVyLmlzRmluaXRlKHJhd0NvdW50KSkgewogICAgICAgIGJhZGdlQ291bnQgPSBy\
YXdDb3VudDsKICAgICAgfSBlbHNlIGlmICh0eXBlb2YgcmF3Q291bnQgPT09ICJzdHJpbmciICYmIHJhd0NvdW50Lmxlbmd0aCA+\
IDApIHsKICAgICAgICBjb25zdCBwYXJzZWQgPSBOdW1iZXIocmF3Q291bnQpOwogICAgICAgIGJhZGdlQ291bnQgPSBOdW1iZXIu\
aXNGaW5pdGUocGFyc2VkKSA/IHBhcnNlZCA6IG51bGw7CiAgICAgIH0KICAgICAgZXZlbnQud2FpdFVudGlsKAogICAgICAgIChh\
c3luYyAoKSA9PiB7CiAgICAgICAgICBhd2FpdCBsb2coImRlYnVnIiwgIm1lc3NhZ2U6IEFQUF9VUERBVEVfQkFER0UiLCB7IGJh\
ZGdlQ291bnQgfSk7CiAgICAgICAgICBhd2FpdCB1cGRhdGVBcHBCYWRnZShiYWRnZUNvdW50KTsKICAgICAgICB9KSgpCiAgICAg\
ICk7CiAgICAgIHJldHVybjsKICAgIH0KICAgIGlmICh0eXBlID09PSBTV19MT0dfU0VUX0VOQUJMRURfTUVTU0FHRSkgewogICAg\
ICBjb25zdCBlbmFibGVkID0gZGF0YT8uZW5hYmxlZCA9PT0gdHJ1ZTsKICAgICAgbG9nZ2luZ0VuYWJsZWQgPSBlbmFibGVkOwog\
ICAgICBldmVudC53YWl0VW50aWwobG9nKCJpbmZvIiwgYG1lc3NhZ2U6IGxvZ2dpbmcgJHtlbmFibGVkID8gImVuYWJsZWQiIDog\
ImRpc2FibGVkIn1gKSk7CiAgICAgIHJldHVybjsKICAgIH0KICAgIGV2ZW50LndhaXRVbnRpbChsb2coImRlYnVnIiwgIm1lc3Nh\
Z2U6IHVua25vd24iLCB7IHR5cGUgfSkpOwogIH0pOwogIHZhciBub3JtYWxpemVQdXNoUGF5bG9hZCA9IChyYXcpID0+IHsKICAg\
IGNvbnN0IHBheWxvYWQgPSByYXcgPz8ge307CiAgICBpZiAocGF5bG9hZC53ZWJfcHVzaCA9PT0gODAzMCAmJiBwYXlsb2FkLm5v\
dGlmaWNhdGlvbiAmJiB0eXBlb2YgcGF5bG9hZC5ub3RpZmljYXRpb24gPT09ICJvYmplY3QiKSB7CiAgICAgIGNvbnN0IGlubmVy\
ID0gcGF5bG9hZC5ub3RpZmljYXRpb247CiAgICAgIGNvbnN0IG5hdmlnYXRlVXJsID0gaW5uZXIubmF2aWdhdGU7CiAgICAgIGNv\
bnN0IGRhdGEgPSB7IC4uLmlubmVyLmRhdGEgPz8ge30gfTsKICAgICAgaWYgKHR5cGVvZiBuYXZpZ2F0ZVVybCA9PT0gInN0cmlu\
ZyIgJiYgIWRhdGEudXJsKSBkYXRhLnVybCA9IG5hdmlnYXRlVXJsOwogICAgICByZXR1cm4geyAuLi5pbm5lciwgZGF0YSB9Owog\
ICAgfQogICAgcmV0dXJuIHBheWxvYWQ7CiAgfTsKICB2YXIgZ2V0QmFkZ2VDb3VudCA9IChwYXlsb2FkKSA9PiB7CiAgICBjb25z\
dCBiYWRnZVZhbHVlID0gcGF5bG9hZC5kYXRhPy5iYWRnZV9jb3VudDsKICAgIGlmICh0eXBlb2YgYmFkZ2VWYWx1ZSA9PT0gIm51\
bWJlciIgJiYgTnVtYmVyLmlzRmluaXRlKGJhZGdlVmFsdWUpKSB7CiAgICAgIHJldHVybiBiYWRnZVZhbHVlOwogICAgfQogICAg\
aWYgKHR5cGVvZiBiYWRnZVZhbHVlID09PSAic3RyaW5nIiAmJiBiYWRnZVZhbHVlLmxlbmd0aCA+IDApIHsKICAgICAgY29uc3Qg\
cGFyc2VkID0gTnVtYmVyKGJhZGdlVmFsdWUpOwogICAgICByZXR1cm4gTnVtYmVyLmlzRmluaXRlKHBhcnNlZCkgPyBwYXJzZWQg\
OiBudWxsOwogICAgfQogICAgcmV0dXJuIG51bGw7CiAgfTsKICB2YXIgdXBkYXRlQXBwQmFkZ2UgPSBhc3luYyAoY291bnQpID0+\
IHsKICAgIGlmICh0eXBlb2YgbmF2aWdhdG9yLnNldEFwcEJhZGdlICE9PSAiZnVuY3Rpb24iICYmIHR5cGVvZiBuYXZpZ2F0b3Iu\
Y2xlYXJBcHBCYWRnZSAhPT0gImZ1bmN0aW9uIikgewogICAgICByZXR1cm47CiAgICB9CiAgICB0cnkgewogICAgICBpZiAoY291\
bnQgIT09IG51bGwgJiYgY291bnQgPiAwKSB7CiAgICAgICAgaWYgKHR5cGVvZiBuYXZpZ2F0b3Iuc2V0QXBwQmFkZ2UgPT09ICJm\
dW5jdGlvbiIpIHsKICAgICAgICAgIGF3YWl0IG5hdmlnYXRvci5zZXRBcHBCYWRnZShjb3VudCk7CiAgICAgICAgICBhd2FpdCBs\
b2coImRlYnVnIiwgImJhZGdlIHNldCIsIHsgY291bnQgfSk7CiAgICAgICAgfQogICAgICB9IGVsc2UgaWYgKHR5cGVvZiBuYXZp\
Z2F0b3IuY2xlYXJBcHBCYWRnZSA9PT0gImZ1bmN0aW9uIikgewogICAgICAgIGF3YWl0IG5hdmlnYXRvci5jbGVhckFwcEJhZGdl\
KCk7CiAgICAgICAgYXdhaXQgbG9nKCJkZWJ1ZyIsICJiYWRnZSBjbGVhcmVkIik7CiAgICAgIH0KICAgIH0gY2F0Y2ggKGVycm9y\
KSB7CiAgICAgIGF3YWl0IGxvZygiZXJyb3IiLCAiRmFpbGVkIHRvIHVwZGF0ZSBhcHAgYmFkZ2UiLCB7IGVycm9yOiBkZXNjcmli\
ZUVycm9yKGVycm9yKSB9KTsKICAgIH0KICB9OwogIHZhciByZXNvbHZlVGFyZ2V0VXJsID0gKHVybCkgPT4gewogICAgaWYgKCF1\
cmwpIHJldHVybiBudWxsOwogICAgdHJ5IHsKICAgICAgY29uc3QgcmVzb2x2ZWQgPSBuZXcgVVJMKHVybCwgc2VsZi5sb2NhdGlv\
bi5vcmlnaW4pOwogICAgICBpZiAocmVzb2x2ZWQucHJvdG9jb2wgIT09ICJodHRwOiIgJiYgcmVzb2x2ZWQucHJvdG9jb2wgIT09\
ICJodHRwczoiKSByZXR1cm4gbnVsbDsKICAgICAgcmV0dXJuIHJlc29sdmVkLnRvU3RyaW5nKCk7CiAgICB9IGNhdGNoIHsKICAg\
ICAgcmV0dXJuIG51bGw7CiAgICB9CiAgfTsKICB2YXIgcG9zdE1lc3NhZ2VUb0NsaWVudHMgPSBhc3luYyAobWVzc2FnZSkgPT4g\
ewogICAgdHJ5IHsKICAgICAgY29uc3QgY2xpZW50TGlzdCA9IGF3YWl0IHNlbGYuY2xpZW50cy5tYXRjaEFsbCh7CiAgICAgICAg\
dHlwZTogIndpbmRvdyIsCiAgICAgICAgaW5jbHVkZVVuY29udHJvbGxlZDogdHJ1ZQogICAgICB9KTsKICAgICAgZm9yIChjb25z\
dCBjbGllbnQgb2YgY2xpZW50TGlzdCkgewogICAgICAgIGNsaWVudC5wb3N0TWVzc2FnZShtZXNzYWdlKTsKICAgICAgfQogICAg\
ICByZXR1cm4gY2xpZW50TGlzdDsKICAgIH0gY2F0Y2ggKGVycm9yKSB7CiAgICAgIGF3YWl0IGxvZygiZXJyb3IiLCAiVW5hYmxl\
IHRvIGJyb2FkY2FzdCB0byBjbGllbnRzIiwgeyBlcnJvcjogZGVzY3JpYmVFcnJvcihlcnJvcikgfSk7CiAgICAgIHJldHVybiBb\
XTsKICAgIH0KICB9OwogIHZhciBmb2N1c09yT3BlbkNsaWVudCA9IGFzeW5jICh0YXJnZXRVcmwsIHRhcmdldFVzZXJJZCkgPT4g\
ewogICAgY29uc3QgbWVzc2FnZSA9IHsKICAgICAgdHlwZTogIk5PVElGSUNBVElPTl9DTElDS19OQVZJR0FURSIsCiAgICAgIHVy\
bDogdGFyZ2V0VXJsCiAgICB9OwogICAgaWYgKHRhcmdldFVzZXJJZCkgewogICAgICBtZXNzYWdlLnRhcmdldFVzZXJJZCA9IHRh\
cmdldFVzZXJJZDsKICAgIH0KICAgIGNvbnN0IGNsaWVudExpc3QgPSBhd2FpdCBwb3N0TWVzc2FnZVRvQ2xpZW50cyhtZXNzYWdl\
KTsKICAgIGNvbnN0IGV4YWN0ID0gY2xpZW50TGlzdC5maW5kKChjKSA9PiBjLnVybCA9PT0gdGFyZ2V0VXJsKTsKICAgIGlmIChl\
eGFjdCkgewogICAgICBhd2FpdCBsb2coImRlYnVnIiwgImZvY3VzIGV4YWN0IGNsaWVudCIsIHsgdXJsOiB0YXJnZXRVcmwgfSk7\
CiAgICAgIGF3YWl0IGV4YWN0LmZvY3VzKCk7CiAgICAgIHJldHVybjsKICAgIH0KICAgIGNvbnN0IHNhbWVPcmlnaW4gPSBjbGll\
bnRMaXN0LmZpbmQoKGMpID0+IHsKICAgICAgdHJ5IHsKICAgICAgICByZXR1cm4gbmV3IFVSTChjLnVybCkub3JpZ2luID09PSBz\
ZWxmLmxvY2F0aW9uLm9yaWdpbjsKICAgICAgfSBjYXRjaCB7CiAgICAgICAgcmV0dXJuIGZhbHNlOwogICAgICB9CiAgICB9KTsK\
ICAgIGlmIChzYW1lT3JpZ2luKSB7CiAgICAgIGF3YWl0IGxvZygiZGVidWciLCAiZm9jdXMgc2FtZS1vcmlnaW4gY2xpZW50Iiwg\
eyB1cmw6IHNhbWVPcmlnaW4udXJsIH0pOwogICAgICBhd2FpdCBzYW1lT3JpZ2luLmZvY3VzKCk7CiAgICAgIHJldHVybjsKICAg\
IH0KICAgIGlmIChzZWxmLmNsaWVudHMub3BlbldpbmRvdykgewogICAgICBhd2FpdCBsb2coImRlYnVnIiwgIm9wZW5XaW5kb3ci\
LCB7IHVybDogdGFyZ2V0VXJsIH0pOwogICAgICBhd2FpdCBzZWxmLmNsaWVudHMub3BlbldpbmRvdyh0YXJnZXRVcmwpOwogICAg\
fQogIH07CiAgdmFyIGRlc2NyaWJlRXJyb3IgPSAoZXJyb3IpID0+IHsKICAgIGlmIChlcnJvciBpbnN0YW5jZW9mIEVycm9yKSB7\
CiAgICAgIHJldHVybiB7IG5hbWU6IGVycm9yLm5hbWUsIG1lc3NhZ2U6IGVycm9yLm1lc3NhZ2UsIHN0YWNrOiBlcnJvci5zdGFj\
ayB9OwogICAgfQogICAgcmV0dXJuIHsgdmFsdWU6IFN0cmluZyhlcnJvcikgfTsKICB9OwogIHNlbGYuYWRkRXZlbnRMaXN0ZW5l\
cigicHVzaCIsIChldmVudCkgPT4gewogICAgZXZlbnQud2FpdFVudGlsKAogICAgICAoYXN5bmMgKCkgPT4gewogICAgICAgIGF3\
YWl0IGVuc3VyZUxvZ2dpbmdFbmFibGVkTG9hZGVkOwogICAgICAgIGxldCByYXc7CiAgICAgICAgbGV0IHJhd1RleHQgPSBudWxs\
OwogICAgICAgIHRyeSB7CiAgICAgICAgICByYXdUZXh0ID0gZXZlbnQuZGF0YT8udGV4dCgpID8/IG51bGw7CiAgICAgICAgfSBj\
YXRjaCB7CiAgICAgICAgICByYXdUZXh0ID0gbnVsbDsKICAgICAgICB9CiAgICAgICAgdHJ5IHsKICAgICAgICAgIHJhdyA9IGV2\
ZW50LmRhdGE/Lmpzb24/LigpOwogICAgICAgIH0gY2F0Y2ggKGVycm9yKSB7CiAgICAgICAgICBhd2FpdCBsb2coImVycm9yIiwg\
InB1c2g6IEpTT04gcGFyc2UgZmFpbGVkIiwgewogICAgICAgICAgICBlcnJvcjogZGVzY3JpYmVFcnJvcihlcnJvciksCiAgICAg\
ICAgICAgIHJhd1ByZXZpZXc6IHJhd1RleHQ/LnNsaWNlKDAsIDUxMikgPz8gbnVsbAogICAgICAgICAgfSk7CiAgICAgICAgfQog\
ICAgICAgIGNvbnN0IHBheWxvYWQgPSBub3JtYWxpemVQdXNoUGF5bG9hZChyYXcgPz8geyB0aXRsZTogIkZsdXhlciIgfSk7CiAg\
ICAgICAgY29uc3QgdGl0bGUgPSBwYXlsb2FkLnRpdGxlID8/ICJGbHV4ZXIiOwogICAgICAgIGNvbnN0IGNoYW5uZWxJZCA9IHBh\
eWxvYWQuZGF0YT8uY2hhbm5lbF9pZDsKICAgICAgICBjb25zdCB0YWcgPSBwYXlsb2FkLnRhZyA/PyAodHlwZW9mIGNoYW5uZWxJ\
ZCA9PT0gInN0cmluZyIgPyBgY2hhbm5lbDoke2NoYW5uZWxJZH1gIDogdm9pZCAwKTsKICAgICAgICBjb25zdCBib2R5ID0gcGF5\
bG9hZC5ib2R5ID8/ICIiOwogICAgICAgIGNvbnN0IG9wdGlvbnMgPSB7CiAgICAgICAgICBib2R5LAogICAgICAgICAgaWNvbjog\
cGF5bG9hZC5pY29uID8/IHZvaWQgMCwKICAgICAgICAgIGJhZGdlOiBwYXlsb2FkLmJhZGdlID8/IHZvaWQgMCwKICAgICAgICAg\
IGRhdGE6IHBheWxvYWQuZGF0YSA/PyB2b2lkIDAsCiAgICAgICAgICB0YWcsCiAgICAgICAgICByZW5vdGlmeTogdGFnICE9PSB2\
b2lkIDAKICAgICAgICB9OwogICAgICAgIGNvbnN0IGJhZGdlQ291bnQgPSBnZXRCYWRnZUNvdW50KHBheWxvYWQpOwogICAgICAg\
IGF3YWl0IGxvZygiaW5mbyIsICJwdXNoIHJlY2VpdmVkIiwgewogICAgICAgICAgdGl0bGUsCiAgICAgICAgICBoYXNCb2R5OiBC\
b29sZWFuKHBheWxvYWQuYm9keSksCiAgICAgICAgICB0YWcsCiAgICAgICAgICBoYXNEYXRhOiBwYXlsb2FkLmRhdGEgIT09IHZv\
aWQgMCwKICAgICAgICAgIGJhZGdlQ291bnQsCiAgICAgICAgICB3ZWJQdXNoOiBwYXlsb2FkLndlYl9wdXNoID8/IG51bGwKICAg\
ICAgICB9KTsKICAgICAgICB0cnkgewogICAgICAgICAgYXdhaXQgUHJvbWlzZS5hbGwoW3NlbGYucmVnaXN0cmF0aW9uLnNob3dO\
b3RpZmljYXRpb24odGl0bGUsIG9wdGlvbnMpLCB1cGRhdGVBcHBCYWRnZShiYWRnZUNvdW50KV0pOwogICAgICAgIH0gY2F0Y2gg\
KGVycm9yKSB7CiAgICAgICAgICBhd2FpdCBsb2coImVycm9yIiwgInB1c2g6IHNob3dOb3RpZmljYXRpb24gZmFpbGVkLCBzaG93\
aW5nIGZhbGxiYWNrIiwgeyBlcnJvcjogZGVzY3JpYmVFcnJvcihlcnJvcikgfSk7CiAgICAgICAgICB0cnkgewogICAgICAgICAg\
ICBhd2FpdCBzZWxmLnJlZ2lzdHJhdGlvbi5zaG93Tm90aWZpY2F0aW9uKCJGbHV4ZXIiLCB7IGJvZHk6ICIiIH0pOwogICAgICAg\
ICAgfSBjYXRjaCAoZmFsbGJhY2tFcnJvcikgewogICAgICAgICAgICBhd2FpdCBsb2coImVycm9yIiwgInB1c2g6IGZhbGxiYWNr\
IHNob3dOb3RpZmljYXRpb24gYWxzbyBmYWlsZWQiLCB7CiAgICAgICAgICAgICAgZXJyb3I6IGRlc2NyaWJlRXJyb3IoZmFsbGJh\
Y2tFcnJvcikKICAgICAgICAgICAgfSk7CiAgICAgICAgICB9CiAgICAgICAgfQogICAgICB9KSgpCiAgICApOwogIH0pOwogIHNl\
bGYuYWRkRXZlbnRMaXN0ZW5lcigibm90aWZpY2F0aW9uY2xpY2siLCAoZXZlbnQpID0+IHsKICAgIGV2ZW50Lm5vdGlmaWNhdGlv\
bi5jbG9zZSgpOwogICAgY29uc3QgdGFyZ2V0VXJsID0gcmVzb2x2ZVRhcmdldFVybChldmVudC5ub3RpZmljYXRpb24uZGF0YT8u\
dXJsKTsKICAgIGNvbnN0IHRhcmdldFVzZXJJZCA9IGV2ZW50Lm5vdGlmaWNhdGlvbi5kYXRhPy50YXJnZXRfdXNlcl9pZDsKICAg\
IGV2ZW50LndhaXRVbnRpbCgKICAgICAgKGFzeW5jICgpID0+IHsKICAgICAgICBhd2FpdCBlbnN1cmVMb2dnaW5nRW5hYmxlZExv\
YWRlZDsKICAgICAgICBhd2FpdCBsb2coImluZm8iLCAibm90aWZpY2F0aW9uY2xpY2siLCB7IHRhcmdldFVybCwgdGFyZ2V0VXNl\
cklkLCB0YWc6IGV2ZW50Lm5vdGlmaWNhdGlvbi50YWcgfSk7CiAgICAgICAgaWYgKCF0YXJnZXRVcmwpIHJldHVybjsKICAgICAg\
ICB0cnkgewogICAgICAgICAgYXdhaXQgZm9jdXNPck9wZW5DbGllbnQodGFyZ2V0VXJsLCB0YXJnZXRVc2VySWQpOwogICAgICAg\
IH0gY2F0Y2ggKGVycm9yKSB7CiAgICAgICAgICBhd2FpdCBsb2coImVycm9yIiwgIm5vdGlmaWNhdGlvbmNsaWNrOiBmb2N1c09y\
T3BlbiBmYWlsZWQiLCB7IGVycm9yOiBkZXNjcmliZUVycm9yKGVycm9yKSB9KTsKICAgICAgICB9CiAgICAgIH0pKCkKICAgICk7\
CiAgfSk7CiAgc2VsZi5hZGRFdmVudExpc3RlbmVyKCJub3RpZmljYXRpb25jbG9zZSIsIChldmVudCkgPT4gewogICAgZXZlbnQu\
d2FpdFVudGlsKGxvZygiZGVidWciLCAibm90aWZpY2F0aW9uY2xvc2UiLCB7IHRhZzogZXZlbnQubm90aWZpY2F0aW9uLnRhZyB9\
KSk7CiAgfSk7CiAgdmFyIGFycmF5QnVmZmVyVG9CYXNlNjRVcmwgPSAoYnVmZmVyKSA9PiB7CiAgICBpZiAoIWJ1ZmZlcikgcmV0\
dXJuIG51bGw7CiAgICBjb25zdCBieXRlcyA9IG5ldyBVaW50OEFycmF5KGJ1ZmZlcik7CiAgICBsZXQgYmluYXJ5ID0gIiI7CiAg\
ICBmb3IgKGxldCBpID0gMDsgaSA8IGJ5dGVzLmxlbmd0aDsgaSArPSAxKSBiaW5hcnkgKz0gU3RyaW5nLmZyb21DaGFyQ29kZShi\
eXRlc1tpXSk7CiAgICByZXR1cm4gYnRvYShiaW5hcnkpLnJlcGxhY2UoL1wrL2csICItIikucmVwbGFjZSgvXC8vZywgIl8iKS5y\
ZXBsYWNlKC89KyQvLCAiIik7CiAgfTsKICB2YXIgdXJsQmFzZTY0VG9VaW50OEFycmF5ID0gKGJhc2U2NCkgPT4gewogICAgY29u\
c3QgcGFkZGluZyA9ICI9Ii5yZXBlYXQoKDQgLSBiYXNlNjQubGVuZ3RoICUgNCkgJSA0KTsKICAgIGNvbnN0IHJhdyA9IGF0b2Io\
KGJhc2U2NCArIHBhZGRpbmcpLnJlcGxhY2UoLy0vZywgIisiKS5yZXBsYWNlKC9fL2csICIvIikpOwogICAgY29uc3Qgb3V0ID0g\
bmV3IFVpbnQ4QXJyYXkocmF3Lmxlbmd0aCk7CiAgICBmb3IgKGxldCBpID0gMDsgaSA8IHJhdy5sZW5ndGg7IGkgKz0gMSkgb3V0\
W2ldID0gcmF3LmNoYXJDb2RlQXQoaSk7CiAgICByZXR1cm4gb3V0OwogIH07CiAgdmFyIGZldGNoSW5zdGFuY2VDb25maWcgPSBh\
c3luYyAoKSA9PiB7CiAgICB0cnkgewogICAgICBjb25zdCByZXMgPSBhd2FpdCBmZXRjaCgiLy53ZWxsLWtub3duL2ZsdXhlciIs\
IHsgY3JlZGVudGlhbHM6ICJpbmNsdWRlIiB9KTsKICAgICAgaWYgKCFyZXMub2spIHJldHVybiBudWxsOwogICAgICBjb25zdCBk\
YXRhID0gYXdhaXQgcmVzLmpzb24oKTsKICAgICAgY29uc3QgYXBpQ2xpZW50ID0gZGF0YS5lbmRwb2ludHM/LmFwaV9jbGllbnQg\
Pz8gZGF0YS5lbmRwb2ludHM/LmFwaTsKICAgICAgaWYgKCFhcGlDbGllbnQpIHJldHVybiBudWxsOwogICAgICByZXR1cm4geyBh\
cGlDbGllbnQsIHZhcGlkS2V5OiBkYXRhLnB1c2g/LnB1YmxpY192YXBpZF9rZXkgPz8gbnVsbCB9OwogICAgfSBjYXRjaCAoZXJy\
b3IpIHsKICAgICAgYXdhaXQgbG9nKCJlcnJvciIsICJGYWlsZWQgdG8gZmV0Y2ggaW5zdGFuY2UgY29uZmlnIGluIFNXIiwgeyBl\
cnJvcjogZGVzY3JpYmVFcnJvcihlcnJvcikgfSk7CiAgICAgIHJldHVybiBudWxsOwogICAgfQogIH07CiAgdmFyIFNXX0FQSV9W\
RVJTSU9OID0gMTsKICB2YXIgcm90YXRlU3Vic2NyaXB0aW9uT25TZXJ2ZXIgPSBhc3luYyAob2xkRW5kcG9pbnQsIG5ld1N1Yiwg\
YXBpQ2xpZW50KSA9PiB7CiAgICBjb25zdCBwMjU2ZGggPSBhcnJheUJ1ZmZlclRvQmFzZTY0VXJsKG5ld1N1Yi5nZXRLZXkoInAy\
NTZkaCIpKTsKICAgIGNvbnN0IGF1dGggPSBhcnJheUJ1ZmZlclRvQmFzZTY0VXJsKG5ld1N1Yi5nZXRLZXkoImF1dGgiKSk7CiAg\
ICBpZiAoIXAyNTZkaCB8fCAhYXV0aCkgewogICAgICBhd2FpdCBsb2coImVycm9yIiwgInJvdGF0ZTogbmV3IHN1YnNjcmlwdGlv\
biBtaXNzaW5nIGtleXMiKTsKICAgICAgcmV0dXJuOwogICAgfQogICAgY29uc3QgdXJsID0gYCR7YXBpQ2xpZW50fS92JHtTV19B\
UElfVkVSU0lPTn0vdXNlcnMvQG1lL3B1c2gvcm90YXRlYDsKICAgIHRyeSB7CiAgICAgIGNvbnN0IHJlcyA9IGF3YWl0IGZldGNo\
KHVybCwgewogICAgICAgIG1ldGhvZDogIlBPU1QiLAogICAgICAgIGNyZWRlbnRpYWxzOiAiaW5jbHVkZSIsCiAgICAgICAgaGVh\
ZGVyczogeyAiQ29udGVudC1UeXBlIjogImFwcGxpY2F0aW9uL2pzb24iIH0sCiAgICAgICAgYm9keTogSlNPTi5zdHJpbmdpZnko\
ewogICAgICAgICAgb2xkX2VuZHBvaW50OiBvbGRFbmRwb2ludCwKICAgICAgICAgIGVuZHBvaW50OiBuZXdTdWIuZW5kcG9pbnQs\
CiAgICAgICAgICBrZXlzOiB7IHAyNTZkaCwgYXV0aCB9LAogICAgICAgICAgdXNlcl9hZ2VudDogc2VsZi5uYXZpZ2F0b3IudXNl\
ckFnZW50CiAgICAgICAgfSkKICAgICAgfSk7CiAgICAgIGF3YWl0IGxvZyhyZXMub2sgPyAiaW5mbyIgOiAiZXJyb3IiLCAicm90\
YXRlOiBzZXJ2ZXIgcmVzcG9uc2UiLCB7IHN0YXR1czogcmVzLnN0YXR1cywgb2s6IHJlcy5vayB9KTsKICAgIH0gY2F0Y2ggKGVy\
cm9yKSB7CiAgICAgIGF3YWl0IGxvZygiZXJyb3IiLCAicm90YXRlOiBmZXRjaCBmYWlsZWQiLCB7IGVycm9yOiBkZXNjcmliZUVy\
cm9yKGVycm9yKSB9KTsKICAgIH0KICB9OwogIHNlbGYuYWRkRXZlbnRMaXN0ZW5lcigicHVzaHN1YnNjcmlwdGlvbmNoYW5nZSIs\
IChldmVudCkgPT4gewogICAgZXZlbnQud2FpdFVudGlsKAogICAgICAoYXN5bmMgKCkgPT4gewogICAgICAgIGF3YWl0IGVuc3Vy\
ZUxvZ2dpbmdFbmFibGVkTG9hZGVkOwogICAgICAgIGF3YWl0IGxvZygiaW5mbyIsICJwdXNoc3Vic2NyaXB0aW9uY2hhbmdlIiwg\
ewogICAgICAgICAgaGFzT2xkOiBCb29sZWFuKGV2ZW50Lm9sZFN1YnNjcmlwdGlvbiksCiAgICAgICAgICBoYXNOZXc6IEJvb2xl\
YW4oZXZlbnQubmV3U3Vic2NyaXB0aW9uKSwKICAgICAgICAgIG9sZEVuZHBvaW50OiBldmVudC5vbGRTdWJzY3JpcHRpb24/LmVu\
ZHBvaW50ID8/IG51bGwsCiAgICAgICAgICBuZXdFbmRwb2ludDogZXZlbnQubmV3U3Vic2NyaXB0aW9uPy5lbmRwb2ludCA/PyBu\
dWxsCiAgICAgICAgfSk7CiAgICAgICAgdHJ5IHsKICAgICAgICAgIGNvbnN0IG9sZEVuZHBvaW50ID0gZXZlbnQub2xkU3Vic2Ny\
aXB0aW9uPy5lbmRwb2ludCA/PyBudWxsOwogICAgICAgICAgbGV0IGFwcFNlcnZlcktleSA9IGV2ZW50Lm9sZFN1YnNjcmlwdGlv\
bj8ub3B0aW9ucz8uYXBwbGljYXRpb25TZXJ2ZXJLZXkgPz8gbnVsbDsKICAgICAgICAgIGNvbnN0IGNvbmZpZyA9IGF3YWl0IGZl\
dGNoSW5zdGFuY2VDb25maWcoKTsKICAgICAgICAgIGlmICghYXBwU2VydmVyS2V5ICYmIGNvbmZpZz8udmFwaWRLZXkpIHsKICAg\
ICAgICAgICAgYXBwU2VydmVyS2V5ID0gdXJsQmFzZTY0VG9VaW50OEFycmF5KGNvbmZpZy52YXBpZEtleSkuYnVmZmVyOwogICAg\
ICAgICAgfQogICAgICAgICAgaWYgKCFhcHBTZXJ2ZXJLZXkpIHsKICAgICAgICAgICAgYXdhaXQgbG9nKCJlcnJvciIsICJwdXNo\
c3Vic2NyaXB0aW9uY2hhbmdlOiBubyBWQVBJRCBrZXkgYXZhaWxhYmxlIik7CiAgICAgICAgICAgIGF3YWl0IHBvc3RNZXNzYWdl\
VG9DbGllbnRzKHsgdHlwZTogIlBVU0hfU1VCU0NSSVBUSU9OX0NIQU5HRSIgfSk7CiAgICAgICAgICAgIHJldHVybjsKICAgICAg\
ICAgIH0KICAgICAgICAgIGNvbnN0IG5ld1N1YiA9IGV2ZW50Lm5ld1N1YnNjcmlwdGlvbiA/PyBhd2FpdCBzZWxmLnJlZ2lzdHJh\
dGlvbi5wdXNoTWFuYWdlci5zdWJzY3JpYmUoewogICAgICAgICAgICB1c2VyVmlzaWJsZU9ubHk6IHRydWUsCiAgICAgICAgICAg\
IGFwcGxpY2F0aW9uU2VydmVyS2V5OiBhcHBTZXJ2ZXJLZXkKICAgICAgICAgIH0pOwogICAgICAgICAgaWYgKG9sZEVuZHBvaW50\
ICYmIGNvbmZpZykgewogICAgICAgICAgICBhd2FpdCByb3RhdGVTdWJzY3JpcHRpb25PblNlcnZlcihvbGRFbmRwb2ludCwgbmV3\
U3ViLCBjb25maWcuYXBpQ2xpZW50KTsKICAgICAgICAgIH0gZWxzZSB7CiAgICAgICAgICAgIGF3YWl0IGxvZygid2FybiIsICJw\
dXNoc3Vic2NyaXB0aW9uY2hhbmdlOiB1bmFibGUgdG8gcm90YXRlIG9uIHNlcnZlciIsIHsKICAgICAgICAgICAgICBoYXNPbGRF\
bmRwb2ludDogQm9vbGVhbihvbGRFbmRwb2ludCksCiAgICAgICAgICAgICAgaGFzQ29uZmlnOiBCb29sZWFuKGNvbmZpZykKICAg\
ICAgICAgICAgfSk7CiAgICAgICAgICB9CiAgICAgICAgICBhd2FpdCBwb3N0TWVzc2FnZVRvQ2xpZW50cyh7IHR5cGU6ICJQVVNI\
X1NVQlNDUklQVElPTl9DSEFOR0UiIH0pOwogICAgICAgIH0gY2F0Y2ggKGVycm9yKSB7CiAgICAgICAgICBhd2FpdCBsb2coImVy\
cm9yIiwgInB1c2hzdWJzY3JpcHRpb25jaGFuZ2UgaGFuZGxlciBmYWlsZWQiLCB7IGVycm9yOiBkZXNjcmliZUVycm9yKGVycm9y\
KSB9KTsKICAgICAgICAgIGF3YWl0IHBvc3RNZXNzYWdlVG9DbGllbnRzKHsgdHlwZTogIlBVU0hfU1VCU0NSSVBUSU9OX0NIQU5H\
RSIgfSkuY2F0Y2goKCkgPT4gdm9pZCAwKTsKICAgICAgICB9CiAgICAgIH0pKCkKICAgICk7CiAgfSk7Cn0pKCk7Ci8vIyBzb3Vy\
Y2VNYXBwaW5nVVJMPXN3LmpzLm1hcAo=\
";
const STABLE_VERSION_JSON: &str = "\
eyJzaGEiOiIzMjUwYzczY2JmOTRlOTdlOGI5Y2E1YTBlY2IwM2MyMGJkZGUzODY3IiwiYnVpbGROdW1iZXIiOjUwOCwiYnVpbGRU\
aW1lc3RhbXAiOiIxNzgwNzgxMTY4In0=\
";
fn decode(b64: &str) -> Vec<u8> {
base64::engine::general_purpose::STANDARD
.decode(b64)
.expect("frozen snapshot base64 decode failed")
}
pub static STABLE_SNAPSHOT: LazyLock<FrozenSnapshot> = LazyLock::new(|| FrozenSnapshot {
sha: STABLE_SNAPSHOT_SHA.to_owned(),
index_html: decode(STABLE_INDEX_HTML),
sw_js: decode(STABLE_SW_JS),
version_json: decode(STABLE_VERSION_JSON),
});
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stable_snapshot_sha_matches() {
assert_eq!(STABLE_SNAPSHOT.sha, STABLE_SNAPSHOT_SHA);
}
#[test]
fn index_html_decodes_and_starts_with_doctype() {
assert_index_html(&STABLE_SNAPSHOT);
}
#[test]
fn sw_js_decodes_and_is_javascript() {
assert_sw_js(&STABLE_SNAPSHOT);
}
#[test]
fn version_json_decodes_and_is_valid_json() {
assert_version_json(&STABLE_SNAPSHOT);
}
fn assert_index_html(snapshot: &FrozenSnapshot) {
let html = std::str::from_utf8(&snapshot.index_html).unwrap();
assert!(
html.starts_with("<!doctype html>"),
"unexpected start: {}",
&html[..40]
);
}
fn assert_sw_js(snapshot: &FrozenSnapshot) {
let js = std::str::from_utf8(&snapshot.sw_js).unwrap();
assert!(
js.contains("use strict"),
"sw.js should contain 'use strict'"
);
}
fn assert_version_json(snapshot: &FrozenSnapshot) {
let json_str = std::str::from_utf8(&snapshot.version_json).unwrap();
let parsed: serde_json::Value = serde_json::from_str(json_str).unwrap();
assert!(
parsed
.get("sha")
.or_else(|| parsed.get("version"))
.is_some(),
"version.json should have a sha or version field"
);
}
}
-3
View File
@@ -4,9 +4,6 @@ pub mod bootstrap;
pub mod config; pub mod config;
pub mod csp; pub mod csp;
pub mod discovery_cache; pub mod discovery_cache;
#[cfg(feature = "time-freeze")]
pub mod frozen_snapshots;
pub mod geoip; pub mod geoip;
pub mod routes; pub mod routes;
pub mod state; pub mod state;
pub mod time_freeze;
+5 -217
View File
@@ -9,9 +9,6 @@ use crate::state::{
AppProxyBudgets, AppState, MAX_RENDERED_SPA_INDEX_BYTES, MAX_SPA_INDEX_BYTES, AppProxyBudgets, AppState, MAX_RENDERED_SPA_INDEX_BYTES, MAX_SPA_INDEX_BYTES,
read_bounded_text_file, read_bounded_text_file,
}; };
use crate::time_freeze::{
load_time_freeze_config_for_request, should_serve_frozen, time_freeze_debug_header,
};
use axum::{ use axum::{
body::{Body, Bytes}, body::{Body, Bytes},
extract::{Request, State}, extract::{Request, State},
@@ -142,8 +139,6 @@ async fn serve_static_file(
} }
async fn serve_spa_index(state: &AppState, headers: &HeaderMap) -> Response { async fn serve_spa_index(state: &AppState, headers: &HeaderMap) -> Response {
let time_freeze = load_time_freeze_config_for_request(&state.config, headers);
let debug_header = time_freeze_debug_header(&time_freeze);
let should_bust_dev_assets = state.config.index_upstream_url.is_some(); let should_bust_dev_assets = state.config.index_upstream_url.is_some();
let discovery = match refresh_discovery_for_spa(state).await { let discovery = match refresh_discovery_for_spa(state).await {
@@ -168,13 +163,9 @@ async fn serve_spa_index(state: &AppState, headers: &HeaderMap) -> Response {
let geoip = build_geoip_response(state.geoip.lookup(headers)); let geoip = build_geoip_response(state.geoip.lookup(headers));
let script_tag = build_bootstrap_script(&state.config, &discovery, &geoip, &nonce); let script_tag = build_bootstrap_script(&state.config, &discovery, &geoip, &nonce);
let raw_html = if let Some(snapshot) = should_serve_frozen(&time_freeze) { let raw_html = match load_spa_index_html(state).await {
String::from_utf8_lossy(&snapshot.index_html).into_owned() Ok(content) => content,
} else { Err(response) => return response,
match load_spa_index_html(state).await {
Ok(content) => content,
Err(response) => return response,
}
}; };
let dev_buster = should_bust_dev_assets.then(current_dev_asset_cache_buster); let dev_buster = should_bust_dev_assets.then(current_dev_asset_cache_buster);
@@ -193,7 +184,7 @@ async fn serve_spa_index(state: &AppState, headers: &HeaderMap) -> Response {
} }
}; };
let html = html.into_boxed_str(); let html = html.into_boxed_str();
build_spa_response(html, csp, debug_header.as_deref(), should_bust_dev_assets) build_spa_response(html, csp, should_bust_dev_assets)
} }
#[derive(Debug)] #[derive(Debug)]
@@ -377,12 +368,7 @@ impl AsRef<[u8]> for SpaDocumentBody {
} }
} }
fn build_spa_response( fn build_spa_response(html: Box<str>, csp: HeaderValue, dev_no_store: bool) -> Response {
html: Box<str>,
csp: HeaderValue,
time_freeze_header: Option<&str>,
dev_no_store: bool,
) -> Response {
let body = Bytes::from_owner(SpaDocumentBody { html }); let body = Bytes::from_owner(SpaDocumentBody { html });
let mut response = Response::new(Body::from(body)); let mut response = Response::new(Body::from(body));
let headers = response.headers_mut(); let headers = response.headers_mut();
@@ -419,17 +405,6 @@ fn build_spa_response(
axum::http::HeaderName::from_static("critical-ch"), axum::http::HeaderName::from_static("critical-ch"),
HeaderValue::from_static(CRITICAL_CH_VALUE), HeaderValue::from_static(CRITICAL_CH_VALUE),
); );
#[cfg(feature = "time-freeze")]
{
if let Some(tf) = time_freeze_header
&& let Ok(v) = HeaderValue::from_str(tf)
{
headers.insert(axum::http::HeaderName::from_static("x-time-freeze"), v);
}
}
#[cfg(not(feature = "time-freeze"))]
let _ = time_freeze_header;
response response
} }
@@ -667,40 +642,6 @@ mod tests {
assert!(!untouched.contains("_=9911")); assert!(!untouched.contains("_=9911"));
} }
#[cfg(feature = "time-freeze")]
#[test]
fn the_frozen_shell_is_never_served_with_an_unfilled_nonce_or_a_missing_bootstrap() {
let frozen = String::from_utf8_lossy(&crate::frozen_snapshots::STABLE_SNAPSHOT.index_html)
.into_owned();
assert!(
frozen.contains("{{CSP_NONCE_PLACEHOLDER}}"),
"the captured shell should still carry the hole this test proves we fill"
);
let served = render_spa_document(
&frozen,
"frozennonce",
"<script>frozenboot</script>",
"https://fluxerstatic.com",
"",
None,
)
.expect("test SPA document must render within its size limit");
assert!(
!served.contains("{{CSP_NONCE_PLACEHOLDER}}"),
"a frozen-served shell shipped a literal nonce placeholder"
);
assert!(
served.contains(r#"nonce="frozennonce""#),
"a frozen-served shell lost its per-request nonce"
);
assert!(
served.contains("<script>frozenboot</script>"),
"a frozen-served shell shipped without the bootstrap script"
);
}
const SHELL_WITH_ENDPOINT_HOLES: &str = r#"<!doctype html><html><head><title>Fluxer</title><link rel="preconnect" href="{{STATIC_CDN_ENDPOINT}}"> const SHELL_WITH_ENDPOINT_HOLES: &str = r#"<!doctype html><html><head><title>Fluxer</title><link rel="preconnect" href="{{STATIC_CDN_ENDPOINT}}">
<link rel="preconnect" href="{{STATIC_CDN_ENDPOINT}}" crossorigin> <link rel="preconnect" href="{{STATIC_CDN_ENDPOINT}}" crossorigin>
<link rel="preconnect" href="{{MEDIA_ENDPOINT}}"> <link rel="preconnect" href="{{MEDIA_ENDPOINT}}">
@@ -840,7 +781,6 @@ mod tests {
let discovery_upstream_url = spawn_local_origin(discovery_body, "application/json").await; let discovery_upstream_url = spawn_local_origin(discovery_body, "application/json").await;
let mut config = AppProxyConfig::from_env(); let mut config = AppProxyConfig::from_env();
config.release_channel = channel; config.release_channel = channel;
config.time_freeze_enabled = true;
config.index_upstream_url = index_upstream_url.map(|url| { config.index_upstream_url = index_upstream_url.map(|url| {
crate::config::HttpUrl::parse("TEST_INDEX_UPSTREAM_URL", &url) crate::config::HttpUrl::parse("TEST_INDEX_UPSTREAM_URL", &url)
.expect("test index upstream URL must be a valid HTTP URL") .expect("test index upstream URL must be a valid HTTP URL")
@@ -948,158 +888,6 @@ mod tests {
); );
} }
#[cfg(feature = "time-freeze")]
#[tokio::test]
async fn the_frozen_document_is_served_with_its_asset_urls_untouched() {
let captured =
String::from_utf8_lossy(&crate::frozen_snapshots::STABLE_SNAPSHOT.index_html)
.into_owned();
assert!(
captured.contains(
r#"<link rel="stylesheet" href="https://fluxerstatic.com/fonts/ibm-plex.css">"#
),
"the captured shell no longer carries the asset URL this test proves we leave alone"
);
let state = spa_state_serving(ReleaseChannel::Stable, None).await;
let response = serve_spa_index(&state, &HeaderMap::new()).await;
assert_eq!(response.status(), StatusCode::OK);
let served = read_document(response).await;
assert!(
served.contains(
r#"<link rel="stylesheet" href="https://fluxerstatic.com/fonts/ibm-plex.css">"#
),
"the frozen document's stylesheet URL was rewritten on the primary hosted path"
);
assert!(
served.contains(r#"<link rel="manifest" href="/manifest.json">"#),
"the frozen document's manifest URL was rewritten on the primary hosted path"
);
assert!(
!served.contains("?_=") && !served.contains("&_="),
"the frozen document was served with a development cache-busting query"
);
}
#[cfg(feature = "time-freeze")]
#[tokio::test]
async fn the_frozen_branch_serves_a_rendered_document_and_not_the_raw_shell() {
let captured =
String::from_utf8_lossy(&crate::frozen_snapshots::STABLE_SNAPSHOT.index_html)
.into_owned();
assert!(
captured.contains("{{CSP_NONCE_PLACEHOLDER}}"),
"the captured shell no longer carries the nonce hole this test proves we fill"
);
assert!(
!captured.contains("window.__FLUXER_BOOTSTRAP__"),
"the captured shell already carries a bootstrap, so this test can no longer tell a rendered document from a raw shell"
);
let state = spa_state_serving(ReleaseChannel::Stable, None).await;
let response = serve_spa_index(&state, &HeaderMap::new()).await;
assert_eq!(response.status(), StatusCode::OK);
let granted_nonce = nonce_granted_by(&response);
let served = read_document(response).await;
assert!(
!served.contains("{{CSP_NONCE_PLACEHOLDER}}"),
"the primary hosted path shipped a literal nonce placeholder"
);
assert_eq!(
served
.matches(&format!(r#"nonce="{granted_nonce}""#))
.count(),
served.matches(r#"nonce=""#).count(),
"the frozen document carries a nonce its own policy header never granted"
);
assert!(
served.contains(&format!(
r#"<script nonce="{granted_nonce}">window.__FLUXER_BOOTSTRAP__"#
)),
"the primary hosted path shipped without a bootstrap script the browser will run"
);
let second = serve_spa_index(&state, &HeaderMap::new()).await;
assert_ne!(
nonce_granted_by(&second),
granted_nonce,
"two frozen requests were granted the same nonce"
);
}
#[cfg(feature = "time-freeze")]
#[tokio::test]
async fn every_branch_announces_which_snapshot_decision_it_took() {
let frozen_state = spa_state_serving(ReleaseChannel::Stable, None).await;
let frozen = serve_spa_index(&frozen_state, &HeaderMap::new()).await;
assert_eq!(
frozen
.headers()
.get("x-time-freeze")
.expect("the frozen response announced no snapshot decision")
.to_str()
.unwrap(),
format!(
"frozen; sha={}",
crate::frozen_snapshots::STABLE_SNAPSHOT.sha
),
"the frozen response named the wrong snapshot"
);
let live_state =
spa_state_serving(ReleaseChannel::Canary, Some(SHELL_WITH_ENDPOINT_HOLES)).await;
let live = serve_spa_index(&live_state, &HeaderMap::new()).await;
assert_eq!(
live.headers()
.get("x-time-freeze")
.expect("the live response announced no snapshot decision")
.to_str()
.unwrap(),
"no-snapshot",
"a channel with no snapshot claimed one anyway"
);
}
#[cfg(feature = "time-freeze")]
#[tokio::test]
async fn the_frozen_shell_is_never_served_with_the_asset_lifetime() {
let state = spa_state_serving(ReleaseChannel::Stable, None).await;
let response = serve_spa_index(&state, &HeaderMap::new()).await;
assert_eq!(
response
.headers()
.get("x-time-freeze")
.expect("the response announced no snapshot decision")
.to_str()
.unwrap(),
format!(
"frozen; sha={}",
crate::frozen_snapshots::STABLE_SNAPSHOT.sha
),
"this test never reached the frozen branch, so it proves nothing about it"
);
let cache_control = response
.headers()
.get(header::CACHE_CONTROL)
.expect("the frozen shell was served without a cache policy at all")
.to_str()
.unwrap();
assert_eq!(
cache_control, "no-cache",
"the frozen document naming the hashed bundle must be revalidated on every load"
);
assert_ne!(
cache_control, LONG_LIVED_ASSET_CACHE_CONTROL,
"a frozen shell cached for a year pins every returning visitor to the deployed-over bundle"
);
}
#[tokio::test] #[tokio::test]
async fn a_crawl_control_document_is_never_served_with_the_asset_lifetime() { async fn a_crawl_control_document_is_never_served_with_the_asset_lifetime() {
let root = std::env::temp_dir().join(format!( let root = std::env::temp_dir().join(format!(
+4 -74
View File
@@ -3,54 +3,14 @@
use crate::config::HttpEndpoint; use crate::config::HttpEndpoint;
use crate::discovery_cache::discovery_endpoint; use crate::discovery_cache::discovery_endpoint;
use crate::state::{AppState, MAX_STATIC_TEXT_FILE_BYTES, read_bounded_file}; use crate::state::{AppState, MAX_STATIC_TEXT_FILE_BYTES, read_bounded_file};
use crate::time_freeze::{
TimeFreezeConfig, describe_decision, load_time_freeze_config_for_request,
time_freeze_debug_header,
};
use axum::{ use axum::{
extract::State, extract::State,
http::{HeaderMap, HeaderValue, StatusCode, header}, http::{HeaderValue, StatusCode, header},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use std::path::Path; use std::path::Path;
fn serve_frozen_file( pub async fn version_json(State(state): State<AppState>) -> Response {
config: &TimeFreezeConfig,
pick: impl FnOnce(&crate::time_freeze::FrozenSnapshot) -> Option<(&[u8], &str)>,
) -> (Option<Response>, Option<String>) {
let debug = describe_decision(config);
let debug_header = time_freeze_debug_header(config);
if debug.decision != crate::time_freeze::TimeFreezeDecision::Frozen {
return (None, debug_header);
}
if let Some(snapshot) = config.snapshot
&& let Some((bytes, content_type)) = pick(snapshot)
{
let mut response = bytes.to_vec().into_response();
if let Ok(ct) = HeaderValue::from_str(content_type) {
response.headers_mut().insert(header::CONTENT_TYPE, ct);
}
response
.headers_mut()
.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
set_time_freeze_header(&mut response, debug_header.as_deref());
return (Some(response), None);
}
(None, debug_header)
}
pub async fn version_json(State(state): State<AppState>, headers: HeaderMap) -> Response {
let tf = load_time_freeze_config_for_request(&state.config, &headers);
let (frozen, debug_header) = serve_frozen_file(&tf, |snap| {
Some((snap.version_json.as_slice(), "application/json"))
});
if let Some(resp) = frozen {
return resp;
}
let mut result = serve_static_text_file(&state, "version.json", "application/json").await; let mut result = serve_static_text_file(&state, "version.json", "application/json").await;
if result.status() == StatusCode::NOT_FOUND && !state.config.build_version.is_empty() { if result.status() == StatusCode::NOT_FOUND && !state.config.build_version.is_empty() {
@@ -61,7 +21,6 @@ pub async fn version_json(State(state): State<AppState>, headers: HeaderMap) ->
.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache")); .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
} }
set_time_freeze_header(&mut result, debug_header.as_deref());
result result
} }
@@ -87,43 +46,14 @@ pub async fn browserconfig_xml(State(state): State<AppState>) -> Response {
.await .await
} }
pub async fn service_worker(State(state): State<AppState>, headers: HeaderMap) -> Response { pub async fn service_worker(State(state): State<AppState>) -> Response {
let tf = load_time_freeze_config_for_request(&state.config, &headers); serve_static_text_file(&state, "sw.js", "application/javascript; charset=utf-8").await
let (frozen, debug_header) = serve_frozen_file(&tf, |snap| {
Some((
snap.sw_js.as_slice(),
"application/javascript; charset=utf-8",
))
});
if let Some(resp) = frozen {
return resp;
}
let mut result =
serve_static_text_file(&state, "sw.js", "application/javascript; charset=utf-8").await;
set_time_freeze_header(&mut result, debug_header.as_deref());
result
} }
pub async fn service_worker_map(State(state): State<AppState>) -> Response { pub async fn service_worker_map(State(state): State<AppState>) -> Response {
serve_static_text_file(&state, "sw.js.map", "application/json").await serve_static_text_file(&state, "sw.js.map", "application/json").await
} }
fn set_time_freeze_header(response: &mut Response, value: Option<&str>) {
#[cfg(feature = "time-freeze")]
{
if let Some(v) = value
&& let Ok(hv) = HeaderValue::from_str(v)
{
response
.headers_mut()
.insert(axum::http::HeaderName::from_static("x-time-freeze"), hv);
}
}
#[cfg(not(feature = "time-freeze"))]
let _ = (response, value);
}
async fn runtime_static_cdn_endpoint(state: &AppState) -> Option<HttpEndpoint> { async fn runtime_static_cdn_endpoint(state: &AppState) -> Option<HttpEndpoint> {
if let Some(discovery) = state.discovery_cache.get().await if let Some(discovery) = state.discovery_cache.get().await
&& let Some(endpoint) = discovery_endpoint(&discovery, "static_cdn") && let Some(endpoint) = discovery_endpoint(&discovery, "static_cdn")
-498
View File
@@ -1,498 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::config::{AppProxyConfig, ReleaseChannel};
use axum::http::{HeaderMap, header};
use std::net::{IpAddr, Ipv4Addr};
const TIME_FREEZE_EXEMPT_CLIENT_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(188, 149, 230, 148));
const CANARY_USER_AGENT_PRODUCT: &str = "FluxerCanary/";
const CANARY_LIVE_WEB_CLIENT_MIN_VERSION: CalVer = CalVer {
year: 2026,
month_day: 614,
micro: 0,
};
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
struct CalVer {
year: u16,
month_day: u16,
micro: u32,
}
#[derive(Clone, Debug)]
pub struct FrozenSnapshot {
pub sha: String,
pub index_html: Vec<u8>,
pub sw_js: Vec<u8>,
pub version_json: Vec<u8>,
}
#[derive(Clone, Debug)]
pub struct TimeFreezeConfig {
pub enabled: bool,
pub release_channel: ReleaseChannel,
pub snapshot: Option<&'static FrozenSnapshot>,
pub client_exempt: bool,
}
#[derive(Clone, Debug)]
pub struct TimeFreezeDebug {
pub decision: TimeFreezeDecision,
pub snapshot_sha: Option<String>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TimeFreezeDecision {
Frozen,
Live,
NoSnapshot,
}
pub fn load_time_freeze_config_for_request(
app_config: &AppProxyConfig,
headers: &HeaderMap,
) -> TimeFreezeConfig {
let client_exempt = is_time_freeze_exempt_client(
headers,
app_config.trust_client_ip_header,
&app_config.client_ip_header_name,
);
load_time_freeze_config_for_index_source(
app_config.release_channel,
client_exempt,
app_config.index_upstream_url.is_some(),
app_config.time_freeze_enabled,
)
}
pub fn load_time_freeze_config(
release_channel: ReleaseChannel,
client_exempt: bool,
) -> TimeFreezeConfig {
load_time_freeze_config_for_index_source(release_channel, client_exempt, false, true)
}
pub fn load_time_freeze_config_for_index_source(
release_channel: ReleaseChannel,
client_exempt: bool,
has_index_upstream: bool,
enabled: bool,
) -> TimeFreezeConfig {
if !enabled || has_index_upstream {
return TimeFreezeConfig {
enabled,
release_channel,
snapshot: None,
client_exempt,
};
}
let snapshot: Option<&'static FrozenSnapshot> = load_frozen_snapshot(release_channel);
TimeFreezeConfig {
enabled,
release_channel,
snapshot,
client_exempt,
}
}
#[cfg(feature = "time-freeze")]
fn load_frozen_snapshot(release_channel: ReleaseChannel) -> Option<&'static FrozenSnapshot> {
match release_channel {
ReleaseChannel::Stable => Some(&crate::frozen_snapshots::STABLE_SNAPSHOT),
ReleaseChannel::Canary => None,
}
}
#[cfg(not(feature = "time-freeze"))]
fn load_frozen_snapshot(_release_channel: ReleaseChannel) -> Option<&'static FrozenSnapshot> {
None
}
pub fn should_serve_frozen(config: &TimeFreezeConfig) -> Option<&'static FrozenSnapshot> {
if !config.enabled {
return None;
}
if config.client_exempt {
return None;
}
config.snapshot
}
pub fn describe_decision(config: &TimeFreezeConfig) -> TimeFreezeDebug {
let decision = match &config.snapshot {
None => TimeFreezeDecision::NoSnapshot,
Some(_) if should_serve_frozen(config).is_some() => TimeFreezeDecision::Frozen,
Some(_) => TimeFreezeDecision::Live,
};
TimeFreezeDebug {
decision,
snapshot_sha: config.snapshot.as_ref().map(|s| s.sha.clone()),
}
}
#[cfg(feature = "time-freeze")]
pub fn time_freeze_debug_header(config: &TimeFreezeConfig) -> Option<String> {
if !config.enabled {
return None;
}
Some(format_debug_header(&describe_decision(config)))
}
#[cfg(not(feature = "time-freeze"))]
pub fn time_freeze_debug_header(_config: &TimeFreezeConfig) -> Option<String> {
None
}
pub fn is_time_freeze_exempt_client(
headers: &HeaderMap,
trust_client_ip_header: bool,
header_name: &str,
) -> bool {
extract_client_ip(headers, trust_client_ip_header, header_name)
.is_some_and(|ip| ip == TIME_FREEZE_EXEMPT_CLIENT_IP)
|| is_time_freeze_exempt_user_agent(headers)
}
fn is_time_freeze_exempt_user_agent(headers: &HeaderMap) -> bool {
let Some(user_agent) = headers
.get(header::USER_AGENT)
.and_then(|value| value.to_str().ok())
else {
return false;
};
user_agent
.match_indices(CANARY_USER_AGENT_PRODUCT)
.any(|(index, _)| {
let version_start = index + CANARY_USER_AGENT_PRODUCT.len();
let version = user_agent[version_start..]
.chars()
.take_while(|ch| ch.is_ascii_digit() || *ch == '.')
.collect::<String>();
parse_calver(&version)
.is_some_and(|version| version >= CANARY_LIVE_WEB_CLIENT_MIN_VERSION)
})
}
fn parse_calver(value: &str) -> Option<CalVer> {
let mut parts = value.split('.');
let year = parts.next()?;
let month_day = parts.next()?;
let micro = parts.next()?;
if parts.next().is_some()
|| year.len() != 4
|| !valid_nonzero_digits(year, 4)
|| !(month_day.len() == 3 || month_day.len() == 4)
|| !valid_nonzero_digits(month_day, month_day.len())
|| !valid_micro(micro)
{
return None;
}
let year = year.parse().ok()?;
let month_day_value: u16 = month_day.parse().ok()?;
let month = month_day_value / 100;
let day = month_day_value % 100;
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
let micro = micro.parse().ok()?;
let hour = micro / 10_000;
let minute = (micro / 100) % 100;
let second = micro % 100;
if hour > 23 || minute > 59 || second > 59 {
return None;
}
Some(CalVer {
year,
month_day: month_day_value,
micro,
})
}
fn valid_nonzero_digits(value: &str, max_len: usize) -> bool {
!value.is_empty()
&& value.len() <= max_len
&& !value.starts_with('0')
&& value.bytes().all(|byte| byte.is_ascii_digit())
}
fn valid_micro(value: &str) -> bool {
value == "0" || valid_nonzero_digits(value, 6)
}
fn extract_client_ip(
headers: &HeaderMap,
trust_client_ip_header: bool,
header_name: &str,
) -> Option<IpAddr> {
if !trust_client_ip_header {
return None;
}
let header_value = headers.get(header_name)?.to_str().ok()?;
let first_hop = header_value.split(',').next()?.trim();
parse_ip(first_hop)
}
fn parse_ip(value: &str) -> Option<IpAddr> {
if value.is_empty() {
return None;
}
if let Ok(ip) = value.parse() {
return Some(ip);
}
if let Some(stripped) = value
.strip_prefix('[')
.and_then(|value| value.split_once(']').map(|(host, _)| host))
{
return stripped.parse().ok();
}
if value.matches(':').count() == 1
&& let Some((host, _port)) = value.rsplit_once(':')
{
return host.parse().ok();
}
None
}
pub fn format_debug_header(debug: &TimeFreezeDebug) -> String {
let decision_str = match debug.decision {
TimeFreezeDecision::Frozen => "frozen",
TimeFreezeDecision::Live => "live",
TimeFreezeDecision::NoSnapshot => "no-snapshot",
};
match &debug.snapshot_sha {
Some(sha) => format!("{decision_str}; sha={sha}"),
None => decision_str.to_owned(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::{HeaderMap, HeaderValue};
use std::sync::LazyLock;
#[cfg(feature = "time-freeze")]
#[test]
fn stable_channel_has_frozen_snapshot() {
let config = load_time_freeze_config(ReleaseChannel::Stable, false);
assert!(
config.snapshot.is_some(),
"stable channel should have a frozen snapshot"
);
assert!(should_serve_frozen(&config).is_some());
}
#[cfg(feature = "time-freeze")]
#[test]
fn canary_channel_has_no_frozen_snapshot() {
let config = load_time_freeze_config(ReleaseChannel::Canary, false);
assert_eq!(config.release_channel, ReleaseChannel::Canary);
assert!(config.snapshot.is_none());
assert!(should_serve_frozen(&config).is_none());
let debug = describe_decision(&config);
assert_eq!(debug.decision, TimeFreezeDecision::NoSnapshot);
assert!(debug.snapshot_sha.is_none());
}
#[test]
fn index_upstream_disables_frozen_snapshot() {
let config =
load_time_freeze_config_for_index_source(ReleaseChannel::Canary, false, true, true);
assert!(config.enabled);
assert_eq!(config.release_channel, ReleaseChannel::Canary);
assert!(config.snapshot.is_none());
assert!(should_serve_frozen(&config).is_none());
let debug = describe_decision(&config);
assert_eq!(debug.decision, TimeFreezeDecision::NoSnapshot);
assert!(debug.snapshot_sha.is_none());
}
#[test]
fn disabled_time_freeze_has_no_snapshot_or_debug_header() {
let config =
load_time_freeze_config_for_index_source(ReleaseChannel::Stable, false, false, false);
assert!(!config.enabled);
assert!(config.snapshot.is_none());
assert!(should_serve_frozen(&config).is_none());
assert!(time_freeze_debug_header(&config).is_none());
}
#[cfg(not(feature = "time-freeze"))]
#[test]
fn no_feature_build_has_no_frozen_snapshot() {
let config = load_time_freeze_config(ReleaseChannel::Stable, false);
assert!(config.enabled);
assert!(config.snapshot.is_none());
assert!(should_serve_frozen(&config).is_none());
assert!(time_freeze_debug_header(&config).is_none());
}
#[cfg(feature = "time-freeze")]
#[test]
fn exempt_client_gets_live_content_even_with_snapshot() {
let config = load_time_freeze_config(ReleaseChannel::Stable, true);
assert!(config.snapshot.is_some());
assert!(should_serve_frozen(&config).is_none());
let debug = describe_decision(&config);
assert_eq!(debug.decision, TimeFreezeDecision::Live);
}
#[test]
fn describe_decision_no_snapshot() {
let config = TimeFreezeConfig {
enabled: true,
release_channel: ReleaseChannel::Stable,
snapshot: None,
client_exempt: false,
};
let debug = describe_decision(&config);
assert_eq!(debug.decision, TimeFreezeDecision::NoSnapshot);
assert!(debug.snapshot_sha.is_none());
}
static TEST_SNAPSHOT: LazyLock<FrozenSnapshot> = LazyLock::new(|| FrozenSnapshot {
sha: "deadbeef".to_owned(),
index_html: vec![],
sw_js: vec![],
version_json: vec![],
});
#[test]
fn describe_decision_frozen() {
let config = TimeFreezeConfig {
enabled: true,
release_channel: ReleaseChannel::Stable,
snapshot: Some(&TEST_SNAPSHOT),
client_exempt: false,
};
let debug = describe_decision(&config);
assert_eq!(debug.decision, TimeFreezeDecision::Frozen);
assert_eq!(debug.snapshot_sha.as_deref(), Some("deadbeef"));
}
#[test]
fn format_debug_header_no_snapshot() {
let debug = TimeFreezeDebug {
decision: TimeFreezeDecision::NoSnapshot,
snapshot_sha: None,
};
assert_eq!(format_debug_header(&debug), "no-snapshot");
}
#[test]
fn format_debug_header_frozen() {
let debug = TimeFreezeDebug {
decision: TimeFreezeDecision::Frozen,
snapshot_sha: Some("abc".to_owned()),
};
assert_eq!(format_debug_header(&debug), "frozen; sha=abc");
}
#[test]
fn format_debug_header_live() {
let debug = TimeFreezeDebug {
decision: TimeFreezeDecision::Live,
snapshot_sha: Some("def".to_owned()),
};
assert_eq!(format_debug_header(&debug), "live; sha=def");
}
#[test]
fn recognizes_exempt_x_real_ip_when_trusted() {
let mut headers = HeaderMap::new();
headers.insert("x-real-ip", HeaderValue::from_static("188.149.230.148"));
assert!(is_time_freeze_exempt_client(&headers, true, "x-real-ip"));
assert!(!is_time_freeze_exempt_client(&headers, false, "x-real-ip"));
}
#[test]
fn recognizes_exempt_first_forwarded_ip() {
let mut headers = HeaderMap::new();
headers.insert(
"x-forwarded-for",
HeaderValue::from_static("188.149.230.148, 10.0.0.4"),
);
assert!(is_time_freeze_exempt_client(
&headers,
true,
"x-forwarded-for"
));
}
#[test]
fn non_exempt_ip_stays_frozen() {
let mut headers = HeaderMap::new();
headers.insert("x-real-ip", HeaderValue::from_static("203.0.113.10"));
assert!(!is_time_freeze_exempt_client(&headers, true, "x-real-ip"));
}
#[test]
fn canary_user_agent_at_min_version_is_exempt() {
let mut headers = HeaderMap::new();
headers.insert(
header::USER_AGENT,
HeaderValue::from_static("FluxerCanary/2026.614.0"),
);
assert!(is_time_freeze_exempt_client(&headers, false, "x-real-ip"));
}
#[test]
fn canary_user_agent_newer_than_min_version_is_exempt() {
let mut headers = HeaderMap::new();
headers.insert(
header::USER_AGENT,
HeaderValue::from_static("Mozilla/5.0 FluxerCanary/2026.614.83512 Electron/99.0"),
);
assert!(is_time_freeze_exempt_client(&headers, false, "x-real-ip"));
}
#[test]
fn canary_user_agent_before_min_version_stays_frozen() {
let mut headers = HeaderMap::new();
headers.insert(
header::USER_AGENT,
HeaderValue::from_static("FluxerCanary/2026.613.235959"),
);
assert!(!is_time_freeze_exempt_client(&headers, false, "x-real-ip"));
}
#[test]
fn malformed_canary_user_agent_stays_frozen() {
let mut headers = HeaderMap::new();
headers.insert(
header::USER_AGENT,
HeaderValue::from_static("FluxerCanary/2026.614.not-a-calver"),
);
assert!(!is_time_freeze_exempt_client(&headers, false, "x-real-ip"));
}
#[test]
fn stable_user_agent_stays_frozen() {
let mut headers = HeaderMap::new();
headers.insert(
header::USER_AGENT,
HeaderValue::from_static("FluxerStable/2026.614.83512"),
);
assert!(!is_time_freeze_exempt_client(&headers, false, "x-real-ip"));
}
}
@@ -1559,10 +1559,6 @@ Default `/api`. The API path put in the page bootstrap.
No default. The absolute API URL in the bootstrap. Compose builds it from the public origin. No default. The absolute API URL in the bootstrap. Compose builds it from the public origin.
#### `FLUXER_APP_PROXY_TIME_FREEZE_ENABLED`
Defaults to the inverse of `FLUXER_SELF_HOSTED`. A frozen asset snapshot. Also compiled out of the self-hosted image.
## Admin settings ## Admin settings
`admin` serves the dashboard at `/admin`. All are optional. `admin` serves the dashboard at `/admin`. All are optional.
+1 -15
View File
@@ -19,7 +19,6 @@ use std::fs::{self, File};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
const DEFAULT_PUBLIC_ASSET_BASE_URL: &str = "https://fluxerstatic.com"; const DEFAULT_PUBLIC_ASSET_BASE_URL: &str = "https://fluxerstatic.com";
const DEFAULT_APP_PROXY_TIME_FREEZE_ENABLED: &str = "true";
const DEFAULT_APP_PROXY_BUNDLE_LOCAL_ASSETS: &str = "false"; const DEFAULT_APP_PROXY_BUNDLE_LOCAL_ASSETS: &str = "false";
const DEFAULT_STATIC_BUCKET: &str = "fluxer-static"; const DEFAULT_STATIC_BUCKET: &str = "fluxer-static";
const DEFAULT_S3_ENDPOINT: &str = "https://ewr1.vultrobjects.com"; const DEFAULT_S3_ENDPOINT: &str = "https://ewr1.vultrobjects.com";
@@ -178,11 +177,6 @@ fn bake_command(targets: &[&str]) -> Result<CommandSpec> {
.env("IMAGE_REPO", image_repo()?) .env("IMAGE_REPO", image_repo()?)
.env("BUILD_VERSION", build_version) .env("BUILD_VERSION", build_version)
.env("PUBLIC_ASSET_BASE_URL", public_asset_base_url) .env("PUBLIC_ASSET_BASE_URL", public_asset_base_url)
.env(
"FLUXER_APP_PROXY_TIME_FREEZE_ENABLED",
env::var("FLUXER_APP_PROXY_TIME_FREEZE_ENABLED")
.unwrap_or_else(|_| DEFAULT_APP_PROXY_TIME_FREEZE_ENABLED.to_string()),
)
.env( .env(
"BUNDLE_LOCAL_ASSETS", "BUNDLE_LOCAL_ASSETS",
env::var("BUNDLE_LOCAL_ASSETS") env::var("BUNDLE_LOCAL_ASSETS")
@@ -636,11 +630,7 @@ mod tests {
.args(["buildx", "bake", "-f", "fluxer_app_proxy/docker-bake.hcl"]) .args(["buildx", "bake", "-f", "fluxer_app_proxy/docker-bake.hcl"])
.env("IMAGE_REPO", "ghcr.io/example/fluxer-app-proxy") .env("IMAGE_REPO", "ghcr.io/example/fluxer-app-proxy")
.env("BUILD_VERSION", "2026.520.1") .env("BUILD_VERSION", "2026.520.1")
.env("PUBLIC_ASSET_BASE_URL", DEFAULT_PUBLIC_ASSET_BASE_URL) .env("PUBLIC_ASSET_BASE_URL", DEFAULT_PUBLIC_ASSET_BASE_URL);
.env(
"FLUXER_APP_PROXY_TIME_FREEZE_ENABLED",
DEFAULT_APP_PROXY_TIME_FREEZE_ENABLED,
);
assert_eq!(command.program, OsString::from("docker")); assert_eq!(command.program, OsString::from("docker"));
assert_eq!( assert_eq!(
@@ -656,10 +646,6 @@ mod tests {
OsString::from("BUILD_VERSION"), OsString::from("BUILD_VERSION"),
OsString::from("2026.520.1") OsString::from("2026.520.1")
))); )));
assert!(command.env.contains(&(
OsString::from("FLUXER_APP_PROXY_TIME_FREEZE_ENABLED"),
OsString::from(DEFAULT_APP_PROXY_TIME_FREEZE_ENABLED)
)));
} }
#[test] #[test]
@@ -1,13 +0,0 @@
[package]
name = "fluxer-content-update-frozen-snapshot"
version = "0.1.0"
edition.workspace = true
license.workspace = true
[dependencies]
anyhow = "1.0.104"
base64 = "0.23.1"
sha2 = "0.11.0"
[dev-dependencies]
tempfile = "3.27.0"
@@ -1,158 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use anyhow::{Context, Result, bail};
use base64::Engine;
use sha2::{Digest, Sha256};
use std::fs;
use std::path::Path;
pub fn generate_snapshot_source(static_dir: &Path) -> Result<String> {
if !static_dir.is_dir() {
bail!("{} is not a directory", static_dir.display());
}
let index_html = read_file(&static_dir.join("index.html"))?;
let sw_js = read_file(&static_dir.join("sw.js"))?;
let version_json = read_file(&static_dir.join("version.json"))?;
let sha = snapshot_sha(&index_html, &sw_js, &version_json);
let mut output = String::new();
output.push_str(&format!("const SNAPSHOT_SHA: &str = \"{sha}\";\n\n"));
output.push_str(&format!(
"// -- base64-encoded index.html ({} bytes) --\n",
index_html.len()
));
output.push_str(&format_const(
"STABLE_INDEX_HTML",
&base64_standard(&index_html),
));
output.push_str(&format!(
"// -- base64-encoded sw.js ({} bytes) --\n",
sw_js.len()
));
output.push_str(&format_const("STABLE_SW_JS", &base64_standard(&sw_js)));
output.push_str(&format!(
"// -- base64-encoded version.json ({} bytes) --\n",
version_json.len()
));
output.push_str(&format_const(
"STABLE_VERSION_JSON",
&base64_standard(&version_json),
));
Ok(output)
}
fn read_file(path: &Path) -> Result<Vec<u8>> {
if !path.exists() {
bail!("{} does not exist", path.display());
}
fs::read(path).with_context(|| format!("failed to read {}", path.display()))
}
fn snapshot_sha(index_html: &[u8], sw_js: &[u8], version_json: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(index_html);
hasher.update(sw_js);
hasher.update(version_json);
bytes_to_lower_hex(&hasher.finalize())
}
fn bytes_to_lower_hex(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0f) as usize] as char);
}
out
}
fn base64_standard(data: &[u8]) -> String {
base64::engine::general_purpose::STANDARD.encode(data)
}
fn break_lines(value: &str, width: usize) -> Vec<&str> {
value
.as_bytes()
.chunks(width)
.map(|chunk| std::str::from_utf8(chunk).expect("base64 should be ascii"))
.collect()
}
fn format_const(name: &str, data: &str) -> String {
let escaped = break_lines(data, 100).join("\\\n");
format!("const {name}: &str = \"\\\n{escaped}\\\n\";\n")
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn emits_snapshot_constants_with_combined_sha() {
let dir = tempdir().unwrap();
let index_html = b"<!doctype html><html><body>fixture</body></html>";
let sw_js = b"\"use strict\";\nself.addEventListener('install', () => {});\n";
let version_json = br#"{"sha":"fixture","buildNumber":7}"#;
fs::write(dir.path().join("index.html"), index_html).unwrap();
fs::write(dir.path().join("sw.js"), sw_js).unwrap();
fs::write(dir.path().join("version.json"), version_json).unwrap();
let output = generate_snapshot_source(dir.path()).unwrap();
let expected_sha = snapshot_sha(index_html, sw_js, version_json);
assert!(output.starts_with(&format!(
"const SNAPSHOT_SHA: &str = \"{expected_sha}\";\n\n"
)));
assert!(output.contains("// -- base64-encoded index.html (48 bytes) --"));
assert!(output.contains("// -- base64-encoded sw.js (58 bytes) --"));
assert!(output.contains("// -- base64-encoded version.json (33 bytes) --"));
assert!(output.contains(&format_const(
"STABLE_INDEX_HTML",
&base64_standard(index_html)
)));
assert!(output.contains(&format_const("STABLE_SW_JS", &base64_standard(sw_js))));
assert!(output.contains(&format_const(
"STABLE_VERSION_JSON",
&base64_standard(version_json)
)));
}
#[test]
fn wraps_base64_constants_at_100_columns() {
let data = "a".repeat(205);
let formatted = format_const("TEST", &data);
let lines = formatted.lines().collect::<Vec<_>>();
assert_eq!(lines[0], "const TEST: &str = \"\\");
assert_eq!(lines[1], format!("{}\\", "a".repeat(100)));
assert_eq!(lines[2], format!("{}\\", "a".repeat(100)));
assert_eq!(lines[3], format!("{}\\", "a".repeat(5)));
assert_eq!(lines[4], "\";");
}
#[test]
fn reports_non_directory_static_path() {
let dir = tempdir().unwrap();
let err = generate_snapshot_source(&dir.path().join("missing"))
.unwrap_err()
.to_string();
assert!(err.contains("missing is not a directory"));
}
#[test]
fn reports_missing_required_file() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("index.html"), b"index").unwrap();
let err = generate_snapshot_source(dir.path())
.unwrap_err()
.to_string();
assert!(err.contains("sw.js does not exist"));
}
}
@@ -1,36 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use anyhow::Result;
use fluxer_content_update_frozen_snapshot::generate_snapshot_source;
use std::env;
use std::path::PathBuf;
use std::process;
fn main() {
if let Err(err) = run() {
eprintln!("error: {err:#}");
process::exit(1);
}
}
fn run() -> Result<()> {
let mut args = env::args_os();
let program = args
.next()
.and_then(|value| PathBuf::from(value).file_name().map(|name| name.to_owned()))
.and_then(|name| name.into_string().ok())
.unwrap_or_else(|| "fluxer-content-update-frozen-snapshot".to_owned());
let Some(static_dir) = args.next() else {
eprintln!("usage: {program} <static_dir>");
process::exit(1);
};
if args.next().is_some() {
eprintln!("usage: {program} <static_dir>");
process::exit(1);
}
let output = generate_snapshot_source(&PathBuf::from(static_dir))?;
print!("{output}");
Ok(())
}