v0.30.1
This commit is contained in:
1 parent
f3dd34dae4
commit
f213d853e2
28 files changed
+5368
-4692
No files matched your search
+49
-5
@@ -24,6 +24,10 @@ pub struct RuntimeStatus {
|
||||
pub python: bool,
|
||||
pub pip: bool,
|
||||
pub certbot: bool,
|
||||
/// Versions of the executables actually selected for ACME, never a guessed system default.
|
||||
pub python_version: Option<String>,
|
||||
pub pip_version: Option<String>,
|
||||
pub certbot_version: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for RuntimeStatus {
|
||||
@@ -36,6 +40,9 @@ impl Default for RuntimeStatus {
|
||||
python: false,
|
||||
pip: false,
|
||||
certbot: false,
|
||||
python_version: None,
|
||||
pip_version: None,
|
||||
certbot_version: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,11 +75,22 @@ fn find_on_path(name: &str) -> Option<PathBuf> {
|
||||
if candidate.components().count() > 1 {
|
||||
return candidate.is_file().then(|| candidate.to_owned());
|
||||
}
|
||||
env::var_os("PATH")
|
||||
.into_iter()
|
||||
.flat_map(|value| env::split_paths(&value).collect::<Vec<_>>())
|
||||
.map(|directory| directory.join(name))
|
||||
.find(|path| path.is_file())
|
||||
for directory in env::var_os("PATH").into_iter().flat_map(|paths| env::split_paths(&paths).collect::<Vec<_>>()) {
|
||||
let path = directory.join(name);
|
||||
if path.is_file() {
|
||||
return Some(path);
|
||||
}
|
||||
// Windows PATH lookup uses PATHEXT; Path::is_file alone does not.
|
||||
// Python and Certbot are installed as .exe launchers on Windows.
|
||||
#[cfg(windows)]
|
||||
if path.extension().is_none() {
|
||||
let executable = path.with_extension("exe");
|
||||
if executable.is_file() {
|
||||
return Some(executable);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn python_candidates() -> impl Iterator<Item = PathBuf> {
|
||||
@@ -137,6 +155,8 @@ async fn command_success(
|
||||
}
|
||||
}
|
||||
|
||||
include!("runtime_versions.rs");
|
||||
|
||||
async fn managed_environment_valid(
|
||||
directory: &Path,
|
||||
shutdown: &tokio_util::sync::CancellationToken,
|
||||
@@ -203,12 +223,14 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
|
||||
python: false,
|
||||
pip: false,
|
||||
certbot: false,
|
||||
..RuntimeStatus::default()
|
||||
});
|
||||
return Err("acme.runtime_data_dir_missing".into());
|
||||
};
|
||||
let managed = data_dir.join("acme-python");
|
||||
let managed_directory = Some(managed.to_string_lossy().into_owned());
|
||||
if managed_environment_valid(&managed, &state.shutdown).await {
|
||||
let versions = runtime_versions(&venv_python(&managed), true, Some(&venv_certbot(&managed)));
|
||||
state.acme.set_runtime(RuntimeStatus {
|
||||
status: "ready",
|
||||
mode: "managed",
|
||||
@@ -217,6 +239,9 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
|
||||
python: true,
|
||||
pip: true,
|
||||
certbot: true,
|
||||
python_version: versions.python,
|
||||
pip_version: versions.pip,
|
||||
certbot_version: versions.certbot,
|
||||
});
|
||||
return Ok(RuntimeLaunch {
|
||||
python: venv_python(&managed),
|
||||
@@ -241,10 +266,16 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
|
||||
python: false,
|
||||
pip: false,
|
||||
certbot: distro_certbot_available,
|
||||
..RuntimeStatus::default()
|
||||
});
|
||||
return Err("acme.runtime_python_missing".into());
|
||||
};
|
||||
let system_pip = system_has_pip(&python, &state.shutdown).await;
|
||||
let system_versions = runtime_versions(
|
||||
&python,
|
||||
system_pip,
|
||||
distro_certbot.as_deref().filter(|_| distro_certbot_available),
|
||||
);
|
||||
state.acme.set_runtime(RuntimeStatus {
|
||||
status: "initializing",
|
||||
mode: "managed",
|
||||
@@ -253,10 +284,14 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
|
||||
python: true,
|
||||
pip: system_pip,
|
||||
certbot: distro_certbot_available,
|
||||
python_version: system_versions.python.clone(),
|
||||
pip_version: system_versions.pip.clone(),
|
||||
certbot_version: system_versions.certbot.clone(),
|
||||
});
|
||||
let build = build_managed_environment(&python, &managed, system_pip, &state.shutdown).await;
|
||||
if build.is_ok() {
|
||||
tracing::info!(path = %managed.display(), "managed ACME runtime ready");
|
||||
let versions = runtime_versions(&venv_python(&managed), true, Some(&venv_certbot(&managed)));
|
||||
state.acme.set_runtime(RuntimeStatus {
|
||||
status: "ready",
|
||||
mode: "managed",
|
||||
@@ -265,6 +300,9 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
|
||||
python: true,
|
||||
pip: true,
|
||||
certbot: true,
|
||||
python_version: versions.python,
|
||||
pip_version: versions.pip,
|
||||
certbot_version: versions.certbot,
|
||||
});
|
||||
return Ok(RuntimeLaunch {
|
||||
python: venv_python(&managed),
|
||||
@@ -282,6 +320,9 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
|
||||
python: true,
|
||||
pip: system_pip,
|
||||
certbot: true,
|
||||
python_version: system_versions.python,
|
||||
pip_version: system_versions.pip,
|
||||
certbot_version: system_versions.certbot,
|
||||
});
|
||||
return Ok(RuntimeLaunch {
|
||||
python,
|
||||
@@ -312,6 +353,9 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
|
||||
python: true,
|
||||
pip: system_pip,
|
||||
certbot: false,
|
||||
python_version: system_versions.python,
|
||||
pip_version: system_versions.pip,
|
||||
certbot_version: None,
|
||||
});
|
||||
Err(message.into())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
// Version information is diagnostic, not a health check. Read package metadata
|
||||
// for the *selected* runtime without launching Python, pip, Certbot or a shell.
|
||||
// A missing/ambiguous version stays None; do not guess an interpreter patch
|
||||
// version from an executable name or report a hard-coded Certbot pin.
|
||||
use std::io::Read;
|
||||
|
||||
const MAX_VERSION_METADATA_BYTES: u64 = 16 * 1024;
|
||||
const MAX_SITE_DIRECTORIES: usize = 64;
|
||||
const MAX_PACKAGES_PER_SITE: usize = 4096;
|
||||
|
||||
fn metadata_text(path: &Path) -> Option<String> {
|
||||
let mut bytes = Vec::new();
|
||||
std::fs::File::open(path)
|
||||
.ok()?
|
||||
.take(MAX_VERSION_METADATA_BYTES)
|
||||
.read_to_end(&mut bytes)
|
||||
.ok()?;
|
||||
String::from_utf8(bytes).ok()
|
||||
}
|
||||
|
||||
fn safe_version(raw: &str) -> Option<String> {
|
||||
let version = raw.trim();
|
||||
(version.len() <= 48
|
||||
&& version.bytes().next().is_some_and(|byte| byte.is_ascii_digit())
|
||||
&& version
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || b".+-_".contains(&byte)))
|
||||
.then(|| version.to_owned())
|
||||
}
|
||||
|
||||
fn python_environment_root(executable: &Path) -> Option<std::path::PathBuf> {
|
||||
let parent = executable.parent()?;
|
||||
let name = parent.file_name()?.to_str()?;
|
||||
let root = if name.eq_ignore_ascii_case("bin") || name.eq_ignore_ascii_case("scripts") {
|
||||
parent.parent()?
|
||||
} else {
|
||||
parent
|
||||
};
|
||||
Some(root.to_owned())
|
||||
}
|
||||
|
||||
fn interpreter_version(root: &Path) -> Option<String> {
|
||||
let content = metadata_text(&root.join("pyvenv.cfg"))?;
|
||||
// CPython venv writes `version = X.Y.Z`. `version_info` from virtualenv
|
||||
// is a fallback when its standard `version` field is not present.
|
||||
for key in ["version", "version_info"] {
|
||||
for line in content.lines() {
|
||||
if let Some((name, value)) = line.split_once('=') {
|
||||
if name.trim().eq_ignore_ascii_case(key) {
|
||||
if let Some(version) = safe_version(value) {
|
||||
return Some(version);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn python_minor(version: &str) -> Option<String> {
|
||||
let mut parts = version.split('.');
|
||||
let major = parts.next()?;
|
||||
let minor = parts.next()?;
|
||||
(major.bytes().all(|b| b.is_ascii_digit())
|
||||
&& minor.bytes().all(|b| b.is_ascii_digit()))
|
||||
.then(|| format!("python{major}.{minor}"))
|
||||
}
|
||||
|
||||
fn package_sites(root: &Path, python_version: Option<&str>) -> Vec<std::path::PathBuf> {
|
||||
let mut sites = vec![root.join("Lib/site-packages"), root.join("lib/python3/dist-packages")];
|
||||
for base in [root.join("lib"), root.join("lib64")] {
|
||||
if let Some(minor) = python_version.and_then(python_minor) {
|
||||
sites.push(base.join(&minor).join("site-packages"));
|
||||
sites.push(base.join(&minor).join("dist-packages"));
|
||||
} else if let Ok(entries) = std::fs::read_dir(&base) {
|
||||
// Python installations with no pyvenv.cfg may contain multiple
|
||||
// interpreter versions. Never choose between conflicting results.
|
||||
for entry in entries.take(MAX_SITE_DIRECTORIES).flatten() {
|
||||
let path = entry.path();
|
||||
if entry.file_name().to_string_lossy().starts_with("python3.") && path.is_dir() {
|
||||
sites.push(path.join("site-packages"));
|
||||
sites.push(path.join("dist-packages"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sites
|
||||
}
|
||||
|
||||
fn package_version(root: &Path, package: &str, python_version: Option<&str>) -> Option<String> {
|
||||
let prefix = format!("{package}-");
|
||||
let mut result: Option<String> = None;
|
||||
for site in package_sites(root, python_version) {
|
||||
let Ok(entries) = std::fs::read_dir(site) else { continue };
|
||||
for entry in entries.take(MAX_PACKAGES_PER_SITE).flatten() {
|
||||
let name = entry.file_name();
|
||||
let Some(dir_name) = name.to_str() else { continue };
|
||||
let Some(suffix) = dir_name.strip_prefix(prefix.as_str())
|
||||
.and_then(|name| name.strip_suffix(".dist-info")) else { continue };
|
||||
let Some(directory_version) = safe_version(suffix) else { continue };
|
||||
let Some(content) = metadata_text(&entry.path().join("METADATA")) else { continue };
|
||||
let mut reported_name = None;
|
||||
let mut reported_version = None;
|
||||
for line in content.lines() {
|
||||
if let Some(value) = line.strip_prefix("Name: ") {
|
||||
reported_name = Some(value.trim());
|
||||
} else if let Some(value) = line.strip_prefix("Version: ") {
|
||||
reported_version = safe_version(value);
|
||||
}
|
||||
if reported_name.is_some() && reported_version.is_some() { break; }
|
||||
}
|
||||
if !reported_name.is_some_and(|name| name.eq_ignore_ascii_case(package))
|
||||
|| reported_version.as_deref() != Some(directory_version.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if result.as_ref().is_some_and(|previous| previous != &directory_version) {
|
||||
return None;
|
||||
}
|
||||
result = Some(directory_version);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct RuntimeVersions {
|
||||
python: Option<String>,
|
||||
pip: Option<String>,
|
||||
certbot: Option<String>,
|
||||
}
|
||||
|
||||
fn runtime_versions(python: &Path, has_pip: bool, certbot: Option<&Path>) -> RuntimeVersions {
|
||||
let root = python_environment_root(python);
|
||||
let python_version = root.as_deref().and_then(interpreter_version);
|
||||
let pip_version = if has_pip {
|
||||
root.as_deref().and_then(|root| package_version(root, "pip", python_version.as_deref()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let certbot_version = certbot.and_then(|path| {
|
||||
let certbot_root = python_environment_root(path)?;
|
||||
let certbot_python_version = if root.as_deref() == Some(certbot_root.as_path()) {
|
||||
python_version.as_deref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
package_version(&certbot_root, "certbot", certbot_python_version)
|
||||
});
|
||||
RuntimeVersions { python: python_version, pip: pip_version, certbot: certbot_version }
|
||||
}
|
||||
|
||||
@@ -146,3 +146,65 @@ mod renewal_regressions {
|
||||
stop(app).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mod runtime_metadata_tests {
|
||||
use super::*;
|
||||
|
||||
fn write_package(site: &Path, package: &str, version: &str, metadata: &str) {
|
||||
let folder = site.join(format!("{package}-{version}.dist-info"));
|
||||
std::fs::create_dir_all(&folder).unwrap();
|
||||
std::fs::write(folder.join("METADATA"), metadata).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_python_pip_and_certbot_versions_use_metadata_only() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let root = temp.path();
|
||||
let python = venv_python(root);
|
||||
let certbot = venv_certbot(root);
|
||||
std::fs::create_dir_all(venv_bin(root)).unwrap();
|
||||
std::fs::write(&python, "").unwrap();
|
||||
std::fs::write(&certbot, "").unwrap();
|
||||
std::fs::write(root.join("pyvenv.cfg"), "home = /usr/bin\nversion = 3.13.5\n").unwrap();
|
||||
let site = if cfg!(windows) {
|
||||
root.join("Lib/site-packages")
|
||||
} else {
|
||||
root.join("lib/python3.13/site-packages")
|
||||
};
|
||||
write_package(&site, "pip", "25.2", "Metadata-Version: 2.3\nName: pip\nVersion: 25.2\n");
|
||||
write_package(&site, "certbot", "5.8.0", "Name: certbot\nVersion: 5.8.0\n");
|
||||
let versions = runtime_versions(&python, true, Some(&certbot));
|
||||
assert_eq!(versions.python.as_deref(), Some("3.13.5"));
|
||||
assert_eq!(versions.pip.as_deref(), Some("25.2"));
|
||||
assert_eq!(versions.certbot.as_deref(), Some("5.8.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absent_or_unverified_versions_are_not_invented() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let root = temp.path();
|
||||
let executable = root.join("bin/python3");
|
||||
std::fs::create_dir_all(root.join("bin")).unwrap();
|
||||
std::fs::write(&executable, "").unwrap();
|
||||
let site = root.join("lib/python3/dist-packages");
|
||||
write_package(&site, "pip", "99.0", "Name: other-package\nVersion: 99.0\n");
|
||||
let versions = runtime_versions(&executable, true, None);
|
||||
assert!(versions.python.is_none());
|
||||
assert!(versions.pip.is_none());
|
||||
assert!(versions.certbot.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conflicting_site_distributions_do_not_return_stale_versions() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let root = temp.path();
|
||||
let executable = root.join("bin/python3");
|
||||
std::fs::create_dir_all(root.join("bin")).unwrap();
|
||||
std::fs::write(&executable, "").unwrap();
|
||||
write_package(&root.join("lib/python3/dist-packages"), "pip", "23.0", "Name: pip\nVersion: 23.0\n");
|
||||
write_package(&root.join("lib/python3.13/site-packages"), "pip", "25.0", "Name: pip\nVersion: 25.0\n");
|
||||
assert!(runtime_versions(&executable, true, None).pip.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
+52
-5
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.30.0",
|
||||
"version": "0.30.1",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
|
||||
},
|
||||
"servers": [
|
||||
@@ -549,7 +549,19 @@
|
||||
"description": "Off by default. For these origin response HTTP statuses, replace the application response with a RustProxy error page (no-store). Does not change RustProxy-generated routing/security errors.",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"enum": [400, 401, 403, 404, 408, 413, 429, 500, 502, 503, 504]
|
||||
"enum": [
|
||||
400,
|
||||
401,
|
||||
403,
|
||||
404,
|
||||
408,
|
||||
413,
|
||||
429,
|
||||
500,
|
||||
502,
|
||||
503,
|
||||
504
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4737,7 +4749,7 @@
|
||||
},
|
||||
"certificate_runtime": {
|
||||
"type": "object",
|
||||
"description": "Python/pip/Certbot capability used by managed certificate automation.",
|
||||
"description": "ACME Python/pip/Certbot capability and versions read from local runtime/package metadata without executing diagnostic commands. Missing metadata returns null.",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
@@ -4771,6 +4783,21 @@
|
||||
},
|
||||
"certbot": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"python_version": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Version declared by the selected runtime (pyvenv.cfg for Python, dist-info/METADATA for pip and Certbot); null if missing or ambiguous."
|
||||
},
|
||||
"pip_version": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Version declared by the selected runtime (pyvenv.cfg for Python, dist-info/METADATA for pip and Certbot); null if missing or ambiguous."
|
||||
},
|
||||
"certbot_version": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Version declared by the selected runtime (pyvenv.cfg for Python, dist-info/METADATA for pip and Certbot); null if missing or ambiguous."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -4780,8 +4807,25 @@
|
||||
"managed_directory",
|
||||
"python",
|
||||
"pip",
|
||||
"certbot"
|
||||
"certbot",
|
||||
"python_version",
|
||||
"pip_version",
|
||||
"certbot_version"
|
||||
]
|
||||
},
|
||||
"architecture": {
|
||||
"type": "string",
|
||||
"description": "Architecture of the RustProxy executable."
|
||||
},
|
||||
"operating_system": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Host OS release name from the cross-platform sysinfo library, when supported."
|
||||
},
|
||||
"kernel_version": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Host kernel version from the cross-platform sysinfo library, when available."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -4789,7 +4833,10 @@
|
||||
"statistics_source",
|
||||
"data_directory",
|
||||
"database_path",
|
||||
"certificate_runtime"
|
||||
"certificate_runtime",
|
||||
"architecture",
|
||||
"operating_system",
|
||||
"kernel_version"
|
||||
]
|
||||
},
|
||||
"CertificateApiCredential": {
|
||||
|
||||
@@ -1,3 +1,33 @@
|
||||
// Reuse sysinfo's native OS APIs on Linux, BSD, macOS and Windows. The
|
||||
// diagnostics endpoint must never spawn system commands or parse platform files.
|
||||
// Host identification is cached because it changes only after a reboot.
|
||||
fn valid_host_text(value: String) -> Option<String> {
|
||||
let value = value.trim();
|
||||
(!value.is_empty() && value.len() <= 128 && !value.chars().any(char::is_control))
|
||||
.then(|| value.to_owned())
|
||||
}
|
||||
|
||||
fn os_release() -> Option<&'static str> {
|
||||
static VALUE: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
|
||||
VALUE.get_or_init(|| {
|
||||
sysinfo::System::long_os_version()
|
||||
.or_else(|| {
|
||||
let name = sysinfo::System::name()?;
|
||||
Some(match sysinfo::System::os_version() {
|
||||
Some(version) if !version.is_empty() => format!("{name} {version}"),
|
||||
_ => name,
|
||||
})
|
||||
})
|
||||
.and_then(valid_host_text)
|
||||
}).as_deref()
|
||||
}
|
||||
|
||||
fn kernel_release() -> Option<&'static str> {
|
||||
static VALUE: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
|
||||
VALUE.get_or_init(|| sysinfo::System::kernel_version().and_then(valid_host_text))
|
||||
.as_deref()
|
||||
}
|
||||
|
||||
fn status_payload(state: &App) -> Value {
|
||||
let snapshot = state.config.load_full();
|
||||
let listeners = json!({
|
||||
@@ -180,6 +210,9 @@ fn status_payload(state: &App) -> Value {
|
||||
});
|
||||
payload["runtime"] = json!({
|
||||
"platform": system.platform,
|
||||
"operating_system": os_release(),
|
||||
"kernel_version": kernel_release(),
|
||||
"architecture": std::env::consts::ARCH,
|
||||
"statistics_source": system.source,
|
||||
"data_directory": state.policy.data_dir.as_ref().map(|path| path.display().to_string()),
|
||||
"database_path": state.policy.data_dir.as_ref().map(|path| path.join("config.sqlite3").display().to_string()),
|
||||
|
||||
@@ -192,21 +192,8 @@ function enhanceEnabledControl(input, hintKey = "entityEnableHint") {
|
||||
}
|
||||
window.rustProxyEnableControl = enhanceEnabledControl;
|
||||
function action(label, fn) { const b = el("button", label, "quiet"); b.type = "button"; b.addEventListener("click", fn); return b; }
|
||||
function renderAboutRuntime(status = latestStatus) {
|
||||
const value = status?.runtime?.certificate_runtime || {};
|
||||
const availability = flag => flag === true ? t("runtimeAvailable") : flag === false ? t("runtimeUnavailable") : t("unavailable");
|
||||
const mode = value.status === "unavailable" ? t("certificateRuntimeUnavailable") : value.status === "ready" && value.mode === "managed" ? t("certificateRuntimeManaged") : value.status === "ready" && value.mode === "system" ? t("certificateRuntimeSystem") : t("certificateRuntimePreparing");
|
||||
const modeNode = $("about-acme-mode"), runtimeNode = $("about-python-runtime"), details = $("about-python-details");
|
||||
if (modeNode) modeNode.textContent = mode;
|
||||
if (runtimeNode) runtimeNode.textContent = `${t("runtimePython")}: ${availability(value.python)} · ${t("runtimePip")}: ${availability(value.pip)} · ${t("runtimeCertbot")}: ${availability(value.certbot)}`;
|
||||
if (details) {
|
||||
const parts = [value.message ? t(value.message) : ""];
|
||||
if (value.managed_directory) parts.push(`${t("certificateRuntimePath")}: ${value.managed_directory}`);
|
||||
details.textContent = parts.filter(Boolean).join(" · ");
|
||||
}
|
||||
}
|
||||
function renderStatus(s) {
|
||||
const firstStatus = latestStatus === null; latestStatus = s; renderAboutRuntime(s); window.rustProxySettings?.render(); if (firstStatus) renderSettings();
|
||||
const firstStatus = latestStatus === null; latestStatus = s; window.rustProxySettings?.render(); if (firstStatus) renderSettings();
|
||||
connected = true; $("connection-label").textContent = t("connected"); $("signal").classList.remove("status-error"); const m = s.metrics;
|
||||
$("metric-ws").textContent = m.websocket_active || 0;
|
||||
$("metric-ws-h1").textContent = m.websocket_h1_active || 0;
|
||||
@@ -384,7 +371,7 @@ function stopTunnelRefresh() { if (tunnelRefreshTimer) clearInterval(tunnelRefre
|
||||
function startTunnelRefresh() { stopTunnelRefresh(); tunnelRefreshTimer = setInterval(() => { if ($("tunnels-dialog")?.open) void loadTunnels(); }, 2000); }
|
||||
$("login-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; authMessage(""); try { await withFormSubmitState(form, event.submitter, async () => { const username = $("username").value.trim(); const password = $("password").value; if (setupRequired) { if (password !== $("password-confirm").value) throw fieldError("password-confirm", t("passwordMismatch")); await api("/api/auth/setup", { method: "POST", body: JSON.stringify({ token: $("setup-token").value.trim(), username, password }) }); setupRequired = false; syncAuthForm(); } const info = await api("/api/auth/login", { method: "POST", body: JSON.stringify({ username, password }) }); await activate(info); clearTimeout(toastTimer); hideToast(); }); } catch (e) { if (e.code === "auth.already_initialized") { setupRequired = false; syncAuthForm(); } $("password").value = ""; $("password-confirm").value = ""; authMessage(e.message); } });
|
||||
$("logout").addEventListener("click", async () => { try { await api("/api/auth/logout", { method: "POST" }); lock(); toast(t("locked")); } catch (e) { if (e.status === 401) lock(); toast(e.message, true); } });
|
||||
$("about-open").addEventListener("click", () => { renderAboutRuntime(); const dialog = $("about-dialog"); if (!dialog.open) dialog.showModal(); });
|
||||
$("about-open").addEventListener("click", () => { const dialog = $("about-dialog"); if (!dialog.open) dialog.showModal(); });
|
||||
$("about-close").addEventListener("click", () => $("about-dialog").close());
|
||||
const mobileMenuToggle = $("mobile-menu-toggle"), dashboardSidebar = document.querySelector(".sidebar");
|
||||
function setMobileMenu(open) {
|
||||
|
||||
+45
-29
@@ -1768,6 +1768,9 @@
|
||||
aria-controls="settings-page-administration" aria-selected="false"
|
||||
data-i18n="administration" data-settings-tab="administration"
|
||||
id="settings-tab-administration" role="tab" tabindex="-1" type="button"></button><button
|
||||
aria-controls="settings-page-system-info" aria-selected="false"
|
||||
data-i18n="systemInformation" data-settings-tab="system-info"
|
||||
id="settings-tab-system-info" role="tab" tabindex="-1" type="button"></button><button
|
||||
aria-controls="settings-page-certificate-api" aria-selected="false"
|
||||
data-i18n="certificateApiTitle" data-settings-tab="certificate-api"
|
||||
id="settings-tab-certificate-api" role="tab" tabindex="-1" type="button"></button><button
|
||||
@@ -2698,30 +2701,52 @@
|
||||
<div class="form-actions"><button class="primary" data-i18n="apply"
|
||||
type="submit">Validate, save and apply</button></div>
|
||||
</form>
|
||||
</div>
|
||||
<div aria-labelledby="settings-tab-system-info" class="settings-page"
|
||||
data-settings-page="system-info" hidden="" id="settings-page-system-info" role="tabpanel">
|
||||
<article class="card edit-form runtime-information-card">
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="runtimeInformation">Runtime information</h2><span
|
||||
class="pill">API</span>
|
||||
<h2 data-i18n="systemInformation">System information</h2><span class="pill">API</span>
|
||||
</div>
|
||||
<p class="fine" data-i18n="runtimeInformationHelp">Reported by the running RustProxy
|
||||
process and available only from the authenticated administrator status API.</p>
|
||||
<p class="fine" data-i18n="systemInformationHelp">Live, read-only information from the authenticated RustProxy status API. Unavailable metrics are not estimated.</p>
|
||||
<h3 data-i18n="systemHostHeading">Operating system and hardware</h3>
|
||||
<div class="runtime-information-grid">
|
||||
<div><span class="small-label" data-i18n="runtimePlatform">Platform</span><code
|
||||
id="runtime-platform">-</code></div>
|
||||
<div><span class="small-label" data-i18n="statisticsSource">Statistics
|
||||
source</span><code id="runtime-statistics-source">-</code></div>
|
||||
<div><span class="small-label" data-i18n="dataDirectory">Data directory</span><code
|
||||
id="runtime-data-directory">-</code></div>
|
||||
<div><span class="small-label" data-i18n="databaseLocation">Database</span><code
|
||||
id="runtime-database-path">-</code></div>
|
||||
<div><span class="small-label" data-i18n="runtimeAcmeMode">ACME mode</span><code
|
||||
id="runtime-acme-mode">-</code></div>
|
||||
<div><span class="small-label" data-i18n="runtimePython">Python</span><code
|
||||
id="runtime-python">-</code></div>
|
||||
<div><span class="small-label" data-i18n="runtimePip">pip</span><code
|
||||
id="runtime-pip">-</code></div>
|
||||
<div><span class="small-label" data-i18n="runtimeCertbot">Certbot</span><code
|
||||
id="runtime-certbot">-</code></div>
|
||||
<div><span class="small-label" data-i18n="runtimePlatform">Platform</span><code id="runtime-platform">-</code></div>
|
||||
<div><span class="small-label" data-i18n="systemOsVersion">Operating system</span><code id="runtime-operating-system">-</code></div>
|
||||
<div><span class="small-label" data-i18n="systemKernel">Kernel</span><code id="runtime-kernel-version">-</code></div>
|
||||
<div><span class="small-label" data-i18n="systemArchitecture">Architecture</span><code id="runtime-architecture">-</code></div>
|
||||
<div><span class="small-label" data-i18n="systemLogicalCpus">Logical CPUs</span><code id="runtime-logical-cpus">-</code></div>
|
||||
<div><span class="small-label" data-i18n="systemCpuLoad">CPU usage</span><code id="runtime-cpu-usage">-</code></div>
|
||||
<div><span class="small-label" data-i18n="systemMemory">RAM used / total</span><code id="runtime-system-memory">-</code></div>
|
||||
<div><span class="small-label" data-i18n="systemUptime">System uptime</span><code id="runtime-system-uptime">-</code></div>
|
||||
</div>
|
||||
<h3 data-i18n="systemProcessHeading">RustProxy process</h3>
|
||||
<div class="runtime-information-grid">
|
||||
<div><span class="small-label" data-i18n="systemAppVersion">RustProxy version</span><code id="runtime-version">-</code></div>
|
||||
<div><span class="small-label" data-i18n="systemProcessUptime">Process uptime</span><code id="runtime-process-uptime">-</code></div>
|
||||
<div><span class="small-label" data-i18n="systemProcessMemory">Process RAM (RSS)</span><code id="runtime-process-memory">-</code></div>
|
||||
<div><span class="small-label" data-i18n="systemProcessThreads">Process threads</span><code id="runtime-process-threads">-</code></div>
|
||||
<div><span class="small-label" data-i18n="systemOpenFds">Open file descriptors</span><code id="runtime-open-fds">-</code></div>
|
||||
<div><span class="small-label" data-i18n="statisticsSource">Statistics source</span><code id="runtime-statistics-source">-</code></div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card edit-form runtime-information-card">
|
||||
<div class="card-heading"><h2 data-i18n="systemCertificateHeading">ACME / Python environment</h2></div>
|
||||
<p class="fine" data-i18n="systemCertificateHelp">Versions refer to the interpreter and tools actually selected for certificate management, not an arbitrary Python installation.</p>
|
||||
<div class="runtime-information-grid">
|
||||
<div><span class="small-label" data-i18n="runtimeAcmeMode">ACME mode</span><code id="runtime-acme-mode">-</code></div>
|
||||
<div><span class="small-label" data-i18n="systemAcmeStatus">ACME status</span><code id="runtime-acme-status">-</code></div>
|
||||
<div><span class="small-label" data-i18n="runtimePython">Python</span><code id="runtime-python">-</code></div>
|
||||
<div><span class="small-label" data-i18n="runtimePip">pip</span><code id="runtime-pip">-</code></div>
|
||||
<div><span class="small-label" data-i18n="runtimeCertbot">Certbot</span><code id="runtime-certbot">-</code></div>
|
||||
<div><span class="small-label" data-i18n="certificateRuntimePath">Managed environment</span><code id="runtime-acme-directory">-</code></div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card edit-form runtime-information-card">
|
||||
<div class="card-heading"><h2 data-i18n="systemPathsHeading">Data locations</h2></div>
|
||||
<div class="runtime-information-grid">
|
||||
<div><span class="small-label" data-i18n="dataDirectory">Data directory</span><code id="runtime-data-directory">-</code></div>
|
||||
<div><span class="small-label" data-i18n="databaseLocation">Database</span><code id="runtime-database-path">-</code></div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
@@ -3659,18 +3684,9 @@ log_errors = On</code></pre>
|
||||
<dt data-i18n="aboutLicense">License</dt>
|
||||
<dd data-i18n="aboutLicenseValue">Open Source</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="runtimeAcmeMode">ACME mode</dt>
|
||||
<dd id="about-acme-mode">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="aboutPythonRuntime">Certificate Python runtime</dt>
|
||||
<dd id="about-python-runtime">-</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p data-i18n="aboutOpenSourceText">RustProxy is an open-source project. Its source code is publicly
|
||||
available for inspection, development and community contribution.</p>
|
||||
<p class="about-python-details" id="about-python-details"></p>
|
||||
<a class="outline about-repository" href="https://git.linuxiarz.pl/gru/RustProxy" rel="noopener noreferrer"
|
||||
target="_blank"><span data-i18n="aboutRepository">Official source
|
||||
repository</span><span>git.linuxiarz.pl/gru/RustProxy</span></a>
|
||||
|
||||
@@ -219,13 +219,44 @@ curl --fail --show-error --request POST \
|
||||
const syncAdminTls = () => { $("admin-certificate").disabled = !$("admin-tls").checked; }; syncAdminTls();
|
||||
$("admin-status").textContent = adminStatus?.restart_required ? t("restartPending") : adminStatus?.active?.tls ? `${t("administrationActiveHttps")}: ${adminStatus.active.listen}` : `${t("administrationActiveHttp")}: ${adminStatus?.active?.listen || latestStatus?.admin_listen || "-"}`;
|
||||
const runtime = latestStatus?.runtime || {};
|
||||
for (const [id, key] of [["runtime-platform", "platform"], ["runtime-statistics-source", "statistics_source"], ["runtime-data-directory", "data_directory"], ["runtime-database-path", "database_path"]]) {
|
||||
const node = $(id); if (node) node.textContent = runtime[key] || t("unavailable");
|
||||
}
|
||||
const system = latestStatus?.system || {};
|
||||
const hasMetric = value => value !== null && value !== undefined && Number.isFinite(Number(value));
|
||||
const humanCount = value => hasMetric(value) ? Number(value).toLocaleString(locale()) : t("unavailable");
|
||||
const humanBytes = value => hasMetric(value) ? bytes(Number(value)) : t("unavailable");
|
||||
const humanUptime = value => hasMetric(value) ? uptime(Number(value)) : t("unavailable");
|
||||
const percent = value => hasMetric(value) ? `${Number(value).toLocaleString(locale(), { maximumFractionDigits: 1 })}%` : t("unavailable");
|
||||
const ram = hasMetric(system.memory_used_bytes) || hasMetric(system.memory_total_bytes)
|
||||
? `${humanBytes(system.memory_used_bytes)} / ${humanBytes(system.memory_total_bytes)}` : t("unavailable");
|
||||
for (const [id, value] of [
|
||||
["runtime-version", latestStatus?.version],
|
||||
["runtime-platform", runtime.platform],
|
||||
["runtime-operating-system", runtime.operating_system],
|
||||
["runtime-kernel-version", runtime.kernel_version],
|
||||
["runtime-architecture", runtime.architecture],
|
||||
["runtime-logical-cpus", humanCount(system.logical_cpus)],
|
||||
["runtime-cpu-usage", percent(system.cpu_percent)],
|
||||
["runtime-system-memory", ram],
|
||||
["runtime-system-uptime", humanUptime(system.system_uptime_seconds)],
|
||||
["runtime-process-uptime", humanUptime(latestStatus?.uptime_seconds)],
|
||||
["runtime-process-memory", humanBytes(system.process_rss_bytes)],
|
||||
["runtime-process-threads", humanCount(system.process_threads)],
|
||||
["runtime-open-fds", humanCount(system.open_fds)],
|
||||
["runtime-statistics-source", runtime.statistics_source],
|
||||
["runtime-data-directory", runtime.data_directory],
|
||||
["runtime-database-path", runtime.database_path]
|
||||
]) { const node = $(id); if (node) node.textContent = value ?? t("unavailable"); }
|
||||
const certificateRuntime = runtime.certificate_runtime || {};
|
||||
const availability = flag => flag === true ? t("runtimeAvailable") : flag === false ? t("runtimeUnavailable") : t("unavailable");
|
||||
const modeLabel = certificateRuntime.status === "unavailable" ? t("certificateRuntimeUnavailable") : certificateRuntime.status === "ready" && certificateRuntime.mode === "managed" ? t("certificateRuntimeManaged") : certificateRuntime.status === "ready" && certificateRuntime.mode === "system" ? t("certificateRuntimeSystem") : t("certificateRuntimePreparing");
|
||||
for (const [id, text] of [["runtime-acme-mode", modeLabel], ["runtime-python", availability(certificateRuntime.python)], ["runtime-pip", availability(certificateRuntime.pip)], ["runtime-certbot", availability(certificateRuntime.certbot)]]) { const node = $(id); if (node) node.textContent = text; }
|
||||
const versioned = (flag, version) => version && flag === true ? `${availability(flag)} · ${version}` : availability(flag);
|
||||
for (const [id, value] of [
|
||||
["runtime-acme-mode", modeLabel],
|
||||
["runtime-acme-status", certificateRuntime.message ? t(certificateRuntime.message) : t("unavailable")],
|
||||
["runtime-python", versioned(certificateRuntime.python, certificateRuntime.python_version)],
|
||||
["runtime-pip", versioned(certificateRuntime.pip, certificateRuntime.pip_version)],
|
||||
["runtime-certbot", versioned(certificateRuntime.certbot, certificateRuntime.certbot_version)],
|
||||
["runtime-acme-directory", certificateRuntime.managed_directory]
|
||||
]) { const node = $(id); if (node) node.textContent = value || t("unavailable"); }
|
||||
|
||||
const policy = envelope.config.tls?.policy || { tls12: true, tls13: true, cipher_suites: [] };
|
||||
if (!settingsDirty.has("tls-policy-form")) { $("tls-policy-12").checked = policy.tls12 !== false; $("tls-policy-13").checked = policy.tls13 !== false; $("tls-policy-ciphers").value = (policy.cipher_suites || []).join("\n"); }
|
||||
|
||||
+12
-1
@@ -7606,6 +7606,11 @@ body[data-density="compact"] .page-footer {
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
/* The legend keys must use the same theme/preset palette as the SVG strokes. */
|
||||
.stat-chart .stat-chart-legend .legend-h1 i { background: var(--traffic-h1); }
|
||||
.stat-chart .stat-chart-legend .legend-h2 i { background: var(--traffic-h2); }
|
||||
.stat-chart .stat-chart-legend .legend-h3 i { background: var(--traffic-h3); }
|
||||
|
||||
.stat-chart .stat-chart-legend b {
|
||||
font-family: var(--mono);
|
||||
font-weight: 500;
|
||||
@@ -7809,7 +7814,13 @@ body[data-density="compact"] .page-footer {
|
||||
}
|
||||
|
||||
.runtime-information-card {
|
||||
margin-top: 12px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.runtime-information-card h3 {
|
||||
margin: 16px 0 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.runtime-information-grid {
|
||||
|
||||
Reference in new issue
Block a user