v0.27.1
This commit is contained in:
1 parent
3a2eb4fe79
commit
e0194fbaff
63 files changed
+1553
-379
No files matched your search
@@ -3,6 +3,7 @@
|
||||
//! travel through argv, logs or API responses.
|
||||
|
||||
include!("acme/model.rs");
|
||||
include!("acme/runtime_env.rs");
|
||||
include!("acme/providers.rs");
|
||||
include!("acme/jobs.rs");
|
||||
#[cfg(test)]
|
||||
|
||||
+6
-85
@@ -6,13 +6,8 @@ use base64::Engine as _;
|
||||
use http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
process::Stdio,
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{io::AsyncWriteExt, process::Command, sync::Semaphore};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::Semaphore;
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@@ -229,6 +224,8 @@ fn provider_credential_bytes(
|
||||
|
||||
pub struct Manager {
|
||||
slot: Arc<Semaphore>,
|
||||
runtime_slot: Arc<Semaphore>,
|
||||
runtime: Mutex<RuntimeStatus>,
|
||||
job: Mutex<Option<Job>>,
|
||||
renewals: Mutex<std::collections::BTreeMap<String, RenewalState>>,
|
||||
}
|
||||
@@ -236,6 +233,8 @@ impl Manager {
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
slot: Arc::new(Semaphore::new(1)),
|
||||
runtime_slot: Arc::new(Semaphore::new(1)),
|
||||
runtime: Mutex::new(RuntimeStatus::default()),
|
||||
job: Mutex::new(None),
|
||||
renewals: Mutex::new(Default::default()),
|
||||
})
|
||||
@@ -418,81 +417,3 @@ struct Output {
|
||||
cert_file: String,
|
||||
key_file: String,
|
||||
}
|
||||
|
||||
/// Kill the entire private process group if execution is cancelled/abandoned.
|
||||
/// Certbot inherits the group, so it cannot outlive a stopped RustProxy worker.
|
||||
struct ProcessGroup(Option<u32>);
|
||||
impl Drop for ProcessGroup {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(unix)]
|
||||
if let Some(id) = self.0 {
|
||||
// SAFETY: this is the process group created solely for this child.
|
||||
unsafe {
|
||||
libc::kill(-(id as i32), libc::SIGKILL);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
async fn worker(state: &App, directory: PathBuf, raw: Zeroizing<String>) -> Result<Value, String> {
|
||||
let mut command = Command::new("python3");
|
||||
command
|
||||
.arg("-I")
|
||||
.arg("-c")
|
||||
.arg(include_str!("../../scripts/acme_provider.py"))
|
||||
.arg("--cert-dir")
|
||||
.arg(&directory)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.kill_on_drop(true);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
command.as_std_mut().process_group(0);
|
||||
}
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.map_err(|_| "acme.python_not_available".to_owned())?;
|
||||
let mut group = ProcessGroup(child.id());
|
||||
let stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| "acme.worker_io".to_owned())?;
|
||||
let work = async move {
|
||||
let mut stdin = stdin;
|
||||
stdin
|
||||
.write_all(raw.as_bytes())
|
||||
.await
|
||||
.map_err(|_| "acme.worker_io".to_owned())?;
|
||||
stdin
|
||||
.shutdown()
|
||||
.await
|
||||
.map_err(|_| "acme.worker_io".to_owned())?;
|
||||
drop(stdin);
|
||||
drop(raw);
|
||||
child
|
||||
.wait_with_output()
|
||||
.await
|
||||
.map_err(|_| "acme.worker_io".to_owned())
|
||||
};
|
||||
let output = tokio::select! {
|
||||
_ = state.shutdown.cancelled() => return Err("acme.cancelled".into()),
|
||||
result = tokio::time::timeout(Duration::from_secs(2160), work) => result.map_err(|_| "acme.provider_timeout".to_owned())??,
|
||||
};
|
||||
group.0 = None; // the process has been reaped, never kill a reused PID
|
||||
if output.stdout.len() > 16384 {
|
||||
return Err("acme.invalid_worker_response".into());
|
||||
}
|
||||
let value: Value = serde_json::from_slice(&output.stdout)
|
||||
.map_err(|_| "acme.invalid_worker_response".to_owned())?;
|
||||
if !output.status.success() {
|
||||
let reason = value.get("error").and_then(Value::as_str).filter(|s| {
|
||||
s.starts_with("acme.")
|
||||
&& s.len() < 96
|
||||
&& s.bytes()
|
||||
.all(|c| c.is_ascii_lowercase() || c == b'_' || c == b'.')
|
||||
});
|
||||
return Err(reason.unwrap_or("acme.worker_failed").to_owned());
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
use std::{
|
||||
env,
|
||||
ffi::OsString,
|
||||
path::{Path, PathBuf},
|
||||
process::Stdio,
|
||||
sync::Weak,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{io::AsyncWriteExt, process::Command};
|
||||
|
||||
const CERTBOT_PACKAGES: &[&str] = &[
|
||||
"certbot==5.8.0",
|
||||
"certbot-dns-cloudflare==5.8.0",
|
||||
"certbot-dns-ovh==5.8.0",
|
||||
"certbot-dns-rfc2136==5.8.0",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct RuntimeStatus {
|
||||
pub status: &'static str,
|
||||
pub mode: &'static str,
|
||||
pub message: &'static str,
|
||||
pub managed_directory: Option<String>,
|
||||
pub python: bool,
|
||||
pub pip: bool,
|
||||
pub certbot: bool,
|
||||
}
|
||||
|
||||
impl Default for RuntimeStatus {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
status: "checking",
|
||||
mode: "none",
|
||||
message: "acme.runtime_checking",
|
||||
managed_directory: None,
|
||||
python: false,
|
||||
pip: false,
|
||||
certbot: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Manager {
|
||||
pub fn runtime_view(&self) -> RuntimeStatus {
|
||||
self.runtime
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner())
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn set_runtime(&self, value: RuntimeStatus) {
|
||||
*self
|
||||
.runtime
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner()) = value;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RuntimeLaunch {
|
||||
python: PathBuf,
|
||||
prepend_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn find_on_path(name: &str) -> Option<PathBuf> {
|
||||
let candidate = Path::new(name);
|
||||
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())
|
||||
}
|
||||
|
||||
fn system_python() -> Option<PathBuf> {
|
||||
find_on_path("python3").or_else(|| find_on_path("python"))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn venv_bin(directory: &Path) -> PathBuf {
|
||||
directory.join("Scripts")
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
fn venv_bin(directory: &Path) -> PathBuf {
|
||||
directory.join("bin")
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn venv_python(directory: &Path) -> PathBuf {
|
||||
venv_bin(directory).join("python.exe")
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
fn venv_python(directory: &Path) -> PathBuf {
|
||||
venv_bin(directory).join("python")
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn venv_certbot(directory: &Path) -> PathBuf {
|
||||
venv_bin(directory).join("certbot.exe")
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
fn venv_certbot(directory: &Path) -> PathBuf {
|
||||
venv_bin(directory).join("certbot")
|
||||
}
|
||||
|
||||
async fn command_success(
|
||||
mut command: Command,
|
||||
timeout: Duration,
|
||||
shutdown: &tokio_util::sync::CancellationToken,
|
||||
) -> bool {
|
||||
command
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.kill_on_drop(true);
|
||||
let Ok(mut child) = command.spawn() else {
|
||||
return false;
|
||||
};
|
||||
tokio::select! {
|
||||
_ = shutdown.cancelled() => false,
|
||||
result = tokio::time::timeout(timeout, child.wait()) => {
|
||||
matches!(result, Ok(Ok(status)) if status.success())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn managed_environment_valid(
|
||||
directory: &Path,
|
||||
shutdown: &tokio_util::sync::CancellationToken,
|
||||
) -> bool {
|
||||
let python = venv_python(directory);
|
||||
let certbot = venv_certbot(directory);
|
||||
if !python.is_file() || !certbot.is_file() {
|
||||
return false;
|
||||
}
|
||||
let mut imports = Command::new(&python);
|
||||
imports.args([
|
||||
"-I",
|
||||
"-c",
|
||||
"import certbot, certbot_dns_cloudflare, certbot_dns_ovh, certbot_dns_rfc2136",
|
||||
]);
|
||||
if !command_success(imports, Duration::from_secs(20), shutdown).await {
|
||||
return false;
|
||||
}
|
||||
let mut version = Command::new(&certbot);
|
||||
version.arg("--version");
|
||||
command_success(version, Duration::from_secs(20), shutdown).await
|
||||
}
|
||||
|
||||
async fn system_has_pip(
|
||||
python: &Path,
|
||||
shutdown: &tokio_util::sync::CancellationToken,
|
||||
) -> bool {
|
||||
let mut command = Command::new(python);
|
||||
command.args(["-m", "pip", "--version"]);
|
||||
command_success(command, Duration::from_secs(20), shutdown).await
|
||||
}
|
||||
|
||||
async fn create_virtual_environment(
|
||||
python: &Path,
|
||||
staging: &Path,
|
||||
system_pip: bool,
|
||||
shutdown: &tokio_util::sync::CancellationToken,
|
||||
) -> bool {
|
||||
let mut create = Command::new(python);
|
||||
create.arg("-m").arg("venv").arg(staging);
|
||||
if command_success(create, Duration::from_secs(180), shutdown).await {
|
||||
return true;
|
||||
}
|
||||
if !system_pip {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Some distributions split the stdlib venv module into a separate package.
|
||||
// Bootstrap virtualenv with the system pip into the RustProxy data directory
|
||||
// instead of modifying site-packages owned by the distribution.
|
||||
let bootstrap = staging.with_extension("virtualenv-bootstrap");
|
||||
let _ = tokio::fs::remove_dir_all(&bootstrap).await;
|
||||
let mut install = Command::new(python);
|
||||
install.args([
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--disable-pip-version-check",
|
||||
"--no-input",
|
||||
"--no-cache-dir",
|
||||
"--retries",
|
||||
"1",
|
||||
"--timeout",
|
||||
"15",
|
||||
"--target",
|
||||
]);
|
||||
install.arg(&bootstrap).arg("virtualenv>=20,<21");
|
||||
if !command_success(install, Duration::from_secs(300), shutdown).await {
|
||||
let _ = tokio::fs::remove_dir_all(&bootstrap).await;
|
||||
return false;
|
||||
}
|
||||
let mut create = Command::new(python);
|
||||
create
|
||||
.arg("-c")
|
||||
.arg("import runpy,sys; sys.path.insert(0,sys.argv[1]); sys.argv=['virtualenv',sys.argv[2]]; runpy.run_module('virtualenv',run_name='__main__')")
|
||||
.arg(&bootstrap)
|
||||
.arg(staging);
|
||||
let created = command_success(create, Duration::from_secs(180), shutdown).await;
|
||||
let _ = tokio::fs::remove_dir_all(&bootstrap).await;
|
||||
created
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum BuildFailure {
|
||||
Bootstrap,
|
||||
Install,
|
||||
}
|
||||
|
||||
async fn build_managed_environment(
|
||||
python: &Path,
|
||||
target: &Path,
|
||||
system_pip: bool,
|
||||
shutdown: &tokio_util::sync::CancellationToken,
|
||||
) -> Result<(), BuildFailure> {
|
||||
let staging = target.with_extension("installing");
|
||||
let _ = tokio::fs::remove_dir_all(&staging).await;
|
||||
|
||||
if !create_virtual_environment(python, &staging, system_pip, shutdown).await {
|
||||
let _ = tokio::fs::remove_dir_all(&staging).await;
|
||||
return Err(BuildFailure::Bootstrap);
|
||||
}
|
||||
|
||||
let staging_python = venv_python(&staging);
|
||||
let mut pip_check = Command::new(&staging_python);
|
||||
pip_check.args(["-m", "pip", "--version"]);
|
||||
if !command_success(pip_check, Duration::from_secs(30), shutdown).await {
|
||||
let mut ensurepip = Command::new(&staging_python);
|
||||
ensurepip.args(["-m", "ensurepip", "--upgrade"]);
|
||||
if !command_success(ensurepip, Duration::from_secs(120), shutdown).await {
|
||||
let _ = tokio::fs::remove_dir_all(&staging).await;
|
||||
return Err(BuildFailure::Bootstrap);
|
||||
}
|
||||
}
|
||||
|
||||
let mut pip = Command::new(&staging_python);
|
||||
pip.args([
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--disable-pip-version-check",
|
||||
"--no-input",
|
||||
"--no-cache-dir",
|
||||
"--retries",
|
||||
"1",
|
||||
"--timeout",
|
||||
"15",
|
||||
]);
|
||||
pip.args(CERTBOT_PACKAGES.iter().copied());
|
||||
if !command_success(pip, Duration::from_secs(300), shutdown).await
|
||||
|| !managed_environment_valid(&staging, shutdown).await
|
||||
{
|
||||
let _ = tokio::fs::remove_dir_all(&staging).await;
|
||||
return Err(BuildFailure::Install);
|
||||
}
|
||||
|
||||
if target.exists() && tokio::fs::remove_dir_all(target).await.is_err() {
|
||||
let _ = tokio::fs::remove_dir_all(&staging).await;
|
||||
return Err(BuildFailure::Install);
|
||||
}
|
||||
if tokio::fs::rename(&staging, target).await.is_err() {
|
||||
let _ = tokio::fs::remove_dir_all(&staging).await;
|
||||
return Err(BuildFailure::Install);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
|
||||
let _permit = state
|
||||
.acme
|
||||
.runtime_slot
|
||||
.clone()
|
||||
.acquire_owned()
|
||||
.await
|
||||
.map_err(|_| "acme.runtime_setup_failed".to_owned())?;
|
||||
let Some(data_dir) = state.policy.data_dir.as_deref() else {
|
||||
state.acme.set_runtime(RuntimeStatus {
|
||||
status: "unavailable",
|
||||
mode: "none",
|
||||
message: "acme.runtime_data_dir_missing",
|
||||
managed_directory: None,
|
||||
python: false,
|
||||
pip: false,
|
||||
certbot: false,
|
||||
});
|
||||
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 {
|
||||
state.acme.set_runtime(RuntimeStatus {
|
||||
status: "ready",
|
||||
mode: "managed",
|
||||
message: "acme.runtime_ready",
|
||||
managed_directory,
|
||||
python: true,
|
||||
pip: true,
|
||||
certbot: true,
|
||||
});
|
||||
return Ok(RuntimeLaunch {
|
||||
python: venv_python(&managed),
|
||||
prepend_path: Some(venv_bin(&managed)),
|
||||
});
|
||||
}
|
||||
|
||||
let distro_certbot = find_on_path("certbot");
|
||||
let distro_certbot_available = if let Some(certbot) = distro_certbot.as_deref() {
|
||||
let mut version = Command::new(certbot);
|
||||
version.arg("--version");
|
||||
command_success(version, Duration::from_secs(20), &state.shutdown).await
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let Some(python) = system_python() else {
|
||||
state.acme.set_runtime(RuntimeStatus {
|
||||
status: "unavailable",
|
||||
mode: "none",
|
||||
message: "acme.runtime_python_missing",
|
||||
managed_directory,
|
||||
python: false,
|
||||
pip: false,
|
||||
certbot: distro_certbot_available,
|
||||
});
|
||||
return Err("acme.runtime_python_missing".into());
|
||||
};
|
||||
let system_pip = system_has_pip(&python, &state.shutdown).await;
|
||||
state.acme.set_runtime(RuntimeStatus {
|
||||
status: "initializing",
|
||||
mode: "managed",
|
||||
message: "acme.runtime_initializing",
|
||||
managed_directory: managed_directory.clone(),
|
||||
python: true,
|
||||
pip: system_pip,
|
||||
certbot: distro_certbot_available,
|
||||
});
|
||||
|
||||
let build = build_managed_environment(&python, &managed, system_pip, &state.shutdown).await;
|
||||
if build.is_ok() {
|
||||
state.acme.set_runtime(RuntimeStatus {
|
||||
status: "ready",
|
||||
mode: "managed",
|
||||
message: "acme.runtime_ready",
|
||||
managed_directory,
|
||||
python: true,
|
||||
pip: true,
|
||||
certbot: true,
|
||||
});
|
||||
return Ok(RuntimeLaunch {
|
||||
python: venv_python(&managed),
|
||||
prepend_path: Some(venv_bin(&managed)),
|
||||
});
|
||||
}
|
||||
|
||||
if distro_certbot_available {
|
||||
state.acme.set_runtime(RuntimeStatus {
|
||||
status: "ready",
|
||||
mode: "system",
|
||||
message: "acme.runtime_fallback",
|
||||
managed_directory,
|
||||
python: true,
|
||||
pip: system_pip,
|
||||
certbot: true,
|
||||
});
|
||||
return Ok(RuntimeLaunch {
|
||||
python,
|
||||
prepend_path: None,
|
||||
});
|
||||
}
|
||||
|
||||
let message = if build == Err(BuildFailure::Bootstrap) && !system_pip {
|
||||
"acme.runtime_pip_missing"
|
||||
} else {
|
||||
"acme.runtime_setup_failed"
|
||||
};
|
||||
state.acme.set_runtime(RuntimeStatus {
|
||||
status: "unavailable",
|
||||
mode: "none",
|
||||
message,
|
||||
managed_directory,
|
||||
python: true,
|
||||
pip: system_pip,
|
||||
certbot: false,
|
||||
});
|
||||
Err(message.into())
|
||||
}
|
||||
|
||||
pub async fn runtime_setup_once(state: Weak<App>) {
|
||||
let Some(state) = state.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let _ = ensure_runtime(&state).await;
|
||||
}
|
||||
|
||||
/// Kill the entire private process group if execution is cancelled/abandoned.
|
||||
/// Certbot inherits the group, so it cannot outlive a stopped RustProxy worker.
|
||||
struct ProcessGroup(Option<u32>);
|
||||
impl Drop for ProcessGroup {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(unix)]
|
||||
if let Some(id) = self.0 {
|
||||
// SAFETY: this is the process group created solely for this child.
|
||||
unsafe {
|
||||
libc::kill(-(id as i32), libc::SIGKILL);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn merged_path(prepend: &Path) -> Result<OsString, String> {
|
||||
let mut values = vec![prepend.to_owned()];
|
||||
if let Some(current) = env::var_os("PATH") {
|
||||
values.extend(env::split_paths(¤t));
|
||||
}
|
||||
env::join_paths(values).map_err(|_| "acme.runtime_setup_failed".to_owned())
|
||||
}
|
||||
|
||||
async fn worker(state: &App, directory: PathBuf, raw: Zeroizing<String>) -> Result<Value, String> {
|
||||
let launch = ensure_runtime(state).await?;
|
||||
let mut command = Command::new(&launch.python);
|
||||
command
|
||||
.arg("-I")
|
||||
.arg("-c")
|
||||
.arg(include_str!("../../scripts/acme_provider.py"))
|
||||
.arg("--cert-dir")
|
||||
.arg(&directory)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.kill_on_drop(true);
|
||||
if let Some(prepend) = launch.prepend_path.as_deref() {
|
||||
command.env("PATH", merged_path(prepend)?);
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
command.as_std_mut().process_group(0);
|
||||
}
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.map_err(|_| "acme.python_not_available".to_owned())?;
|
||||
let mut group = ProcessGroup(child.id());
|
||||
let stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| "acme.worker_io".to_owned())?;
|
||||
let work = async move {
|
||||
let mut stdin = stdin;
|
||||
stdin
|
||||
.write_all(raw.as_bytes())
|
||||
.await
|
||||
.map_err(|_| "acme.worker_io".to_owned())?;
|
||||
stdin
|
||||
.shutdown()
|
||||
.await
|
||||
.map_err(|_| "acme.worker_io".to_owned())?;
|
||||
drop(stdin);
|
||||
drop(raw);
|
||||
child
|
||||
.wait_with_output()
|
||||
.await
|
||||
.map_err(|_| "acme.worker_io".to_owned())
|
||||
};
|
||||
let output = tokio::select! {
|
||||
_ = state.shutdown.cancelled() => return Err("acme.cancelled".into()),
|
||||
result = tokio::time::timeout(Duration::from_secs(2160), work) => result.map_err(|_| "acme.provider_timeout".to_owned())??,
|
||||
};
|
||||
group.0 = None;
|
||||
if output.stdout.len() > 16384 {
|
||||
return Err("acme.invalid_worker_response".into());
|
||||
}
|
||||
let value: Value = serde_json::from_slice(&output.stdout)
|
||||
.map_err(|_| "acme.invalid_worker_response".to_owned())?;
|
||||
if !output.status.success() {
|
||||
let reason = value.get("error").and_then(Value::as_str).filter(|value| {
|
||||
value.starts_with("acme.")
|
||||
&& value.len() < 96
|
||||
&& value
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_lowercase() || byte == b'_' || byte == b'.')
|
||||
});
|
||||
return Err(reason.unwrap_or("acme.worker_failed").to_owned());
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
@@ -374,6 +374,7 @@ async fn acme_status(State(state): State<Arc<App>>) -> Json<Value> {
|
||||
.unwrap_or_default();
|
||||
Json(json!({
|
||||
"job":state.acme.view(),
|
||||
"runtime":state.acme.runtime_view(),
|
||||
"providers":["http01","cloudflare","ovh","rfc2136"],
|
||||
"automatic_renewal":true,
|
||||
"interval_seconds":43200,
|
||||
|
||||
@@ -409,6 +409,7 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
|
||||
"/docs/V0.25.1_AUDIT.md"
|
||||
)),
|
||||
),
|
||||
reader_doc!("docs/V0.27.1_IMPLEMENTATION.md", "technical"),
|
||||
reader_doc!("docs/V0.27.0_IMPLEMENTATION.md", "technical"),
|
||||
reader_doc!("docs/V0.26.7_IMPLEMENTATION.md", "technical"),
|
||||
reader_doc!("docs/V0.26.6_IMPLEMENTATION.md", "technical"),
|
||||
|
||||
+113
-3
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.27.0",
|
||||
"version": "0.27.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": [
|
||||
@@ -3170,6 +3170,101 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Account-scoped sort direction for the Backends diagnostics table."
|
||||
},
|
||||
"vhost_list_sort": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"id",
|
||||
"hostname",
|
||||
"status",
|
||||
"listeners",
|
||||
"certificate",
|
||||
"backends"
|
||||
],
|
||||
"default": "id",
|
||||
"description": "Account-scoped sort key for the Vhosts list."
|
||||
},
|
||||
"vhost_list_descending": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Account-scoped sort direction for the Vhosts list."
|
||||
},
|
||||
"docker_vhost_sort": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"service",
|
||||
"hostname",
|
||||
"status",
|
||||
"backend",
|
||||
"scheme",
|
||||
"published"
|
||||
],
|
||||
"default": "service",
|
||||
"description": "Account-scoped sort key for Docker vhost service rows."
|
||||
},
|
||||
"docker_vhost_descending": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Account-scoped sort direction for Docker vhost service rows."
|
||||
},
|
||||
"redirect_list_sort": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"id",
|
||||
"name",
|
||||
"hostname",
|
||||
"status",
|
||||
"target",
|
||||
"rules",
|
||||
"listeners",
|
||||
"certificate"
|
||||
],
|
||||
"default": "id",
|
||||
"description": "Account-scoped sort key for the Redirect hosts list."
|
||||
},
|
||||
"redirect_list_descending": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Account-scoped sort direction for the Redirect hosts list."
|
||||
},
|
||||
"notfound_list_sort": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"id",
|
||||
"name",
|
||||
"hostname",
|
||||
"status",
|
||||
"mode",
|
||||
"listeners",
|
||||
"certificate"
|
||||
],
|
||||
"default": "id",
|
||||
"description": "Account-scoped sort key for the 404 hosts list."
|
||||
},
|
||||
"notfound_list_descending": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Account-scoped sort direction for the 404 hosts list."
|
||||
},
|
||||
"stream_list_sort": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"name",
|
||||
"id",
|
||||
"protocol",
|
||||
"listen",
|
||||
"upstream",
|
||||
"status",
|
||||
"maxConnections",
|
||||
"idleTimeout"
|
||||
],
|
||||
"default": "name",
|
||||
"description": "Account-scoped sort key for the Streams list."
|
||||
},
|
||||
"stream_list_descending": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Account-scoped sort direction for the Streams list."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -4372,13 +4467,28 @@
|
||||
"database_path": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"certificate_runtime": {
|
||||
"type": "object",
|
||||
"description": "Python/pip/Certbot capability used by managed certificate automation.",
|
||||
"properties": {
|
||||
"status": { "type": "string", "enum": ["checking", "initializing", "ready", "unavailable"] },
|
||||
"mode": { "type": "string", "enum": ["none", "managed", "system"] },
|
||||
"message": { "type": "string" },
|
||||
"managed_directory": { "type": "string", "nullable": true },
|
||||
"python": { "type": "boolean" },
|
||||
"pip": { "type": "boolean" },
|
||||
"certbot": { "type": "boolean" }
|
||||
},
|
||||
"required": ["status", "mode", "message", "managed_directory", "python", "pip", "certbot"]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"platform",
|
||||
"statistics_source",
|
||||
"data_directory",
|
||||
"database_path"
|
||||
"database_path",
|
||||
"certificate_runtime"
|
||||
]
|
||||
},
|
||||
"CertificateApiCredential": {
|
||||
@@ -5528,7 +5638,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Sets an HttpOnly SameSite=Strict session cookie. Browser sessions use the configured 7\u201330 day absolute lifetime. Use the panel sign-in page. No bearer token or URL token is accepted.",
|
||||
"description": "Sets an HttpOnly SameSite=Strict session cookie. Browser sessions use the configured 7–30 day absolute lifetime. Use the panel sign-in page. No bearer token or URL token is accepted.",
|
||||
"security": [],
|
||||
"parameters": [
|
||||
{
|
||||
|
||||
@@ -30,6 +30,51 @@ const BACKEND_TABLE_COLUMNS: &[&str] = &[
|
||||
"lastFailure",
|
||||
"lastRecovery",
|
||||
];
|
||||
const VHOST_LIST_SORT_COLUMNS: &[&str] = &[
|
||||
"id",
|
||||
"hostname",
|
||||
"status",
|
||||
"listeners",
|
||||
"certificate",
|
||||
"backends",
|
||||
];
|
||||
const DOCKER_VHOST_SORT_COLUMNS: &[&str] = &[
|
||||
"service",
|
||||
"hostname",
|
||||
"status",
|
||||
"backend",
|
||||
"scheme",
|
||||
"published",
|
||||
];
|
||||
const REDIRECT_LIST_SORT_COLUMNS: &[&str] = &[
|
||||
"id",
|
||||
"name",
|
||||
"hostname",
|
||||
"status",
|
||||
"target",
|
||||
"rules",
|
||||
"listeners",
|
||||
"certificate",
|
||||
];
|
||||
const NOTFOUND_LIST_SORT_COLUMNS: &[&str] = &[
|
||||
"id",
|
||||
"name",
|
||||
"hostname",
|
||||
"status",
|
||||
"mode",
|
||||
"listeners",
|
||||
"certificate",
|
||||
];
|
||||
const STREAM_LIST_SORT_COLUMNS: &[&str] = &[
|
||||
"name",
|
||||
"id",
|
||||
"protocol",
|
||||
"listen",
|
||||
"upstream",
|
||||
"status",
|
||||
"maxConnections",
|
||||
"idleTimeout",
|
||||
];
|
||||
const DASHBOARD_WIDGETS: &[&str] = &[
|
||||
"requests",
|
||||
"active",
|
||||
@@ -98,6 +143,21 @@ fn default_backend_table_columns() -> Vec<String> {
|
||||
fn default_backend_table_sort() -> String {
|
||||
"vhost".to_owned()
|
||||
}
|
||||
fn default_vhost_list_sort() -> String {
|
||||
"id".to_owned()
|
||||
}
|
||||
fn default_docker_vhost_sort() -> String {
|
||||
"service".to_owned()
|
||||
}
|
||||
fn default_redirect_list_sort() -> String {
|
||||
"id".to_owned()
|
||||
}
|
||||
fn default_notfound_list_sort() -> String {
|
||||
"id".to_owned()
|
||||
}
|
||||
fn default_stream_list_sort() -> String {
|
||||
"name".to_owned()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -131,6 +191,26 @@ struct AdminPreferences {
|
||||
backend_table_sort: String,
|
||||
#[serde(default)]
|
||||
backend_table_descending: bool,
|
||||
#[serde(default = "default_vhost_list_sort")]
|
||||
vhost_list_sort: String,
|
||||
#[serde(default)]
|
||||
vhost_list_descending: bool,
|
||||
#[serde(default = "default_docker_vhost_sort")]
|
||||
docker_vhost_sort: String,
|
||||
#[serde(default)]
|
||||
docker_vhost_descending: bool,
|
||||
#[serde(default = "default_redirect_list_sort")]
|
||||
redirect_list_sort: String,
|
||||
#[serde(default)]
|
||||
redirect_list_descending: bool,
|
||||
#[serde(default = "default_notfound_list_sort")]
|
||||
notfound_list_sort: String,
|
||||
#[serde(default)]
|
||||
notfound_list_descending: bool,
|
||||
#[serde(default = "default_stream_list_sort")]
|
||||
stream_list_sort: String,
|
||||
#[serde(default)]
|
||||
stream_list_descending: bool,
|
||||
}
|
||||
|
||||
impl Default for AdminPreferences {
|
||||
@@ -154,6 +234,16 @@ impl Default for AdminPreferences {
|
||||
backend_table_columns: default_backend_table_columns(),
|
||||
backend_table_sort: default_backend_table_sort(),
|
||||
backend_table_descending: false,
|
||||
vhost_list_sort: default_vhost_list_sort(),
|
||||
vhost_list_descending: false,
|
||||
docker_vhost_sort: default_docker_vhost_sort(),
|
||||
docker_vhost_descending: false,
|
||||
redirect_list_sort: default_redirect_list_sort(),
|
||||
redirect_list_descending: false,
|
||||
notfound_list_sort: default_notfound_list_sort(),
|
||||
notfound_list_descending: false,
|
||||
stream_list_sort: default_stream_list_sort(),
|
||||
stream_list_descending: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -323,6 +413,37 @@ fn validate_admin_preferences(preferences: &AdminPreferences) -> Result<(), ApiE
|
||||
"invalid backend table sort preference".into(),
|
||||
));
|
||||
}
|
||||
for (value, allowed, message) in [
|
||||
(
|
||||
&preferences.vhost_list_sort,
|
||||
VHOST_LIST_SORT_COLUMNS,
|
||||
"invalid vhost list sort preference",
|
||||
),
|
||||
(
|
||||
&preferences.docker_vhost_sort,
|
||||
DOCKER_VHOST_SORT_COLUMNS,
|
||||
"invalid Docker vhost sort preference",
|
||||
),
|
||||
(
|
||||
&preferences.redirect_list_sort,
|
||||
REDIRECT_LIST_SORT_COLUMNS,
|
||||
"invalid redirect list sort preference",
|
||||
),
|
||||
(
|
||||
&preferences.notfound_list_sort,
|
||||
NOTFOUND_LIST_SORT_COLUMNS,
|
||||
"invalid 404-host sort preference",
|
||||
),
|
||||
(
|
||||
&preferences.stream_list_sort,
|
||||
STREAM_LIST_SORT_COLUMNS,
|
||||
"invalid stream list sort preference",
|
||||
),
|
||||
] {
|
||||
if !allowed.contains(&value.as_str()) {
|
||||
return Err(ApiError(StatusCode::UNPROCESSABLE_ENTITY, message.into()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -182,7 +182,8 @@ fn status_payload(state: &App) -> Value {
|
||||
"platform": system.platform,
|
||||
"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())
|
||||
"database_path": state.policy.data_dir.as_ref().map(|path| path.join("config.sqlite3").display().to_string()),
|
||||
"certificate_runtime": state.acme.runtime_view()
|
||||
});
|
||||
payload["system"] = json!(system);
|
||||
payload["backend_pools"] = json!(snapshot.pools.iter().map(|p| p.view()).collect::<Vec<_>>());
|
||||
|
||||
+25
-8
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
let job = null, pending = false, loading = false, renewalLogs = {}, resetIssueFormJob = null;
|
||||
let job = null, pending = false, loading = false, renewalLogs = {}, resetIssueFormJob = null, runtimeAvailable = true;
|
||||
const picker = $("certificate-provider");
|
||||
const secrets = ["acme-secret", "acme-cf-token", "acme-ovh-application-key", "acme-ovh-application-secret", "acme-ovh-consumer-key"];
|
||||
const tabs = [...document.querySelectorAll("[data-certificate-tab]")];
|
||||
@@ -44,6 +44,9 @@
|
||||
clearSecrets();
|
||||
syncProvider();
|
||||
}
|
||||
function syncSubmitState() {
|
||||
$("acme-submit").disabled = pending || job?.status === "running" || !runtimeAvailable;
|
||||
}
|
||||
function syncProvider() {
|
||||
const provider = picker.value, acme = !["import", "existing"].includes(provider);
|
||||
$("certificate-acme-editor").hidden = !acme;
|
||||
@@ -56,6 +59,7 @@
|
||||
for (const input of node.querySelectorAll("input,select")) input.disabled = !visible;
|
||||
}
|
||||
$("acme-domains").placeholder = provider === "http01" ? "example.org, www.example.org" : "example.org, *.example.org";
|
||||
syncSubmitState();
|
||||
}
|
||||
function open(provider = "http01") {
|
||||
selectTab("generate"); picker.value = provider;
|
||||
@@ -67,6 +71,19 @@
|
||||
$("certificate-new").addEventListener("click", () => open());
|
||||
|
||||
const stateKey = value => ({ running: "certificateStateRunning", ready: "certificateStateReady", complete: "certificateStateComplete", failed: "certificateStateFailed" }[value?.status] || "certificateStateIdle");
|
||||
function renderRuntime(value) {
|
||||
const status = value?.status || "checking", mode = value?.mode || "none";
|
||||
runtimeAvailable = status !== "unavailable";
|
||||
$("acme-runtime-card").dataset.state = status;
|
||||
$("acme-runtime-message").textContent = t(value?.message || "acme.runtime_checking");
|
||||
$("acme-runtime-state").textContent = t(status === "unavailable" ? "certificateRuntimeUnavailable" : status === "ready" && mode === "managed" ? "certificateRuntimeManaged" : status === "ready" && mode === "system" ? "certificateRuntimeSystem" : "certificateRuntimePreparing");
|
||||
const path = value?.managed_directory;
|
||||
$("acme-runtime-path").hidden = !path;
|
||||
$("acme-runtime-path").textContent = path ? `${t("certificateRuntimePath")}: ${path}` : "";
|
||||
const availability = flag => t(flag === true ? "runtimeAvailable" : flag === false ? "runtimeUnavailable" : "certificateRuntimePreparing");
|
||||
$("acme-runtime-components").textContent = `${t("runtimePython")}: ${availability(value?.python)} · ${t("runtimePip")}: ${availability(value?.pip)} · ${t("runtimeCertbot")}: ${availability(value?.certbot)}`;
|
||||
syncSubmitState();
|
||||
}
|
||||
function renderOperation(cardId, messageId, stateId, logId, value, idleKey) {
|
||||
const card = $(cardId), message = $(messageId), state = $(stateId), log = $(logId);
|
||||
card.dataset.state = value?.status || "idle";
|
||||
@@ -83,7 +100,7 @@
|
||||
if (value.status === "ready") { resetIssueForm(); resetIssueFormJob = null; }
|
||||
else if (value.status === "failed") resetIssueFormJob = null;
|
||||
}
|
||||
$("acme-submit").disabled = pending || value?.status === "running";
|
||||
syncSubmitState();
|
||||
const issue = value?.kind === "issue" ? value : null;
|
||||
const renewal = value?.kind?.startsWith("renewal") ? value : null;
|
||||
renderOperation("acme-issue-status-card", "acme-job", "acme-issue-state", "acme-log-path", issue, "acmeIdle");
|
||||
@@ -131,7 +148,7 @@
|
||||
if (epoch === authEpoch) toast(error.message, true);
|
||||
} finally { node.disabled = false; }
|
||||
});
|
||||
node.disabled = job?.status === "running";
|
||||
node.disabled = job?.status === "running" || !runtimeAvailable;
|
||||
}
|
||||
entry.append(controls); list.append(entry);
|
||||
}
|
||||
@@ -142,14 +159,14 @@
|
||||
if (!authenticated || currentPanel !== "tls" || loading) return; loading = true; const epoch = authEpoch;
|
||||
try {
|
||||
const data = await api("/api/acme");
|
||||
if (authenticated && epoch === authEpoch) { render(data.job); renderRenewals(data.renewals, data.log_directories); }
|
||||
if (authenticated && epoch === authEpoch) { renderRuntime(data.runtime); render(data.job); renderRenewals(data.renewals, data.log_directories); }
|
||||
} catch (error) {
|
||||
if (epoch === authEpoch) { toast(error.message, true); if (error.status === 401) lock(); }
|
||||
} finally { loading = false; }
|
||||
}
|
||||
$("acme-environment").addEventListener("change", () => { $("acme-confirm-production").required = $("acme-environment").value === "production"; });
|
||||
$("acme-form").addEventListener("submit", async event => {
|
||||
event.preventDefault(); if (!authenticated || pending || job?.status === "running") return;
|
||||
event.preventDefault(); if (!authenticated || pending || job?.status === "running" || !runtimeAvailable) return;
|
||||
const provider = picker.value, input = {
|
||||
provider, certificate_id: $("acme-name").value.trim(), email: $("acme-email").value.trim(), domains: $("acme-domains").value.split(/[,\n]/).map(v => v.trim()).filter(Boolean), environment: $("acme-environment").value,
|
||||
agree_tos: $("acme-tos").checked, confirm_production: $("acme-confirm-production").checked
|
||||
@@ -168,7 +185,7 @@
|
||||
} catch (error) { if (epoch === authEpoch) toast(error.message, true); }
|
||||
finally {
|
||||
for (const key of ["key_secret", "api_token", "application_key", "application_secret", "consumer_key"]) delete input[key];
|
||||
clearSecrets(); pending = false; $("acme-submit").disabled = job?.status === "running";
|
||||
clearSecrets(); pending = false; syncSubmitState();
|
||||
}
|
||||
});
|
||||
$("acme-refresh").addEventListener("click", () => void refresh());
|
||||
@@ -187,8 +204,8 @@
|
||||
syncLocation,
|
||||
current: () => activeTab,
|
||||
reset() {
|
||||
job = null; renewalLogs = {}; resetIssueFormJob = null; $("renewal-list").replaceChildren(); resetIssueForm();
|
||||
render(null); selectTab("installed", false, false);
|
||||
job = null; renewalLogs = {}; resetIssueFormJob = null; runtimeAvailable = true; $("renewal-list").replaceChildren(); resetIssueForm();
|
||||
renderRuntime(null); render(null); selectTab("installed", false, false);
|
||||
}
|
||||
};
|
||||
window.rustProxyCertificates = { open, close, select: selectTab, reset() { clearSecrets(); picker.value = "http01"; syncProvider(); selectTab("installed", false, false); } };
|
||||
|
||||
@@ -192,8 +192,21 @@ 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; window.rustProxySettings?.render(); if (firstStatus) renderSettings();
|
||||
const firstStatus = latestStatus === null; latestStatus = s; renderAboutRuntime(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;
|
||||
@@ -334,7 +347,7 @@ function lock(notify = true) {
|
||||
if (navigating) { document.documentElement.classList.add("app-loading"); location.replace(destination); }
|
||||
}
|
||||
|
||||
async function activate(info) { pendingAuthDestination = null; authMessage(""); $("api-docs").hidden = false; $("logs-menu").hidden = false; $("logs-open").hidden = false; $("file-logs-open").hidden = false; authEpoch++; sessionInfo = info; csrf = info.csrf_token; authenticated = true; const preferences = await loadAdminPreferences(); window.rustProxyLogs?.applyPreferences(preferences); window.rustProxyFileLogs?.applyPreferences(preferences); window.rustProxyVhosts?.applyPreferences(preferences); await loadConfig(); $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("login").hidden = true; $("workspace").hidden = false; $("logout").hidden = false; renderSession(); await refresh(true); startLive(); document.documentElement.classList.remove("app-loading"); }
|
||||
async function activate(info) { pendingAuthDestination = null; authMessage(""); $("api-docs").hidden = false; $("logs-menu").hidden = false; $("logs-open").hidden = false; $("file-logs-open").hidden = false; authEpoch++; sessionInfo = info; csrf = info.csrf_token; authenticated = true; const preferences = await loadAdminPreferences(); window.rustProxyLogs?.applyPreferences(preferences); window.rustProxyFileLogs?.applyPreferences(preferences); window.rustProxyVhosts?.applyPreferences(preferences); window.rustProxyDocker?.applyPreferences(preferences); window.rustProxyRedirects?.applyPreferences(preferences); window.rustProxyNotFound?.applyPreferences(preferences); window.rustProxyStreams?.applyPreferences(preferences); await loadConfig(); $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("login").hidden = true; $("workspace").hidden = false; $("logout").hidden = false; renderSession(); await refresh(true); startLive(); document.documentElement.classList.remove("app-loading"); }
|
||||
async function loadAudit() { if (!authenticated) return; const epoch = authEpoch; try { const data = await api("/api/audit"); if (!authenticated || epoch !== authEpoch) return; const rows = data.entries.map(item => { const tr = el("tr"); for (const value of [formatDateTime(item.time * 1000), item.actor, item.action, item.detail]) { const td = el("td", value); td.title = value; tr.append(td); } return tr; }); $("audit-rows").replaceChildren(...rows); $("empty-audit").hidden = rows.length > 0; } catch (e) { toast(e.message, true); } }
|
||||
let tunnelRefreshTimer = null;
|
||||
function tunnelMessage(value = "", error = false) { const node = $("tunnels-status"); if (!node) return; node.textContent = value; node.classList.toggle("error", !!error); }
|
||||
@@ -371,7 +384,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", () => { const dialog = $("about-dialog"); if (!dialog.open) dialog.showModal(); });
|
||||
$("about-open").addEventListener("click", () => { renderAboutRuntime(); 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) {
|
||||
|
||||
@@ -136,6 +136,10 @@ function applyAdminPreferences(value, cache = true) {
|
||||
};
|
||||
adminPreferences = normalized;
|
||||
window.rustProxyVhosts?.applyPreferences(normalized);
|
||||
window.rustProxyDocker?.applyPreferences(normalized);
|
||||
window.rustProxyRedirects?.applyPreferences(normalized);
|
||||
window.rustProxyNotFound?.applyPreferences(normalized);
|
||||
window.rustProxyStreams?.applyPreferences(normalized);
|
||||
if (languagePacks.size) translate();
|
||||
refreshAppearanceUi(); return normalized;
|
||||
}
|
||||
|
||||
+23
-3
@@ -1,6 +1,11 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
let observations = [], pools = [], poolRoutes = [], poolIndex = new Map(), showHidden = false;
|
||||
const sortColumns = ["service", "hostname", "status", "backend", "scheme", "published"];
|
||||
let observations = [], pools = [], poolRoutes = [], poolIndex = new Map(), showHidden = false, listPrefs = { sort: "service", descending: false };
|
||||
function normalizeListPrefs(preferences) { return { sort: sortColumns.includes(preferences?.docker_vhost_sort) ? preferences.docker_vhost_sort : "service", descending: preferences?.docker_vhost_descending === true }; }
|
||||
function syncSortDirection() { const control = $("docker-vhost-sort-direction"); if (!control) return; control.textContent = listPrefs.descending ? "↓" : "↑"; control.setAttribute("aria-label", t(listPrefs.descending ? "backendSortDescending" : "backendSortAscending")); }
|
||||
function applyPreferences(preferences) { listPrefs = normalizeListPrefs(preferences); if ($("docker-vhost-sort")) $("docker-vhost-sort").value = listPrefs.sort; syncSortDirection(); applyViewFilters(); }
|
||||
async function saveListPrefs() { if (!authenticated) return; try { listPrefs = normalizeListPrefs(await window.rustProxyPreferences.update({ docker_vhost_sort: listPrefs.sort, docker_vhost_descending: listPrefs.descending })); if ($("docker-vhost-sort")) $("docker-vhost-sort").value = listPrefs.sort; syncSortDirection(); applyViewFilters(); } catch (error) { if (error.status !== 401) toast(error.message, true); } }
|
||||
const LIVE_KEY = "rustproxy.docker.live-view";
|
||||
const HIDDEN_KEY = "rustproxy.docker.hidden-vhosts";
|
||||
const cfg = () => envelope?.config?.docker_agents || { agents: [] };
|
||||
@@ -269,11 +274,17 @@
|
||||
|
||||
function assignmentRow(agent, inventory, service, assignment, stale = false) {
|
||||
const key = assignment?.service_key || service.key, row = el("article", undefined, "docker-service-row"); row.dataset.key = key; row.dataset.viewKey = viewKey(agent.id, key);
|
||||
const runtime = runtimeFor(agent.id, key), member = runtime?.members?.[0], technicalName = service?.technical_name || assignment?.service_key || "";
|
||||
row.dockerSort = {
|
||||
service: technicalName.toLowerCase(), hostname: String(legacyHostname(assignment) || technicalName).toLowerCase(),
|
||||
status: stale ? 4 : assignment?.enabled === false || !assignment ? 3 : member?.status === "up" ? 0 : member?.status === "down" ? 1 : 2,
|
||||
backend: String(assignment?.target_ip || agent.default_target_ip || inventory?.public_ip || "").toLowerCase(), scheme: String(assignment?.scheme || "http"), published: assignment && assignment.enabled !== false ? 0 : 1,
|
||||
};
|
||||
const top = el("div", undefined, "docker-service-head"), publish = input("", "checkbox", "docker-publish"); publish.checked = !!assignment && assignment.enabled !== false;
|
||||
const publishLabel = el("label", undefined, "check docker-publish-field"); publishLabel.append(publish, el("span", t("dockerPublish")));
|
||||
const meta = el("div", undefined, "docker-service-meta"), technical = service?.technical_name || assignment?.service_key || "-", bindings = serviceBindings(service).map(binding => `${binding.bind_ip || "*"}:${binding.public_port}`).join(", ");
|
||||
meta.append(el("b", technical), el("span", stale ? t("dockerServiceMissing") : `${service.container_name} · ${service.image}`), el("span", stale ? assignment.service_key : `${service.private_port}/tcp → ${bindings || "-"}`));
|
||||
const runtime = runtimeFor(agent.id, key), visibility = el("button", "", "quiet docker-view-toggle"); visibility.type = "button";
|
||||
const visibility = el("button", "", "quiet docker-view-toggle"); visibility.type = "button";
|
||||
const syncVisibility = () => { visibility.textContent = t(hiddenVhosts.has(row.dataset.viewKey) ? "dockerUnhideVhost" : "dockerHideVhost"); }; syncVisibility(); visibility.addEventListener("click", () => { const key = row.dataset.viewKey; if (hiddenVhosts.has(key)) hiddenVhosts.delete(key); else hiddenVhosts.add(key); saveHidden(); syncVisibility(); applyViewFilters(); });
|
||||
const openApp = el("a", t("dockerOpenApp"), "outline docker-open-app"); openApp.target = "_blank"; openApp.rel = "noopener noreferrer";
|
||||
const syncOpenApp = hostname => { const url = proxiedUrl(agent, hostname); openApp.href = url || "#"; openApp.hidden = !publish.checked || !url; openApp.title = url || ""; };
|
||||
@@ -326,9 +337,16 @@
|
||||
}
|
||||
applyViewFilters();
|
||||
}
|
||||
function sortServiceRows(card) {
|
||||
const holder = card.querySelector(".docker-service-list"); if (!holder) return;
|
||||
const rows = [...holder.querySelectorAll(":scope > .docker-service-row")];
|
||||
rows.sort((left, right) => { const a = left.dockerSort?.[listPrefs.sort] ?? "", b = right.dockerSort?.[listPrefs.sort] ?? ""; const result = typeof a === "number" && typeof b === "number" ? a - b : String(a).localeCompare(String(b), locale(), { numeric: true, sensitivity: "base" }); return listPrefs.descending ? -result : result; });
|
||||
for (const row of rows) holder.append(row);
|
||||
}
|
||||
function applyViewFilters() {
|
||||
const query = ($("docker-search")?.value || "").trim().toLowerCase();
|
||||
for (const card of document.querySelectorAll("#docker-discovery-list .docker-agent-card")) {
|
||||
sortServiceRows(card);
|
||||
const agentMatch = !!query && (card.dataset.searchText || "").includes(query);
|
||||
const rows = [...card.querySelectorAll(".docker-service-row")]; let visible = 0;
|
||||
for (const row of rows) {
|
||||
@@ -355,10 +373,12 @@
|
||||
$("docker-live-refresh")?.addEventListener("change", event => { localStorage.setItem(LIVE_KEY, event.target.checked ? "1" : "0"); if (event.target.checked) void refresh(false); });
|
||||
$("docker-settings-open")?.addEventListener("click", () => { setPanel("settings"); window.rustProxySettings?.select("docker-agents"); });
|
||||
$("docker-search")?.addEventListener("input", applyViewFilters);
|
||||
$("docker-vhost-sort")?.addEventListener("change", event => { listPrefs.sort = event.target.value; void saveListPrefs(); applyViewFilters(); });
|
||||
$("docker-vhost-sort-direction")?.addEventListener("click", () => { listPrefs.descending = !listPrefs.descending; syncSortDirection(); void saveListPrefs(); applyViewFilters(); });
|
||||
$("docker-show-hidden")?.addEventListener("click", () => { showHidden = !showHidden; applyViewFilters(); });
|
||||
$("docker-agent-add")?.addEventListener("click", () => { const target = $("docker-agent-settings-list"); target.querySelector(".empty")?.remove(); target.append(agentSettingsRow()); setSettingsDirty("docker-agents-form"); });
|
||||
$("docker-agents-form")?.addEventListener("input", () => setSettingsDirty("docker-agents-form")); $("docker-agents-form")?.addEventListener("change", () => setSettingsDirty("docker-agents-form"));
|
||||
$("docker-agents-form")?.addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return; try { await withFormSubmitState(form, event.submitter, async () => { if (await saveSection("docker-agents", collectSettings())) { setSettingsDirty("docker-agents-form", false); await refresh(true); } }); } catch (error) { toast(error.message, true); } });
|
||||
setInterval(() => { if (authenticated && liveEnabled() && typeof currentPanel !== "undefined" && currentPanel === "docker-vhosts" && canLiveRender()) void refresh(false); }, 5000);
|
||||
window.rustProxyDocker = { render() { syncLiveSwitch(); renderSettings(); renderDiscovery(); }, refresh, reset() { observations = []; pools = []; poolRoutes = []; poolIndex = new Map(); showHidden = false; hiddenVhosts = loadHidden(); if ($("docker-search")) $("docker-search").value = ""; $("docker-discovery-list")?.replaceChildren(); $("docker-agent-settings-list")?.replaceChildren(); syncLiveSwitch(); applyViewFilters(); } };
|
||||
window.rustProxyDocker = { render() { syncLiveSwitch(); renderSettings(); renderDiscovery(); }, refresh, applyPreferences, reset() { observations = []; pools = []; poolRoutes = []; poolIndex = new Map(); showHidden = false; listPrefs = { sort: "service", descending: false }; hiddenVhosts = loadHidden(); if ($("docker-search")) $("docker-search").value = ""; if ($("docker-vhost-sort")) $("docker-vhost-sort").value = listPrefs.sort; syncSortDirection(); $("docker-discovery-list")?.replaceChildren(); $("docker-agent-settings-list")?.replaceChildren(); syncLiveSwitch(); applyViewFilters(); } };
|
||||
})();
|
||||
@@ -2,7 +2,12 @@
|
||||
(() => {
|
||||
const form = $("notfound-form"), dialog = $("notfound-dialog"), list = $("notfound-list");
|
||||
if (!form || !dialog || !list) return;
|
||||
let editing = null, revision = null, dirty = false, readBindings = () => [], readHosts = () => [];
|
||||
const sortColumns = ["id", "name", "hostname", "status", "mode", "listeners", "certificate"];
|
||||
let editing = null, revision = null, dirty = false, readBindings = () => [], readHosts = () => [], listPrefs = { sort: "id", descending: false };
|
||||
function normalizeListPrefs(preferences) { return { sort: sortColumns.includes(preferences?.notfound_list_sort) ? preferences.notfound_list_sort : "id", descending: preferences?.notfound_list_descending === true }; }
|
||||
function syncSortDirection() { const control = $("notfound-sort-direction"); if (!control) return; control.textContent = listPrefs.descending ? "↓" : "↑"; control.setAttribute("aria-label", t(listPrefs.descending ? "backendSortDescending" : "backendSortAscending")); }
|
||||
function applyPreferences(preferences) { listPrefs = normalizeListPrefs(preferences); if ($("notfound-sort")) $("notfound-sort").value = listPrefs.sort; syncSortDirection(); render(); }
|
||||
async function saveListPrefs() { if (!authenticated) return; try { listPrefs = normalizeListPrefs(await window.rustProxyPreferences.update({ notfound_list_sort: listPrefs.sort, notfound_list_descending: listPrefs.descending })); if ($("notfound-sort")) $("notfound-sort").value = listPrefs.sort; syncSortDirection(); } catch (error) { if (error.status !== 401) toast(error.message, true); } }
|
||||
const syncEnabledControl = window.rustProxyEnableControl($("notfound-enabled"));
|
||||
const ERROR_STATUSES = [400, 401, 403, 404, 408, 413, 429, 500, 502, 503, 504];
|
||||
const BUILTIN_STATUSES = [401, 403, 404, 429, 503, 504];
|
||||
@@ -70,6 +75,8 @@
|
||||
function render() {
|
||||
if (!envelope) return; const query = ($("notfound-search").value || "").trim().toLowerCase();
|
||||
const entries = (envelope.config.not_found_hosts || []).filter(item => !query || [item.id, item.name, ...(item.frontend?.hosts || [])].join(" ").toLowerCase().includes(query));
|
||||
const sortValue = item => ({ id: String(item.id || "").toLowerCase(), name: String(item.name || item.id || "").toLowerCase(), hostname: String(item.frontend?.hosts?.[0] || "").toLowerCase(), status: Number(item.page?.status ?? 404), mode: String(item.page?.kind || "welcome"), listeners: (item.frontend?.listener_ids || []).join(" ").toLowerCase(), certificate: String(item.certificate_id || "").toLowerCase() }[listPrefs.sort]);
|
||||
entries.sort((left, right) => { const a = sortValue(left), b = sortValue(right); const result = typeof a === "number" && typeof b === "number" ? a - b : String(a).localeCompare(String(b), locale(), { numeric: true, sensitivity: "base" }); return listPrefs.descending ? -result : result; });
|
||||
list.replaceChildren(...entries.map(item => {
|
||||
const card = el("article", undefined, "vhost-card redirect-card"), head = el("div", undefined, "vhost-card-head"), identity = el("div", undefined, "vhost-card-identity"), title = el("div", undefined, "vhost-title-line");
|
||||
title.append(el("strong", item.name || item.id, "entity-title")); if (item.enabled === false) title.append(el("span", t("vhDisabled"), "pill")); identity.append(title, el("div", (item.frontend?.hosts || []).join(" · "), "vhost-hosts")); head.append(identity);
|
||||
@@ -80,8 +87,8 @@
|
||||
}
|
||||
form.addEventListener("input", changed); form.addEventListener("change", changed); $("notfound-mode").addEventListener("change", syncMode); $("notfound-status").addEventListener("change", syncMode);
|
||||
form.addEventListener("submit", async event => { event.preventDefault(); if (!envelope || busy) return; const invalid = [...form.elements].find(input => input.willValidate && !input.checkValidity()); if (invalid) { invalid.reportValidity(); return; } if (revision !== envelope.revision) { toast(t("stale"), true); return; } try { const item = collect(), next = structuredClone(envelope.config.not_found_hosts || []); if (!editing && next.some(value => value.id === item.id)) throw new Error(t("notfoundDuplicate")); const index = next.findIndex(value => value.id === editing); if (index < 0) next.push(item); else next[index] = item; if (await saveSection("not_found_hosts", next)) { dirty = false; editing = item.id; dialog.close(); render(); } } catch (error) { toast(error.message, true); } });
|
||||
$("notfound-new").addEventListener("click", () => { window.rustProxyDrafts.prepare(); load(); }); $("notfound-search").addEventListener("input", render); $("notfound-close").addEventListener("click", closeEditor); $("notfound-reset").addEventListener("click", async () => { if (await allowDiscard()) load(editing ? structuredClone((envelope?.config.not_found_hosts || []).find(item => item.id === editing) || defaults()) : defaults()); }); dialog.addEventListener("cancel", event => { event.preventDefault(); closeEditor(); });
|
||||
$("notfound-new").addEventListener("click", () => { window.rustProxyDrafts.prepare(); load(); }); $("notfound-search").addEventListener("input", render); $("notfound-sort")?.addEventListener("change", event => { listPrefs.sort = event.target.value; void saveListPrefs(); render(); }); $("notfound-sort-direction")?.addEventListener("click", () => { listPrefs.descending = !listPrefs.descending; syncSortDirection(); void saveListPrefs(); render(); }); $("notfound-close").addEventListener("click", closeEditor); $("notfound-reset").addEventListener("click", async () => { if (await allowDiscard()) load(editing ? structuredClone((envelope?.config.not_found_hosts || []).find(item => item.id === editing) || defaults()) : defaults()); }); dialog.addEventListener("cancel", event => { event.preventDefault(); closeEditor(); });
|
||||
window.rustProxyDrafts.register("notfound", { labelKey: "notfound", isOpen: () => dialog.open, isDirty: () => dirty, suspend: suspendDraft, restore: restoreDraft, close: () => { dirty = false; if (dialog.open) dialog.close(); } });
|
||||
window.rustProxyNotFound = { render, reset() { dialog.close(); editing = null; revision = null; dirty = false; list.replaceChildren(); $("notfound-search").value = ""; } };
|
||||
window.rustProxyNotFound = { render, applyPreferences, reset() { dialog.close(); editing = null; revision = null; dirty = false; listPrefs = { sort: "id", descending: false }; list.replaceChildren(); $("notfound-search").value = ""; if ($("notfound-sort")) $("notfound-sort").value = listPrefs.sort; syncSortDirection(); } };
|
||||
if (envelope) render();
|
||||
})();
|
||||
+189
-49
@@ -86,7 +86,9 @@
|
||||
</div>
|
||||
<div hidden="" id="setup-fields">
|
||||
<p class="fine" data-i18n="setupHelp">Create the first administrator using a one-time local
|
||||
capability.</p><code class="command">rust-proxy --data-dir DATA_DIR admin setup-token</code>
|
||||
capability.</p><code class="command">rust-proxy --data-dir ./data admin setup-token</code>
|
||||
<p class="fine" data-i18n="setupCommandExample">Standard service installation example:</p>
|
||||
<code class="command">rust-proxy --data-dir /var/lib/rustproxy admin setup-token</code>
|
||||
<label data-i18n="setupToken" for="setup-token">One-time setup token</label><input
|
||||
autocomplete="off" id="setup-token" maxlength="64" minlength="64" spellcheck="false"
|
||||
type="password" />
|
||||
@@ -649,6 +651,17 @@
|
||||
<label class="sr-only" data-i18n="vhSearch" for="vhost-search">Search vhosts</label>
|
||||
<input autocomplete="off" data-i18n-aria="vhSearch"
|
||||
data-i18n-placeholder="vhSearchPlaceholder" id="vhost-search" type="search" />
|
||||
<label class="backend-sort-control"><span data-i18n="backendSortBy">Sort
|
||||
by</span><select data-i18n-aria="backendSortBy" id="vhost-sort">
|
||||
<option data-i18n="vhVhost" value="id">Vhost</option>
|
||||
<option data-i18n="vhHosts" value="hostname">Hosts</option>
|
||||
<option data-i18n="status" value="status">Status</option>
|
||||
<option data-i18n="vhListeners" value="listeners">Listeners</option>
|
||||
<option data-i18n="vhCertificate" value="certificate">Certificate</option>
|
||||
<option data-i18n="vhBackendPool" value="backends">Backends</option>
|
||||
</select></label>
|
||||
<button class="quiet backend-sort-direction" data-i18n-aria="backendSortAscending"
|
||||
id="vhost-sort-direction" type="button">↑</button>
|
||||
<button class="outline" data-i18n="vhNew" id="vhost-new" type="button">New
|
||||
vhost</button>
|
||||
</div>
|
||||
@@ -691,7 +704,18 @@
|
||||
<div class="toolbar-actions"><label class="sr-only" data-i18n="dockerSearch"
|
||||
for="docker-search">Search Docker vhosts</label><input autocomplete="off"
|
||||
data-i18n-aria="dockerSearch" data-i18n-placeholder="dockerSearchPlaceholder"
|
||||
id="docker-search" type="search" /><label class="check compact-check docker-live-switch"
|
||||
id="docker-search" type="search" /><label class="backend-sort-control"><span
|
||||
data-i18n="backendSortBy">Sort by</span><select data-i18n-aria="backendSortBy"
|
||||
id="docker-vhost-sort">
|
||||
<option data-i18n="dockerServices" value="service">Service</option>
|
||||
<option data-i18n="dockerHostname" value="hostname">Hostname</option>
|
||||
<option data-i18n="status" value="status">Status</option>
|
||||
<option data-i18n="dockerBackendIp" value="backend">Backend host IP</option>
|
||||
<option data-i18n="dockerBackendScheme" value="scheme">Backend protocol</option>
|
||||
<option data-i18n="dockerPublish" value="published">Published</option>
|
||||
</select></label><button class="quiet backend-sort-direction"
|
||||
data-i18n-aria="backendSortAscending" id="docker-vhost-sort-direction"
|
||||
type="button">↑</button><label class="check compact-check docker-live-switch"
|
||||
title="Refresh this view from RustProxy memory every 5 seconds"><input
|
||||
id="docker-live-refresh" type="checkbox" /><span data-i18n="dockerLiveView">Live
|
||||
view</span></label><button class="outline" data-i18n="dockerRefresh"
|
||||
@@ -710,15 +734,26 @@
|
||||
<div class="vhost-layout redirect-layout">
|
||||
<article class="card">
|
||||
<div class="card-heading">
|
||||
<div><span class="small-label" data-i18n="redirectLayer">HTTP REDIRECTION</span>
|
||||
<h2 data-i18n="redirectList">Redirect hosts</h2>
|
||||
</div>
|
||||
<h2 data-i18n="redirectList">Redirect hosts</h2>
|
||||
<div class="vhost-list-tools">
|
||||
<label class="sr-only" data-i18n="redirectSearch" for="redirect-search">Search
|
||||
redirects</label>
|
||||
<input autocomplete="off" data-i18n-aria="redirectSearch"
|
||||
data-i18n-placeholder="redirectSearchPlaceholder" id="redirect-search"
|
||||
type="search" />
|
||||
<label class="backend-sort-control"><span data-i18n="backendSortBy">Sort
|
||||
by</span><select data-i18n-aria="backendSortBy" id="redirect-sort">
|
||||
<option data-i18n="redirectId" value="id">Redirect ID</option>
|
||||
<option data-i18n="redirectName" value="name">Display name</option>
|
||||
<option data-i18n="redirectHosts" value="hostname">Source hostnames</option>
|
||||
<option data-i18n="status" value="status">Status</option>
|
||||
<option data-i18n="redirectTarget" value="target">Target</option>
|
||||
<option data-i18n="redirectRules" value="rules">Rules</option>
|
||||
<option data-i18n="redirectListeners" value="listeners">Listeners</option>
|
||||
<option data-i18n="vhCertificate" value="certificate">Certificate</option>
|
||||
</select></label>
|
||||
<button class="quiet backend-sort-direction" data-i18n-aria="backendSortAscending"
|
||||
id="redirect-sort-direction" type="button">↑</button>
|
||||
<button class="outline" data-i18n="redirectNew" id="redirect-new" type="button">New
|
||||
redirect</button>
|
||||
</div>
|
||||
@@ -734,15 +769,25 @@
|
||||
<div class="vhost-layout redirect-layout">
|
||||
<article class="card">
|
||||
<div class="card-heading">
|
||||
<div><span class="small-label" data-i18n="notfoundLayer">LOCAL RESPONSE</span>
|
||||
<h2 data-i18n="notfoundList">404 hosts</h2>
|
||||
</div>
|
||||
<h2 data-i18n="notfoundList">404 hosts</h2>
|
||||
<div class="vhost-list-tools">
|
||||
<label class="sr-only" data-i18n="notfoundSearch" for="notfound-search">Search 404
|
||||
hosts</label>
|
||||
<input autocomplete="off" data-i18n-aria="notfoundSearch"
|
||||
data-i18n-placeholder="notfoundSearchPlaceholder" id="notfound-search"
|
||||
type="search" />
|
||||
<label class="backend-sort-control"><span data-i18n="backendSortBy">Sort
|
||||
by</span><select data-i18n-aria="backendSortBy" id="notfound-sort">
|
||||
<option data-i18n="notfoundId" value="id">Host ID</option>
|
||||
<option data-i18n="notfoundName" value="name">Display name</option>
|
||||
<option data-i18n="notfoundHosts" value="hostname">Hostnames</option>
|
||||
<option data-i18n="notfoundStatus" value="status">HTTP status</option>
|
||||
<option data-i18n="notfoundMode" value="mode">Response type</option>
|
||||
<option data-i18n="redirectListeners" value="listeners">Listeners</option>
|
||||
<option data-i18n="vhCertificate" value="certificate">Certificate</option>
|
||||
</select></label>
|
||||
<button class="quiet backend-sort-direction" data-i18n-aria="backendSortAscending"
|
||||
id="notfound-sort-direction" type="button">↑</button>
|
||||
<button class="outline" data-i18n="notfoundNew" id="notfound-new" type="button">New
|
||||
404 host</button>
|
||||
</div>
|
||||
@@ -768,6 +813,22 @@
|
||||
<input autocomplete="off" data-i18n-aria="streamSearch"
|
||||
data-i18n-placeholder="streamSearchPlaceholder" id="stream-search"
|
||||
type="search" />
|
||||
<label class="backend-sort-control"><span data-i18n="backendSortBy">Sort
|
||||
by</span><select data-i18n-aria="backendSortBy" id="stream-sort">
|
||||
<option data-i18n="streamName" value="name">Display name</option>
|
||||
<option data-i18n="streamId" value="id">Stream ID</option>
|
||||
<option data-i18n="streamProtocol" value="protocol">Protocol</option>
|
||||
<option data-i18n="streamListen" value="listen">Listen address</option>
|
||||
<option data-i18n="streamUpstream" value="upstream">Upstream address
|
||||
</option>
|
||||
<option data-i18n="status" value="status">Status</option>
|
||||
<option data-i18n="streamMaxConnections" value="maxConnections">Max
|
||||
connections</option>
|
||||
<option data-i18n="streamIdleShort" value="idleTimeout">Idle timeout
|
||||
</option>
|
||||
</select></label>
|
||||
<button class="quiet backend-sort-direction" data-i18n-aria="backendSortAscending"
|
||||
id="stream-sort-direction" type="button">↑</button>
|
||||
<button class="outline" data-i18n="streamNew" id="stream-new" type="button">New
|
||||
stream</button>
|
||||
</div>
|
||||
@@ -811,11 +872,14 @@
|
||||
</div>
|
||||
<section class="acl-policy-builder" aria-labelledby="acl-policy-mode-label">
|
||||
<div class="acl-policy-step-head">
|
||||
<div><span class="acl-policy-step">1</span><div><strong id="acl-policy-mode-label"
|
||||
data-i18n="aclProfilePolicyMode">Choose the base policy</strong><p
|
||||
class="fine" data-i18n="aclProfilePolicyModeHelp">Start with a simple
|
||||
<div><span class="acl-policy-step">1</span>
|
||||
<div><strong id="acl-policy-mode-label" data-i18n="aclProfilePolicyMode">Choose
|
||||
the base policy</strong>
|
||||
<p class="fine" data-i18n="aclProfilePolicyModeHelp">Start with a simple
|
||||
allow-list or block-list. Use custom mode only when you need mixed
|
||||
allow and deny rules.</p></div></div>
|
||||
allow and deny rules.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="acl-policy-modes" id="acl-profile-mode-options">
|
||||
<label class="acl-policy-mode-card"><input checked name="acl-profile-mode"
|
||||
@@ -843,28 +907,39 @@
|
||||
<label><span data-i18n="vhCountryMode">Country access</span><select
|
||||
id="acl-profile-country-mode">
|
||||
<option data-i18n="vhOff" value="off">Off</option>
|
||||
<option data-i18n="vhCountryBlock" value="block">Block listed countries</option>
|
||||
<option data-i18n="vhCountryAllow" value="allow">Allow only listed countries</option>
|
||||
<option data-i18n="vhCountryBlock" value="block">Block listed countries
|
||||
</option>
|
||||
<option data-i18n="vhCountryAllow" value="allow">Allow only listed countries
|
||||
</option>
|
||||
</select></label>
|
||||
</div>
|
||||
</section>
|
||||
<section class="acl-policy-builder">
|
||||
<div class="acl-policy-step-head"><div><span class="acl-policy-step">2</span><div><strong
|
||||
data-i18n="aclProfileCountryExceptions">Country exceptions</strong><p
|
||||
class="fine" id="acl-profile-country-help"></p></div></div><label
|
||||
class="acl-policy-switch"><input id="acl-profile-country-enabled"
|
||||
type="checkbox" /><span data-i18n="aclProfileUseCountries">Use countries</span></label></div>
|
||||
<div class="acl-policy-step-head">
|
||||
<div><span class="acl-policy-step">2</span>
|
||||
<div><strong data-i18n="aclProfileCountryExceptions">Country exceptions</strong>
|
||||
<p class="fine" id="acl-profile-country-help"></p>
|
||||
</div>
|
||||
</div><label class="acl-policy-switch"><input id="acl-profile-country-enabled"
|
||||
type="checkbox" /><span data-i18n="aclProfileUseCountries">Use
|
||||
countries</span></label>
|
||||
</div>
|
||||
<label class="acl-policy-country-list" id="acl-profile-country-field"><span
|
||||
id="acl-profile-country-label" data-i18n="vhCountryList">Countries</span><textarea
|
||||
id="acl-profile-countries" rows="4" maxlength="1024" spellcheck="false"
|
||||
id="acl-profile-country-label"
|
||||
data-i18n="vhCountryList">Countries</span><textarea id="acl-profile-countries"
|
||||
rows="4" maxlength="1024" spellcheck="false"
|
||||
placeholder="CN PL"></textarea></label>
|
||||
</section>
|
||||
<section class="acl-policy-builder">
|
||||
<div class="acl-policy-step-head"><div><span class="acl-policy-step">3</span><div><strong
|
||||
data-i18n="aclProfileIpExceptions">IP / CIDR exceptions</strong><p
|
||||
class="fine" id="acl-profile-ip-help"></p></div></div><button class="quiet"
|
||||
data-i18n="aclProfileAddIpException" id="acl-profile-rule-add" type="button">Add
|
||||
IP exception</button></div>
|
||||
<div class="acl-policy-step-head">
|
||||
<div><span class="acl-policy-step">3</span>
|
||||
<div><strong data-i18n="aclProfileIpExceptions">IP / CIDR exceptions</strong>
|
||||
<p class="fine" id="acl-profile-ip-help"></p>
|
||||
</div>
|
||||
</div><button class="quiet" data-i18n="aclProfileAddIpException"
|
||||
id="acl-profile-rule-add" type="button">Add
|
||||
IP exception</button>
|
||||
</div>
|
||||
<div class="acl-profile-rule-rows" id="acl-profile-rule-rows"></div>
|
||||
<details class="acl-profile-bulk" id="acl-profile-bulk">
|
||||
<summary data-i18n="aclProfileBulkRules">Paste / import rules</summary>
|
||||
@@ -878,8 +953,8 @@
|
||||
</details>
|
||||
</section>
|
||||
<section class="acl-policy-preview" aria-live="polite">
|
||||
<div class="acl-policy-preview-head"><span data-i18n="aclProfilePreview">Effective policy</span><b
|
||||
id="acl-profile-preview-default"></b></div>
|
||||
<div class="acl-policy-preview-head"><span data-i18n="aclProfilePreview">Effective
|
||||
policy</span><b id="acl-profile-preview-default"></b></div>
|
||||
<div class="acl-policy-preview-flow" id="acl-profile-preview-flow"></div>
|
||||
<p class="fine" id="acl-profile-preview-note"></p>
|
||||
</section>
|
||||
@@ -913,6 +988,19 @@
|
||||
data-certificate-tab="generate" data-i18n="certificateTabGenerate"
|
||||
id="certificate-tab-generate" role="tab" tabindex="-1" type="button"></button>
|
||||
</nav>
|
||||
<article class="card certificate-operation-card certificate-runtime-card" id="acme-runtime-card">
|
||||
<div class="certificate-operation-heading">
|
||||
<div>
|
||||
<span class="small-label" data-i18n="certificateRuntimeTitle">ACME runtime</span>
|
||||
<strong data-i18n="acme.runtime_checking" id="acme-runtime-message"
|
||||
role="status">Checking local Python environment…</strong>
|
||||
</div>
|
||||
<span class="pill" data-i18n="certificateRuntimePreparing"
|
||||
id="acme-runtime-state">Preparing</span>
|
||||
</div>
|
||||
<p class="fine" hidden="" id="acme-runtime-path"></p>
|
||||
<p class="fine" id="acme-runtime-components"></p>
|
||||
</article>
|
||||
<div class="certificate-pages">
|
||||
<div aria-labelledby="certificate-tab-installed" class="certificate-page"
|
||||
data-certificate-page="installed" id="certificate-page-installed" role="tabpanel">
|
||||
@@ -1320,10 +1408,10 @@
|
||||
</div>
|
||||
</details>
|
||||
<div class="stats-grid cache-stats">
|
||||
<article class="stat"><span data-i18n="cacheHotObjects" id="cache-hot-objects-label">Hot objects</span><strong
|
||||
id="cp-entries">-</strong><small id="cp-capacity">-</small><progress
|
||||
aria-labelledby="cache-hot-objects-label" id="cp-hot-entries-meter" max="100"
|
||||
value="0"></progress></article>
|
||||
<article class="stat"><span data-i18n="cacheHotObjects" id="cache-hot-objects-label">Hot
|
||||
objects</span><strong id="cp-entries">-</strong><small
|
||||
id="cp-capacity">-</small><progress aria-labelledby="cache-hot-objects-label"
|
||||
id="cp-hot-entries-meter" max="100" value="0"></progress></article>
|
||||
<article class="stat"><span data-i18n="cacheHotMemory" id="cache-memory-label">Hot
|
||||
memory</span><strong id="cp-bytes">-</strong><small
|
||||
id="cp-memory-detail">-</small><progress aria-labelledby="cache-memory-label"
|
||||
@@ -1331,13 +1419,14 @@
|
||||
</article>
|
||||
<article class="stat"><span data-i18n="cacheTransientMemory">Transient cache RAM</span><strong
|
||||
id="cp-transient">-</strong><small id="cp-transient-detail">-</small></article>
|
||||
<article class="stat"><span data-i18n="cacheColdObjects" id="cache-cold-objects-label">Cold objects</span><strong
|
||||
id="cp-cold-entries">-</strong><small id="cp-cold-capacity">-</small><progress
|
||||
aria-labelledby="cache-cold-objects-label" id="cp-cold-entries-meter" max="100"
|
||||
value="0"></progress></article>
|
||||
<article class="stat"><span data-i18n="cacheColdDisk" id="cache-cold-disk-label">Cold disk</span><strong
|
||||
id="cp-cold-bytes">-</strong><small id="cp-cold-disk-detail">-</small><progress
|
||||
aria-labelledby="cache-cold-disk-label" id="cp-cold-disk" max="100" value="0"></progress>
|
||||
<article class="stat"><span data-i18n="cacheColdObjects" id="cache-cold-objects-label">Cold
|
||||
objects</span><strong id="cp-cold-entries">-</strong><small
|
||||
id="cp-cold-capacity">-</small><progress aria-labelledby="cache-cold-objects-label"
|
||||
id="cp-cold-entries-meter" max="100" value="0"></progress></article>
|
||||
<article class="stat"><span data-i18n="cacheColdDisk" id="cache-cold-disk-label">Cold
|
||||
disk</span><strong id="cp-cold-bytes">-</strong><small
|
||||
id="cp-cold-disk-detail">-</small><progress aria-labelledby="cache-cold-disk-label"
|
||||
id="cp-cold-disk" max="100" value="0"></progress>
|
||||
</article>
|
||||
<article class="stat"><span data-i18n="cacheHitRatio">Hit ratio</span><strong
|
||||
id="cp-ratio">-</strong><small id="cp-requests">-</small></article>
|
||||
@@ -1921,7 +2010,8 @@
|
||||
id="server-mode">
|
||||
<option data-i18n="serverPreserve" value="preserve">Preserve origin value</option>
|
||||
<option data-i18n="serverHide" value="hide">Hide completely</option>
|
||||
<option data-i18n="serverCustom" selected value="custom">Replace / add custom value</option>
|
||||
<option data-i18n="serverCustom" selected value="custom">Replace / add custom value
|
||||
</option>
|
||||
</select>
|
||||
<label data-i18n="serverValue" for="server-value">Custom Server value</label><input
|
||||
id="server-value" maxlength="256" placeholder="RustProxy" value="RustProxy"
|
||||
@@ -1941,7 +2031,11 @@
|
||||
<label data-i18n="compressionMin" for="compress-min">Minimum response size
|
||||
(bytes)</label><input id="compress-min" max="1048576" min="32" required=""
|
||||
type="number" />
|
||||
<p class="fine" data-i18n="compressionHelp">Negotiates Accept-Encoding using client q-values and zstd → br → gzip → deflate as the server preference for equal quality. Cookies, Authorization, Set-Cookie, private and no-store responses may be compressed. Existing Content-Encoding, ranges, SSE, upgrades and no-transform are passed through.</p>
|
||||
<p class="fine" data-i18n="compressionHelp">Negotiates Accept-Encoding using client
|
||||
q-values and zstd → br → gzip → deflate as the server preference for equal quality.
|
||||
Cookies, Authorization, Set-Cookie, private and no-store responses may be
|
||||
compressed. Existing Content-Encoding, ranges, SSE, upgrades and no-transform are
|
||||
passed through.</p>
|
||||
<div class="form-actions"><button class="primary" data-i18n="saveResponse"
|
||||
type="submit">Apply response settings</button></div>
|
||||
</form>
|
||||
@@ -2553,6 +2647,14 @@
|
||||
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>
|
||||
</article>
|
||||
</div>
|
||||
@@ -2721,7 +2823,7 @@
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="page-footer"><span>RustProxy <b>0.27.0</b></span><span id="footer-status">API - /
|
||||
<footer class="page-footer"><span>RustProxy <b>0.27.1</b></span><span id="footer-status">API - /
|
||||
dropped: 0</span></footer>
|
||||
</div>
|
||||
</main>
|
||||
@@ -3105,12 +3207,38 @@
|
||||
this controls browser/proxy caching and does not create a second RustProxy object cache.</p>
|
||||
</div>
|
||||
<div class="vh-grid web-service-page" data-web-service-page="delivery" hidden>
|
||||
<label><span data-i18n="webServiceCompression">Dynamic compression</span><select id="web-service-compression"><option value="" data-i18n="inherit">Inherit</option><option value="on" data-i18n="enabled">Enabled</option><option value="off" data-i18n="notEnabled">Disabled</option></select><small class="fine" data-i18n="webServiceCompressionHelp">Inherits the vhost/global policy by default. Codecs can be filtered per service; globally disabled codecs stay unavailable.</small></label>
|
||||
<label><span data-i18n="webServiceCompression">Dynamic compression</span><select
|
||||
id="web-service-compression">
|
||||
<option value="" data-i18n="inherit">Inherit</option>
|
||||
<option value="on" data-i18n="enabled">Enabled</option>
|
||||
<option value="off" data-i18n="notEnabled">Disabled</option>
|
||||
</select><small class="fine" data-i18n="webServiceCompressionHelp">Inherits the vhost/global
|
||||
policy by default. Codecs can be filtered per service; globally disabled codecs stay
|
||||
unavailable.</small></label>
|
||||
<div class="vh-wide vh-grid" id="web-service-compression-options">
|
||||
<label><span data-i18n="compressionZstd">Zstandard (zstd)</span><select id="web-service-compression-zstd"><option value="" data-i18n="inherit">Inherit</option><option value="on" data-i18n="enabled">Enabled</option><option value="off" data-i18n="notEnabled">Disabled</option></select></label>
|
||||
<label><span data-i18n="compressionBr">Brotli (br)</span><select id="web-service-compression-br"><option value="" data-i18n="inherit">Inherit</option><option value="on" data-i18n="enabled">Enabled</option><option value="off" data-i18n="notEnabled">Disabled</option></select></label>
|
||||
<label><span data-i18n="compressionGzip">gzip</span><select id="web-service-compression-gzip"><option value="" data-i18n="inherit">Inherit</option><option value="on" data-i18n="enabled">Enabled</option><option value="off" data-i18n="notEnabled">Disabled</option></select></label>
|
||||
<label><span data-i18n="compressionDeflate">deflate</span><select id="web-service-compression-deflate"><option value="" data-i18n="inherit">Inherit</option><option value="on" data-i18n="enabled">Enabled</option><option value="off" data-i18n="notEnabled">Disabled</option></select></label>
|
||||
<label><span data-i18n="compressionZstd">Zstandard (zstd)</span><select
|
||||
id="web-service-compression-zstd">
|
||||
<option value="" data-i18n="inherit">Inherit</option>
|
||||
<option value="on" data-i18n="enabled">Enabled</option>
|
||||
<option value="off" data-i18n="notEnabled">Disabled</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="compressionBr">Brotli (br)</span><select
|
||||
id="web-service-compression-br">
|
||||
<option value="" data-i18n="inherit">Inherit</option>
|
||||
<option value="on" data-i18n="enabled">Enabled</option>
|
||||
<option value="off" data-i18n="notEnabled">Disabled</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="compressionGzip">gzip</span><select id="web-service-compression-gzip">
|
||||
<option value="" data-i18n="inherit">Inherit</option>
|
||||
<option value="on" data-i18n="enabled">Enabled</option>
|
||||
<option value="off" data-i18n="notEnabled">Disabled</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="compressionDeflate">deflate</span><select
|
||||
id="web-service-compression-deflate">
|
||||
<option value="" data-i18n="inherit">Inherit</option>
|
||||
<option value="on" data-i18n="enabled">Enabled</option>
|
||||
<option value="off" data-i18n="notEnabled">Disabled</option>
|
||||
</select></label>
|
||||
</div>
|
||||
<label class="check"><input id="web-service-precompressed" type="checkbox" /><span
|
||||
data-i18n="webServicePrecompressed">Serve precompressed sidecars</span></label>
|
||||
@@ -3439,7 +3567,7 @@ max_execution_time = 30</code></pre>
|
||||
<article class="card about-card">
|
||||
<div class="about-product">
|
||||
<span aria-hidden="true" class="brand-mark">r<span>/</span>p</span>
|
||||
<div><span class="small-label">RustProxy</span><strong>0.27.0</strong></div>
|
||||
<div><span class="small-label">RustProxy</span><strong>0.27.1</strong></div>
|
||||
</div>
|
||||
<dl class="about-meta">
|
||||
<div>
|
||||
@@ -3450,12 +3578,24 @@ max_execution_time = 30</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">Source
|
||||
target="_blank"><span data-i18n="aboutRepository">Official source
|
||||
repository</span><span>git.linuxiarz.pl/gru/RustProxy</span></a>
|
||||
<a class="outline about-repository" href="https://rustproxy.linuxiarz.pl/" rel="noopener noreferrer"
|
||||
target="_blank"><span data-i18n="aboutPackagesRepository">Official package
|
||||
repository</span><span>rustproxy.linuxiarz.pl</span></a>
|
||||
</article>
|
||||
</dialog>
|
||||
<div aria-live="polite" class="toast" hidden="" id="toast" popover="manual" role="status"></div>
|
||||
|
||||
@@ -20,8 +20,9 @@
|
||||
"password",
|
||||
"passwordMismatch",
|
||||
"setupHelp",
|
||||
"setupCommandExample",
|
||||
"setupTitle",
|
||||
"setupToken",
|
||||
"signIn",
|
||||
"username"
|
||||
]
|
||||
]
|
||||
@@ -23,7 +23,9 @@
|
||||
</div>
|
||||
<div hidden="" id="setup-fields">
|
||||
<p class="fine" data-i18n="setupHelp">Create the first administrator using a one-time local
|
||||
capability.</p><code class="command">rust-proxy --data-dir DATA_DIR admin setup-token</code>
|
||||
capability.</p><code class="command">rust-proxy --data-dir ./data admin setup-token</code>
|
||||
<p class="fine" data-i18n="setupCommandExample">Standard service installation example:</p>
|
||||
<code class="command">rust-proxy --data-dir /var/lib/rustproxy admin setup-token</code>
|
||||
<label data-i18n="setupToken" for="setup-token">One-time setup token</label><input
|
||||
autocomplete="off" id="setup-token" maxlength="64" minlength="64" spellcheck="false"
|
||||
type="password" />
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
(() => {
|
||||
const form = $("redirect-form"), dialog = $("redirect-dialog"), list = $("redirect-list"), rules = $("redirect-rules");
|
||||
if (!form || !dialog || !list || !rules) return;
|
||||
let editing = null, revision = null, dirty = false, readRedirectBindings = () => [], readRedirectHosts = () => [];
|
||||
const sortColumns = ["id", "name", "hostname", "status", "target", "rules", "listeners", "certificate"];
|
||||
let editing = null, revision = null, dirty = false, readRedirectBindings = () => [], readRedirectHosts = () => [], listPrefs = { sort: "id", descending: false };
|
||||
function normalizeListPrefs(preferences) { return { sort: sortColumns.includes(preferences?.redirect_list_sort) ? preferences.redirect_list_sort : "id", descending: preferences?.redirect_list_descending === true }; }
|
||||
function syncSortDirection() { const control = $("redirect-sort-direction"); if (!control) return; control.textContent = listPrefs.descending ? "↓" : "↑"; control.setAttribute("aria-label", t(listPrefs.descending ? "backendSortDescending" : "backendSortAscending")); }
|
||||
function applyPreferences(preferences) { listPrefs = normalizeListPrefs(preferences); if ($("redirect-sort")) $("redirect-sort").value = listPrefs.sort; syncSortDirection(); render(); }
|
||||
async function saveListPrefs() { if (!authenticated) return; try { listPrefs = normalizeListPrefs(await window.rustProxyPreferences.update({ redirect_list_sort: listPrefs.sort, redirect_list_descending: listPrefs.descending })); if ($("redirect-sort")) $("redirect-sort").value = listPrefs.sort; syncSortDirection(); } catch (error) { if (error.status !== 401) toast(error.message, true); } }
|
||||
const syncEnabledControl = window.rustProxyEnableControl($("redirect-enabled"));
|
||||
const yes = id => $(id).checked;
|
||||
const value = id => $(id).value.trim();
|
||||
@@ -100,6 +105,17 @@
|
||||
function render() {
|
||||
if (!envelope) return; const query = ($("redirect-search").value || "").trim().toLowerCase();
|
||||
const entries = (envelope.config.redirects || []).filter(item => !query || [item.id, item.name, ...(item.frontend?.hosts || []), ...(item.rules || []).map(rule => rule.target)].join(" ").toLowerCase().includes(query));
|
||||
const sortValue = item => ({
|
||||
id: String(item.id || "").toLowerCase(),
|
||||
name: String(item.name || item.id || "").toLowerCase(),
|
||||
hostname: String(item.frontend?.hosts?.[0] || "").toLowerCase(),
|
||||
status: item.enabled === false ? 1 : 0,
|
||||
target: (item.rules || []).filter(rule => rule.enabled !== false).map(rule => rule.target || "").join(" ").toLowerCase(),
|
||||
rules: Number(item.rules?.length || 0),
|
||||
listeners: (item.frontend?.listener_ids || []).join(" ").toLowerCase(),
|
||||
certificate: String(item.certificate_id || "").toLowerCase(),
|
||||
}[listPrefs.sort]);
|
||||
entries.sort((left, right) => { const a = sortValue(left), b = sortValue(right); const result = typeof a === "number" && typeof b === "number" ? a - b : String(a).localeCompare(String(b), locale(), { numeric: true, sensitivity: "base" }); return listPrefs.descending ? -result : result; });
|
||||
list.replaceChildren(...entries.map(item => {
|
||||
const card = el("article", undefined, "vhost-card redirect-card"); const head = el("div", undefined, "vhost-card-head"), identity = el("div", undefined, "vhost-card-identity"), title = el("div", undefined, "vhost-title-line");
|
||||
title.append(el("strong", item.name || item.id, "entity-title")); if (item.enabled === false) title.append(el("span", t("vhDisabled"), "pill")); identity.append(title, el("div", (item.frontend?.hosts || []).join(" · "), "vhost-hosts")); head.append(identity);
|
||||
@@ -116,10 +132,10 @@
|
||||
if (revision !== envelope.revision) { toast(t("stale"), true); return; }
|
||||
try { const item = collect(), next = structuredClone(envelope.config.redirects || []); if (!editing && next.some(value => value.id === item.id)) throw new Error(t("redirectDuplicate")); const index = next.findIndex(value => value.id === editing); if (index < 0) next.push(item); else next[index] = item; if (await saveSection("redirects", next)) { dirty = false; editing = item.id; dialog.close(); render(); } } catch (error) { toast(error.message, true); }
|
||||
});
|
||||
$("redirect-new").addEventListener("click", () => { window.rustProxyDrafts.prepare(); load(); }); $("redirect-search").addEventListener("input", render); $("redirect-rule-add").addEventListener("click", () => { addRule({ id: `rule-${rules.children.length + 1}` }); changed(); });
|
||||
$("redirect-new").addEventListener("click", () => { window.rustProxyDrafts.prepare(); load(); }); $("redirect-search").addEventListener("input", render); $("redirect-sort")?.addEventListener("change", event => { listPrefs.sort = event.target.value; void saveListPrefs(); render(); }); $("redirect-sort-direction")?.addEventListener("click", () => { listPrefs.descending = !listPrefs.descending; syncSortDirection(); void saveListPrefs(); render(); }); $("redirect-rule-add").addEventListener("click", () => { addRule({ id: `rule-${rules.children.length + 1}` }); changed(); });
|
||||
$("redirect-close").addEventListener("click", closeEditor); $("redirect-reset").addEventListener("click", async () => { if (await allowDiscard()) load(editing ? structuredClone((envelope?.config.redirects || []).find(item => item.id === editing) || defaults()) : defaults()); });
|
||||
dialog.addEventListener("cancel", event => { event.preventDefault(); closeEditor(); });
|
||||
window.rustProxyDrafts.register("redirect", { labelKey: "redirects", isOpen: () => dialog.open, isDirty: () => dirty, suspend: suspendDraft, restore: restoreDraft, close: () => { dirty = false; if (dialog.open) dialog.close(); } });
|
||||
window.rustProxyRedirects = { render, reset() { dialog.close(); editing = null; revision = null; dirty = false; list.replaceChildren(); rules.replaceChildren(); $("redirect-search").value = ""; } };
|
||||
window.rustProxyRedirects = { render, applyPreferences, reset() { dialog.close(); editing = null; revision = null; dirty = false; listPrefs = { sort: "id", descending: false }; list.replaceChildren(); rules.replaceChildren(); $("redirect-search").value = ""; if ($("redirect-sort")) $("redirect-sort").value = listPrefs.sort; syncSortDirection(); } };
|
||||
if (envelope) render();
|
||||
})();
|
||||
@@ -173,6 +173,10 @@ curl --fail --show-error --request POST \
|
||||
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 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 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"); }
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
// Layer-4 stream management mirrors the vhost workflow while keeping TCP/UDP
|
||||
// listeners separate from the HTTP policy stack.
|
||||
(() => {
|
||||
let editing = null, revision = null, dirty = false, initialized = false;
|
||||
const sortColumns = ["name", "id", "protocol", "listen", "upstream", "status", "maxConnections", "idleTimeout"];
|
||||
let editing = null, revision = null, dirty = false, initialized = false, listPrefs = { sort: "name", descending: false };
|
||||
const chartHistory = new Map();
|
||||
function normalizeListPrefs(preferences) { return { sort: sortColumns.includes(preferences?.stream_list_sort) ? preferences.stream_list_sort : "name", descending: preferences?.stream_list_descending === true }; }
|
||||
function syncSortDirection() { const control = $("stream-sort-direction"); if (!control) return; control.textContent = listPrefs.descending ? "↓" : "↑"; control.setAttribute("aria-label", t(listPrefs.descending ? "backendSortDescending" : "backendSortAscending")); }
|
||||
function applyPreferences(preferences) { listPrefs = normalizeListPrefs(preferences); if ($("stream-sort")) $("stream-sort").value = listPrefs.sort; syncSortDirection(); render(); }
|
||||
async function saveListPrefs() { if (!authenticated) return; try { listPrefs = normalizeListPrefs(await window.rustProxyPreferences.update({ stream_list_sort: listPrefs.sort, stream_list_descending: listPrefs.descending })); if ($("stream-sort")) $("stream-sort").value = listPrefs.sort; syncSortDirection(); } catch (error) { if (error.status !== 401) toast(error.message, true); } }
|
||||
const dialog = $("stream-dialog"), form = $("stream-form"), container = $("stream-fields");
|
||||
const text = (tag, key, cls) => { const node = el(tag, t(key), cls); node.dataset.i18n = key; return node; };
|
||||
const button = (key, callback, cls = "quiet") => { const node = text("button", key, cls); node.type = "button"; node.addEventListener("click", callback); return node; };
|
||||
@@ -102,6 +107,12 @@
|
||||
for (const id of chartHistory.keys()) if (!configuredIds.has(id)) chartHistory.delete(id);
|
||||
const query = ($("stream-search")?.value || "").trim().toLowerCase();
|
||||
const streams = configured.filter(stream => !query || [stream.id, stream.name, stream.protocol, stream.listen, stream.upstream].filter(Boolean).join(" ").toLowerCase().includes(query));
|
||||
const active = new Map((latestStatus?.streams?.active || []).map(item => [item.id, item]));
|
||||
const sortValue = stream => {
|
||||
const live = active.get(stream.id), same = live && JSON.stringify(live) === JSON.stringify(stream);
|
||||
return ({ name: String(stream.name || stream.id || "").toLowerCase(), id: String(stream.id || "").toLowerCase(), protocol: String(stream.protocol || "tcp"), listen: String(stream.listen || "").toLowerCase(), upstream: String(stream.upstream || "").toLowerCase(), status: stream.enabled === false ? 2 : same ? 0 : 1, maxConnections: Number(stream.max_connections || 0), idleTimeout: Number(stream.idle_timeout_seconds || 0) })[listPrefs.sort];
|
||||
};
|
||||
streams.sort((left, right) => { const a = sortValue(left), b = sortValue(right); const result = typeof a === "number" && typeof b === "number" ? a - b : String(a).localeCompare(String(b), locale(), { numeric: true, sensitivity: "base" }); return listPrefs.descending ? -result : result; });
|
||||
const cards = streams.map(stream => {
|
||||
const box = el("article", undefined, "vhost-card stream-card"); box.dataset.streamCard = stream.id;
|
||||
const head = el("div", undefined, "vhost-card-head"), identity = el("div", undefined, "vhost-card-identity"), title = el("div", undefined, "vhost-title-line");
|
||||
@@ -178,10 +189,12 @@
|
||||
$("stream-new").addEventListener("click", () => { window.rustProxyDrafts.prepare(); load(); });
|
||||
$("stream-listener-add").addEventListener("click", () => { setPanel("settings"); window.rustProxySettings?.select("listeners"); });
|
||||
$("stream-search").addEventListener("input", render);
|
||||
$("stream-sort")?.addEventListener("change", event => { listPrefs.sort = event.target.value; void saveListPrefs(); render(); });
|
||||
$("stream-sort-direction")?.addEventListener("click", () => { listPrefs.descending = !listPrefs.descending; syncSortDirection(); void saveListPrefs(); render(); });
|
||||
$("stream-close").addEventListener("click", closeEditor);
|
||||
$("stream-reset").addEventListener("click", async () => { if (await allowDiscard()) load(editing ? structuredClone((envelope?.config.streams || []).find(item => item.id === editing) || defaults()) : defaults()); });
|
||||
dialog.addEventListener("cancel", event => { event.preventDefault(); closeEditor(); });
|
||||
window.rustProxyDrafts.register("stream", { labelKey: "streams", isOpen: () => dialog.open, isDirty: () => dirty, suspend: suspendDraft, restore: restoreDraft, close: () => { dirty = false; if (dialog.open) dialog.close(); } });
|
||||
window.rustProxyStreams = { render, renderStatus, reset() { dialog.close(); editing = null; revision = null; dirty = false; initialized = false; chartHistory.clear(); container.replaceChildren(); $("stream-search").value = ""; $("stream-list").replaceChildren(); } };
|
||||
window.rustProxyStreams = { render, renderStatus, applyPreferences, reset() { dialog.close(); editing = null; revision = null; dirty = false; initialized = false; listPrefs = { sort: "name", descending: false }; chartHistory.clear(); container.replaceChildren(); $("stream-search").value = ""; $("stream-list").replaceChildren(); if ($("stream-sort")) $("stream-sort").value = listPrefs.sort; syncSortDirection(); } };
|
||||
if (envelope) render(); if (latestStatus) renderStatus(latestStatus);
|
||||
})();
|
||||
+21
-9
@@ -4966,19 +4966,24 @@ dialog .window-heading {
|
||||
}
|
||||
|
||||
#panel-vhosts .card-heading,
|
||||
#panel-web-server .card-heading {
|
||||
#panel-web-server .card-heading,
|
||||
#panel-redirects .card-heading,
|
||||
#panel-notfound .card-heading {
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.vhost-list-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-left: auto
|
||||
}
|
||||
|
||||
#vhost-search,
|
||||
#web-service-search {
|
||||
#web-service-search,
|
||||
#redirect-search,
|
||||
#notfound-search {
|
||||
width: min(260px, 32vw);
|
||||
min-width: 180px
|
||||
}
|
||||
@@ -5108,7 +5113,9 @@ dialog .window-heading {
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
#panel-vhosts .card-heading {
|
||||
#panel-vhosts .card-heading,
|
||||
#panel-redirects .card-heading,
|
||||
#panel-notfound .card-heading {
|
||||
align-items: stretch;
|
||||
flex-direction: column
|
||||
}
|
||||
@@ -5118,7 +5125,9 @@ dialog .window-heading {
|
||||
margin-left: 0
|
||||
}
|
||||
|
||||
#vhost-search {
|
||||
#vhost-search,
|
||||
#redirect-search,
|
||||
#notfound-search {
|
||||
flex: 1 1 auto;
|
||||
width: auto;
|
||||
min-width: 0
|
||||
@@ -7140,7 +7149,8 @@ body[data-density="compact"] .page-footer {
|
||||
border-left-color: var(--green)
|
||||
}
|
||||
|
||||
#panel-tls .certificate-operation-card[data-state="failed"] {
|
||||
#panel-tls .certificate-operation-card[data-state="failed"],
|
||||
#panel-tls .certificate-operation-card[data-state="unavailable"] {
|
||||
border-left-color: var(--red)
|
||||
}
|
||||
|
||||
@@ -7162,7 +7172,9 @@ body[data-density="compact"] .page-footer {
|
||||
}
|
||||
|
||||
#panel-tls .certificate-operation-card[data-state="failed"] .certificate-operation-heading strong,
|
||||
#panel-tls .certificate-operation-card[data-state="failed"] .pill {
|
||||
#panel-tls .certificate-operation-card[data-state="failed"] .pill,
|
||||
#panel-tls .certificate-operation-card[data-state="unavailable"] .certificate-operation-heading strong,
|
||||
#panel-tls .certificate-operation-card[data-state="unavailable"] .pill {
|
||||
color: var(--red)
|
||||
}
|
||||
|
||||
@@ -8596,8 +8608,6 @@ button.config-history-day.is-selected {
|
||||
}
|
||||
}
|
||||
|
||||
/* Account-scoped appearance preferences. */
|
||||
|
||||
.appearance-settings-card {
|
||||
max-width: 980px;
|
||||
}
|
||||
@@ -9612,6 +9622,7 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
|
||||
}
|
||||
|
||||
@media(max-width:900px) {
|
||||
|
||||
.acl-profile-layout,
|
||||
.acl-policy-modes,
|
||||
.acl-policy-custom-controls {
|
||||
@@ -9624,6 +9635,7 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
|
||||
}
|
||||
|
||||
@media(max-width:640px) {
|
||||
|
||||
.acl-policy-step-head,
|
||||
.acl-policy-preview-head {
|
||||
align-items: stretch;
|
||||
@@ -9634,4 +9646,4 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
|
||||
.acl-policy-step-head>button {
|
||||
width: 100%
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
const poolColumns = ["vhost", "backend", "status", "active", "requests", "errors", "latency", "traffic", "generation", "generationRequests", "generationAge", "rotations", "lastFailure", "lastRecovery"];
|
||||
const vhostSortColumns = ["id", "hostname", "status", "listeners", "certificate", "backends"];
|
||||
const defaultPoolTablePrefs = () => ({ sort: "vhost", descending: false, columns: [...poolColumns] });
|
||||
let poolTablePrefs = defaultPoolTablePrefs(), poolPreferenceSaveRunning = false, poolPreferenceSaveQueued = false;
|
||||
const defaultVhostListPrefs = () => ({ sort: "id", descending: false });
|
||||
let poolTablePrefs = defaultPoolTablePrefs(), vhostListPrefs = defaultVhostListPrefs(), poolPreferenceSaveRunning = false, poolPreferenceSaveQueued = false;
|
||||
function normalizePoolTablePrefs(preferences) {
|
||||
const selected = Array.isArray(preferences?.backend_table_columns) ? preferences.backend_table_columns.filter(key => poolColumns.includes(key)) : [];
|
||||
return {
|
||||
@@ -9,10 +11,27 @@
|
||||
columns: selected.length ? selected : [...poolColumns],
|
||||
};
|
||||
}
|
||||
function applyPoolPreferences(preferences) {
|
||||
function normalizeVhostListPrefs(preferences) {
|
||||
return { sort: vhostSortColumns.includes(preferences?.vhost_list_sort) ? preferences.vhost_list_sort : "id", descending: preferences?.vhost_list_descending === true };
|
||||
}
|
||||
function syncVhostSortDirection() {
|
||||
const control = $("vhost-sort-direction"); if (!control) return;
|
||||
control.textContent = vhostListPrefs.descending ? "↓" : "↑";
|
||||
control.setAttribute("aria-label", t(vhostListPrefs.descending ? "backendSortDescending" : "backendSortAscending"));
|
||||
}
|
||||
function applyPreferences(preferences) {
|
||||
poolTablePrefs = normalizePoolTablePrefs(preferences);
|
||||
const sort = $("pool-sort"); if (sort) sort.value = poolTablePrefs.sort;
|
||||
syncPoolColumns(); syncPoolSortDirection(); applyPoolTableView();
|
||||
vhostListPrefs = normalizeVhostListPrefs(preferences);
|
||||
const poolSort = $("pool-sort"); if (poolSort) poolSort.value = poolTablePrefs.sort;
|
||||
const vhostSort = $("vhost-sort"); if (vhostSort) vhostSort.value = vhostListPrefs.sort;
|
||||
syncPoolColumns(); syncPoolSortDirection(); syncVhostSortDirection(); applyPoolTableView(); render();
|
||||
}
|
||||
async function saveVhostListPrefs() {
|
||||
if (!authenticated) return;
|
||||
try {
|
||||
const saved = await window.rustProxyPreferences.update({ vhost_list_sort: vhostListPrefs.sort, vhost_list_descending: vhostListPrefs.descending });
|
||||
vhostListPrefs = normalizeVhostListPrefs(saved); const sort = $("vhost-sort"); if (sort) sort.value = vhostListPrefs.sort; syncVhostSortDirection();
|
||||
} catch (error) { if (error.status !== 401) toast(error.message, true); }
|
||||
}
|
||||
async function savePoolTablePrefs() {
|
||||
if (!authenticated) return;
|
||||
@@ -103,6 +122,18 @@
|
||||
].filter(Boolean).join(" ").toLowerCase();
|
||||
return haystack.includes(query);
|
||||
});
|
||||
const sortValue = v => ({
|
||||
id: String(v.id || "").toLowerCase(),
|
||||
hostname: String(v.frontend?.hosts?.[0] || "").toLowerCase(),
|
||||
status: v.enabled === false ? 1 : 0,
|
||||
listeners: (v.frontend?.listener_ids || []).join(" ").toLowerCase(),
|
||||
certificate: String(v.certificate_id || "").toLowerCase(),
|
||||
backends: Number(v.backends?.length || 0),
|
||||
}[vhostListPrefs.sort]);
|
||||
visibleVhosts.sort((left, right) => {
|
||||
const a = sortValue(left), b = sortValue(right); const result = typeof a === "number" && typeof b === "number" ? a - b : String(a).localeCompare(String(b), locale(), { numeric: true, sensitivity: "base" });
|
||||
return vhostListPrefs.descending ? -result : result;
|
||||
});
|
||||
const cards = visibleVhosts.map(v => {
|
||||
const box = el("article", undefined, "vhost-card"); box.dataset.vhostCard = v.id;
|
||||
const head = el("div", undefined, "vhost-card-head"), identity = el("div", undefined, "vhost-card-identity");
|
||||
@@ -324,6 +355,8 @@
|
||||
});
|
||||
$("vhost-new").addEventListener("click", () => { window.rustProxyDrafts.prepare(); load(); });
|
||||
$("vhost-search").addEventListener("input", render);
|
||||
$("vhost-sort")?.addEventListener("change", event => { vhostListPrefs.sort = event.target.value; void saveVhostListPrefs(); render(); });
|
||||
$("vhost-sort-direction")?.addEventListener("click", () => { vhostListPrefs.descending = !vhostListPrefs.descending; syncVhostSortDirection(); void saveVhostListPrefs(); render(); });
|
||||
$("vhost-reset").addEventListener("click", async () => {
|
||||
if (!(await allowDiscard())) return;
|
||||
const saved = editing ? (envelope?.config.vhosts || []).find(item => item.id === editing) : null;
|
||||
@@ -333,5 +366,5 @@
|
||||
initPoolTableTools();
|
||||
$("location-help-close")?.addEventListener("click", () => $("location-help-dialog")?.close());
|
||||
window.rustProxyDrafts.register("vhost", { labelKey: "vhosts", isOpen: () => dialog.open, isDirty: () => dirty, suspend: suspendDraft, restore: restoreDraft, close: () => { dirty = false; if (dialog.open) dialog.close(); } });
|
||||
window.rustProxyVhosts = { render, renderStatus, applyPreferences: applyPoolPreferences, reset() { dialog.close(); draftEpoch++; stagedCertificates = []; uploading = false; editing = null; revision = null; dirty = false; initialized = false; poolTablePrefs = defaultPoolTablePrefs(); poolPreferenceSaveRunning = false; poolPreferenceSaveQueued = false; chartHistory.clear(); backendTrafficHistory.clear(); vhostTrafficHistory.clear(); container.replaceChildren(); $("vhost-search").value = ""; if ($("pool-search")) $("pool-search").value = ""; $("vhost-list").replaceChildren(); $("pool-rows").replaceChildren(); const sort = $("pool-sort"); if (sort) sort.value = poolTablePrefs.sort; applyPoolTableView(); } };
|
||||
window.rustProxyVhosts = { render, renderStatus, applyPreferences, reset() { dialog.close(); draftEpoch++; stagedCertificates = []; uploading = false; editing = null; revision = null; dirty = false; initialized = false; poolTablePrefs = defaultPoolTablePrefs(); vhostListPrefs = defaultVhostListPrefs(); poolPreferenceSaveRunning = false; poolPreferenceSaveQueued = false; chartHistory.clear(); backendTrafficHistory.clear(); vhostTrafficHistory.clear(); container.replaceChildren(); $("vhost-search").value = ""; if ($("pool-search")) $("pool-search").value = ""; $("vhost-list").replaceChildren(); $("pool-rows").replaceChildren(); const sort = $("pool-sort"); if (sort) sort.value = poolTablePrefs.sort; const vhostSort = $("vhost-sort"); if (vhostSort) vhostSort.value = vhostListPrefs.sort; syncVhostSortDirection(); applyPoolTableView(); } };
|
||||
if (envelope) render(); if (latestStatus) renderStatus(latestStatus);
|
||||
@@ -396,6 +396,35 @@ async fn run(
|
||||
} else {
|
||||
println!(" Sign in with your administrator account.");
|
||||
}
|
||||
let certificate_runtime = state.acme.runtime_view();
|
||||
let runtime_pending = matches!(certificate_runtime.status, "checking" | "initializing");
|
||||
let component_state = |available: bool| {
|
||||
if available {
|
||||
"available"
|
||||
} else if runtime_pending {
|
||||
"checking"
|
||||
} else {
|
||||
"unavailable"
|
||||
}
|
||||
};
|
||||
let managed_python_directory = certificate_runtime
|
||||
.managed_directory
|
||||
.clone()
|
||||
.unwrap_or_else(|| data_dir.join("acme-python").to_string_lossy().into_owned());
|
||||
println!(" Source: https://git.linuxiarz.pl/gru/RustProxy");
|
||||
println!(" Packages: https://rustproxy.linuxiarz.pl/");
|
||||
println!(
|
||||
" Python: {} (ACME mode: {}, pip: {}, Certbot: {}; managed env: {})",
|
||||
component_state(certificate_runtime.python),
|
||||
if runtime_pending {
|
||||
"preparing"
|
||||
} else {
|
||||
certificate_runtime.mode
|
||||
},
|
||||
component_state(certificate_runtime.pip),
|
||||
component_state(certificate_runtime.certbot),
|
||||
managed_python_directory
|
||||
);
|
||||
println!(" Data: {}\n Stop: Ctrl+C\n", data_dir.display());
|
||||
if state
|
||||
.active_listeners
|
||||
|
||||
@@ -41,7 +41,7 @@ use zeroize::Zeroizing;
|
||||
#[command(
|
||||
name = "rust-proxy",
|
||||
version,
|
||||
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.27.0"
|
||||
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.27.1"
|
||||
)]
|
||||
struct Cli {
|
||||
#[arg(
|
||||
|
||||
+1
-1
@@ -239,7 +239,7 @@ async fn live_logs_command(dir: PathBuf, addr: SocketAddr) -> Result<()> {
|
||||
}
|
||||
});
|
||||
let interactive = io::stdin().is_terminal() && io::stdout().is_terminal();
|
||||
println!("RustProxy live logs 0.27.0 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
|
||||
println!("RustProxy live logs 0.27.1 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
|
||||
if interactive { print!("> "); io::stdout().flush()?; }
|
||||
let mut filters = LiveLogFilters::default();
|
||||
let mut history = std::collections::BTreeMap::<u64, serde_json::Value>::new();
|
||||
|
||||
@@ -200,6 +200,10 @@ impl App {
|
||||
Arc::downgrade(&app),
|
||||
app.shutdown.clone(),
|
||||
));
|
||||
if !cfg!(test) {
|
||||
app.tunnels
|
||||
.spawn(crate::acme::runtime_setup_once(Arc::downgrade(&app)));
|
||||
}
|
||||
app.tunnels.spawn(crate::docker::poll_loop(
|
||||
Arc::downgrade(&app),
|
||||
app.shutdown.clone(),
|
||||
|
||||
@@ -159,7 +159,7 @@ pub(super) fn fastcgi_params(
|
||||
if context.tls { "on" } else { "off" }.into(),
|
||||
);
|
||||
params.insert("GATEWAY_INTERFACE".into(), "CGI/1.1".into());
|
||||
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.27.0".into());
|
||||
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.27.1".into());
|
||||
params.insert("REDIRECT_STATUS".into(), "200".into());
|
||||
if let Some(value) = headers
|
||||
.get(CONTENT_TYPE)
|
||||
|
||||
@@ -185,7 +185,7 @@ pub(super) fn directory_listing(path: &str, entries: &[DirectoryEntry]) -> Vec<u
|
||||
"<a class=\"{class}\" href=\"{href}\"><span class=\"type\">{type_label}</span><span class=\"name\">{label}{slash}</span><span class=\"kind\">{kind}</span></a>"
|
||||
));
|
||||
}
|
||||
html.push_str(r#"</div></section><footer class="page-footer"><span>RustProxy <b>0.27.0</b></span><span>Web Server · Directory listing</span></footer></main></body></html>"#);
|
||||
html.push_str(r#"</div></section><footer class="page-footer"><span>RustProxy <b>0.27.1</b></span><span>Web Server · Directory listing</span></footer></main></body></html>"#);
|
||||
html.into_bytes()
|
||||
}
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ async fn empty_index_can_go_directly_to_directory_listing() {
|
||||
let body = std::str::from_utf8(&body).unwrap();
|
||||
assert!(body.contains("file.txt"));
|
||||
assert!(body.contains("DIRECTORY LISTING"));
|
||||
assert!(body.contains("RustProxy <b>0.27.0</b>"));
|
||||
assert!(body.contains("RustProxy <b>0.27.1</b>"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in new issue
Block a user