v0.28.1
This commit is contained in:
1 parent
3b3f0db377
commit
d940f9995f
47 files changed
+542
-187
No files matched your search
@@ -40,6 +40,16 @@ async fn certificate_deploy(
|
||||
),
|
||||
));
|
||||
}
|
||||
if crate::tls::certificate_files_are_external(&selected.cert_file, &selected.key_file)
|
||||
.map_err(|error| ApiError(StatusCode::UNPROCESSABLE_ENTITY, format!("{error:#}")))?
|
||||
{
|
||||
return Err(ApiError(
|
||||
StatusCode::CONFLICT,
|
||||
format!(
|
||||
"certificate {certificate_id} uses external file paths; renew those files in place and call the certificate reload endpoint instead of deploy"
|
||||
),
|
||||
));
|
||||
}
|
||||
Some((selected.cert_file.clone(), selected.key_file.clone()))
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -409,6 +409,7 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
|
||||
"/docs/V0.25.1_AUDIT.md"
|
||||
)),
|
||||
),
|
||||
reader_doc!("docs/V0.28.1_IMPLEMENTATION.md", "technical"),
|
||||
reader_doc!("docs/V0.28.0_IMPLEMENTATION.md", "technical"),
|
||||
reader_doc!("docs/V0.27.3_IMPLEMENTATION.md", "technical"),
|
||||
reader_doc!("docs/V0.27.2_IMPLEMENTATION.md", "technical"),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.28.0",
|
||||
"version": "0.28.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": [
|
||||
|
||||
@@ -20,6 +20,25 @@
|
||||
}
|
||||
})();
|
||||
|
||||
function certificateUsesExternalFiles(cert) {
|
||||
return String(cert?.cert_file || "").startsWith("/") && String(cert?.key_file || "").startsWith("/");
|
||||
}
|
||||
function syncCertificateFileSource() {
|
||||
const external = $("certificate-file-source").value === "external";
|
||||
const cert = $("certificate-file"), key = $("certificate-key");
|
||||
for (const input of [cert, key]) {
|
||||
input.maxLength = external ? 4096 : 96;
|
||||
if (external) input.removeAttribute("pattern");
|
||||
else input.pattern = "[A-Za-z0-9_.-]+";
|
||||
}
|
||||
cert.placeholder = external ? "/etc/letsencrypt/live/example.com/fullchain.pem" : "app.pem";
|
||||
key.placeholder = external ? "/etc/letsencrypt/live/example.com/privkey.pem" : "app.key";
|
||||
const helpKey = external ? "certificateSourceExternalHelp" : "certificateSourceManagedHelp";
|
||||
$("certificate-file-source-help").dataset.i18n = helpKey;
|
||||
$("certificate-file-source-help").textContent = t(helpKey);
|
||||
}
|
||||
$("certificate-file-source").addEventListener("change", syncCertificateFileSource);
|
||||
|
||||
function renderCertificates() {
|
||||
if (!envelope) return;
|
||||
const settings = envelope.config.tls || {};
|
||||
@@ -41,6 +60,7 @@ function renderCertificates() {
|
||||
identity.append(el("span", cert.id, "entity-title"));
|
||||
const statePills = el("div", undefined, "vhost-policy-pills certificate-state-pills");
|
||||
if (managed) statePills.append(el("small", `${t("certificateManaged")} · ${cert.issuer.provider} / ${cert.issuer.environment}`, "pill"));
|
||||
else if (certificateUsesExternalFiles(cert)) statePills.append(el("small", t("certificateExternalFiles"), "pill"));
|
||||
if (settings.default_certificate === cert.id) statePills.append(el("small", t("defaultLabel"), "pill"));
|
||||
if (inventory?.expiry_status === "expired") statePills.append(el("small", t("certificateExpired"), "pill status-error"));
|
||||
else if (inventory?.expiry_status === "warning") statePills.append(el("small", t("certificateExpiresSoon"), "pill status-error"));
|
||||
@@ -91,7 +111,7 @@ function renderCertificates() {
|
||||
if (result.administration_restart_required) toast(t("certificateAdminRestartRequired"), true);
|
||||
} catch (error) { toast(error.message, true); } finally { busy = false; }
|
||||
}));
|
||||
buttons.append(action(t("edit"), () => { window.rustProxyCertificates?.open("existing"); $("certificate-advanced").open = true; $("certificate-id").value = cert.id; $("certificate-hosts").value = cert.hosts.join(", "); $("certificate-file").value = cert.cert_file; $("certificate-key").value = cert.key_file; $("certificate-default").checked = settings.default_certificate === cert.id; }));
|
||||
buttons.append(action(t("edit"), () => { window.rustProxyCertificates?.open("existing"); $("certificate-advanced").open = true; $("certificate-id").value = cert.id; $("certificate-hosts").value = cert.hosts.join(", "); $("certificate-file-source").value = certificateUsesExternalFiles(cert) ? "external" : "managed"; syncCertificateFileSource(); $("certificate-file").value = cert.cert_file; $("certificate-key").value = cert.key_file; $("certificate-default").checked = settings.default_certificate === cert.id; }));
|
||||
}
|
||||
const remove = action(t("remove"), async () => { if (inUse) { toast(t("certInUse"), true); return; } if (!(await appConfirm(t("confirmDelete"), { danger: true }))) return; const next = structuredClone(envelope.config); next.tls.certificates = next.tls.certificates.filter(v => v.id !== cert.id); await saveSection("tls", next.tls); });
|
||||
remove.disabled = inUse; remove.title = inUse ? t("certInUse") : ""; buttons.append(remove);
|
||||
@@ -101,7 +121,7 @@ function renderCertificates() {
|
||||
$("certificate-list").replaceChildren(...entries);
|
||||
if (!entries.length) $("certificate-list").append(el("p", t("emptyCertificates"), "empty"));
|
||||
}
|
||||
$("certificate-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return; try { await withFormSubmitState(form, event.submitter, async () => { const next = structuredClone(envelope.config); next.tls = next.tls || {}; next.tls.certificates = next.tls.certificates || []; const cert = { id: $("certificate-id").value.trim(), hosts: $("certificate-hosts").value.split(",").map(v => v.trim()).filter(Boolean), cert_file: $("certificate-file").value.trim(), key_file: $("certificate-key").value.trim() }; const index = next.tls.certificates.findIndex(v => v.id === cert.id); if (index >= 0 && next.tls.certificates[index].issuer) throw fieldError($("certificate-id"), t("certManagedReadOnly")); if (index < 0) next.tls.certificates.push(cert); else { const old = next.tls.certificates[index]; const sameFiles = old.cert_file === cert.cert_file && old.key_file === cert.key_file; next.tls.certificates[index] = { ...old, ...cert, covered_hosts: sameFiles ? (old.covered_hosts || []) : [], issuer: sameFiles ? old.issuer : null }; } if ($("certificate-default").checked) next.tls.default_certificate = cert.id; else if (next.tls.default_certificate === cert.id) delete next.tls.default_certificate; if (await saveSection("tls", next.tls)) { form.reset(); window.rustProxyCertificates?.close(); } }); } catch (error) { toast(error.message, true); } });
|
||||
$("certificate-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return; try { await withFormSubmitState(form, event.submitter, async () => { const next = structuredClone(envelope.config); next.tls = next.tls || {}; next.tls.certificates = next.tls.certificates || []; const cert = { id: $("certificate-id").value.trim(), hosts: $("certificate-hosts").value.split(",").map(v => v.trim()).filter(Boolean), cert_file: $("certificate-file").value.trim(), key_file: $("certificate-key").value.trim() }; const index = next.tls.certificates.findIndex(v => v.id === cert.id); if (index >= 0 && next.tls.certificates[index].issuer) throw fieldError($("certificate-id"), t("certManagedReadOnly")); if (index < 0) next.tls.certificates.push(cert); else { const old = next.tls.certificates[index]; const sameFiles = old.cert_file === cert.cert_file && old.key_file === cert.key_file; next.tls.certificates[index] = { ...old, ...cert, covered_hosts: sameFiles ? (old.covered_hosts || []) : [], issuer: sameFiles ? old.issuer : null }; } if ($("certificate-default").checked) next.tls.default_certificate = cert.id; else if (next.tls.default_certificate === cert.id) delete next.tls.default_certificate; if (await saveSection("tls", next.tls)) { form.reset(); syncCertificateFileSource(); window.rustProxyCertificates?.close(); } }); } catch (error) { toast(error.message, true); } });
|
||||
$("reload-certificates").addEventListener("click", async () => {
|
||||
if (!envelope || busy) return;
|
||||
try {
|
||||
@@ -114,7 +134,8 @@ $("reload-certificates").addEventListener("click", async () => {
|
||||
} catch (error) { toast(error.message, true); }
|
||||
finally { busy = false; }
|
||||
});
|
||||
$("reset-certificate").addEventListener("click", () => $("certificate-form").reset());
|
||||
$("reset-certificate").addEventListener("click", () => { $("certificate-form").reset(); $("certificate-file-source").value = "managed"; syncCertificateFileSource(); });
|
||||
syncCertificateFileSource();
|
||||
|
||||
// Dashboard metrics are actual server samples. Null means unavailable, not zero.
|
||||
let systemSamples = [], connectionOpenSamples = [], connectionExchangeSamples = [], cacheMemorySamples = [], cacheTransientSamples = [], cacheFillSamples = [], cacheColdQueueSamples = [], cacheColdIoSamples = [], cacheColdStorageSamples = [], cacheHitSamples = [], cacheMissSamples = [], cacheBypassSamples = [], lastCacheCounters = null,
|
||||
|
||||
@@ -1180,6 +1180,12 @@
|
||||
placeholder="app" required="" />
|
||||
<label data-i18n="certLegacyNames" for="certificate-hosts"></label><input
|
||||
id="certificate-hosts" placeholder="app.localhost" />
|
||||
<label data-i18n="certificateFileSource" for="certificate-file-source"></label><select
|
||||
id="certificate-file-source">
|
||||
<option data-i18n="certificateSourceManaged" value="managed"></option>
|
||||
<option data-i18n="certificateSourceExternal" value="external"></option>
|
||||
</select>
|
||||
<p class="fine" id="certificate-file-source-help" data-i18n="certificateSourceManagedHelp"></p>
|
||||
<label data-i18n="certificateFile" for="certificate-file"></label><input
|
||||
id="certificate-file" maxlength="96" pattern="[A-Za-z0-9_.-]+"
|
||||
placeholder="app.pem" required="" />
|
||||
@@ -2878,7 +2884,7 @@
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="page-footer"><span>RustProxy <b>0.28.0</b></span><span id="footer-status">API - /
|
||||
<footer class="page-footer"><span>RustProxy <b>0.28.1</b></span><span id="footer-status">API - /
|
||||
dropped: 0</span></footer>
|
||||
</div>
|
||||
</main>
|
||||
@@ -3622,7 +3628,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.28.0</strong></div>
|
||||
<div><span class="small-label">RustProxy</span><strong>0.28.1</strong></div>
|
||||
</div>
|
||||
<dl class="about-meta">
|
||||
<div>
|
||||
|
||||
@@ -259,8 +259,16 @@ fn validate_snapshot_config(
|
||||
) -> Result<()> {
|
||||
let cert_dir = snapshot_root.join("certs");
|
||||
for certificate in &envelope.config.tls.certificates {
|
||||
if crate::tls::certificate_files_are_external(
|
||||
&certificate.cert_file,
|
||||
&certificate.key_file,
|
||||
)? {
|
||||
// External certificate material is intentionally outside the RustProxy
|
||||
// data tree and is therefore referenced by configuration, not copied
|
||||
// into a RustProxy backup. The operator must restore/mount it separately.
|
||||
continue;
|
||||
}
|
||||
for name in [&certificate.cert_file, &certificate.key_file] {
|
||||
crate::tls::safe_name(name)?;
|
||||
let path = cert_dir.join(name);
|
||||
let meta = fs::symlink_metadata(&path).with_context(|| {
|
||||
format!("configured certificate file changed during backup: {name}")
|
||||
|
||||
@@ -41,7 +41,7 @@ use zeroize::Zeroizing;
|
||||
#[command(
|
||||
name = "rust-proxy",
|
||||
version,
|
||||
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.28.0"
|
||||
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.28.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.28.0 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
|
||||
println!("RustProxy live logs 0.28.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();
|
||||
|
||||
@@ -310,16 +310,14 @@ fn certificate_public_metadata(certificate: &[u8]) -> Result<CertificateMetadata
|
||||
/// Used by the authenticated certificate inventory to enrich older/manual entries
|
||||
/// that predate persisted `covered_hosts` metadata. Private key material is not read.
|
||||
pub fn certificate_covered_hosts(directory: &Path, cert_file: &str) -> Result<Vec<String>> {
|
||||
safe_name(cert_file)?;
|
||||
let certificate = read_pem(&directory.join(cert_file), false)?;
|
||||
let certificate = read_configured_pem(directory, cert_file, false)?;
|
||||
certificate_dns_names(&certificate)
|
||||
}
|
||||
|
||||
/// Read non-secret metadata directly from the configured public leaf certificate.
|
||||
/// Private keys are never opened by this path.
|
||||
pub fn certificate_metadata(directory: &Path, cert_file: &str) -> Result<CertificateMetadata> {
|
||||
safe_name(cert_file)?;
|
||||
let certificate = read_pem(&directory.join(cert_file), false)?;
|
||||
let certificate = read_configured_pem(directory, cert_file, false)?;
|
||||
certificate_public_metadata(&certificate)
|
||||
}
|
||||
|
||||
@@ -340,11 +338,10 @@ fn parse_pair(certificate: &[u8], key: &[u8]) -> Result<CertifiedKey> {
|
||||
Ok(certified)
|
||||
}
|
||||
fn load_pair(directory: &Path, cert: &str, key: &str) -> Result<CertifiedKey> {
|
||||
safe_name(cert)?;
|
||||
safe_name(key)?;
|
||||
certificate_files_are_external(cert, key)?;
|
||||
parse_pair(
|
||||
&read_pem(&directory.join(cert), false)?,
|
||||
&read_pem(&directory.join(key), true)?,
|
||||
&read_configured_pem(directory, cert, false)?,
|
||||
&read_configured_pem(directory, key, true)?,
|
||||
)
|
||||
}
|
||||
/// Runs on a bounded control-plane blocking worker, never from a handshake.
|
||||
|
||||
@@ -158,6 +158,71 @@ mod tests {
|
||||
assert!(safe_name(path).is_err(), "{path}");
|
||||
}
|
||||
assert!(safe_name("app-v2.key").is_ok());
|
||||
assert!(certificate_files_are_external("app.pem", "app.key").is_ok());
|
||||
assert!(certificate_files_are_external("app.pem", "/etc/tls/app.key").is_err());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn external_absolute_paths_follow_certbot_style_symlinks_on_each_reload() {
|
||||
use std::os::unix::fs::{symlink, PermissionsExt};
|
||||
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let cert_dir = root.path().join("rustproxy-certs");
|
||||
fs::create_dir(&cert_dir).unwrap();
|
||||
initialize_default(&cert_dir).unwrap();
|
||||
|
||||
let archive = root.path().join("archive/example.test");
|
||||
let live = root.path().join("live/example.test");
|
||||
fs::create_dir_all(&archive).unwrap();
|
||||
fs::create_dir_all(&live).unwrap();
|
||||
|
||||
let first = rcgen::generate_simple_self_signed(vec!["example.test".into()]).unwrap();
|
||||
fs::write(archive.join("fullchain1.pem"), first.cert.pem()).unwrap();
|
||||
fs::write(archive.join("privkey1.pem"), first.key_pair.serialize_pem()).unwrap();
|
||||
fs::set_permissions(archive.join("privkey1.pem"), fs::Permissions::from_mode(0o640)).unwrap();
|
||||
symlink("../../archive/example.test/fullchain1.pem", live.join("fullchain.pem")).unwrap();
|
||||
symlink("../../archive/example.test/privkey1.pem", live.join("privkey.pem")).unwrap();
|
||||
|
||||
let cert_path = live.join("fullchain.pem").display().to_string();
|
||||
let key_path = live.join("privkey.pem").display().to_string();
|
||||
assert!(certificate_files_are_external(&cert_path, &key_path).unwrap());
|
||||
let settings = Settings {
|
||||
default_certificate: Some("letsencrypt".into()),
|
||||
certificates: vec![Certificate {
|
||||
issuer: None,
|
||||
id: "letsencrypt".into(),
|
||||
covered_hosts: Vec::new(),
|
||||
hosts: vec!["example.test".into()],
|
||||
cert_file: cert_path.clone(),
|
||||
key_file: key_path.clone(),
|
||||
}],
|
||||
..Settings::default()
|
||||
};
|
||||
assert!(compile(&settings, Some(&cert_dir)).unwrap().is_some());
|
||||
assert_eq!(
|
||||
certificate_metadata(&cert_dir, &cert_path).unwrap().covered_hosts,
|
||||
vec!["example.test"]
|
||||
);
|
||||
|
||||
let second = rcgen::generate_simple_self_signed(vec![
|
||||
"example.test".into(),
|
||||
"www.example.test".into(),
|
||||
])
|
||||
.unwrap();
|
||||
fs::write(archive.join("fullchain2.pem"), second.cert.pem()).unwrap();
|
||||
fs::write(archive.join("privkey2.pem"), second.key_pair.serialize_pem()).unwrap();
|
||||
fs::set_permissions(archive.join("privkey2.pem"), fs::Permissions::from_mode(0o640)).unwrap();
|
||||
fs::remove_file(live.join("fullchain.pem")).unwrap();
|
||||
fs::remove_file(live.join("privkey.pem")).unwrap();
|
||||
symlink("../../archive/example.test/fullchain2.pem", live.join("fullchain.pem")).unwrap();
|
||||
symlink("../../archive/example.test/privkey2.pem", live.join("privkey.pem")).unwrap();
|
||||
|
||||
assert!(compile(&settings, Some(&cert_dir)).unwrap().is_some());
|
||||
assert_eq!(
|
||||
certificate_metadata(&cert_dir, &cert_path).unwrap().covered_hosts,
|
||||
vec!["example.test", "www.example.test"]
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn default_is_persistent_and_tls_advertises_both_protocols() {
|
||||
|
||||
+77
-6
@@ -16,6 +16,7 @@ use std::{
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
const MAX_PEM: u64 = 131_072;
|
||||
const MAX_CERTIFICATE_PATH: usize = 4096;
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Settings {
|
||||
@@ -324,7 +325,68 @@ pub fn safe_name(name: &str) -> Result<()> {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate how a configured certificate pair is addressed. Legacy/simple names
|
||||
/// remain private to `<data-dir>/certs`; an explicitly selected external pair uses
|
||||
/// two absolute paths. Mixing the two modes is rejected so the source is never
|
||||
/// ambiguous.
|
||||
pub fn certificate_files_are_external(cert_file: &str, key_file: &str) -> Result<bool> {
|
||||
let cert_external = Path::new(cert_file).is_absolute();
|
||||
let key_external = Path::new(key_file).is_absolute();
|
||||
ensure!(
|
||||
cert_external == key_external,
|
||||
"certificate and private key must both use filenames from the RustProxy certificate directory or both use absolute external paths"
|
||||
);
|
||||
if cert_external {
|
||||
ensure!(
|
||||
!cert_file.is_empty()
|
||||
&& !key_file.is_empty()
|
||||
&& cert_file.len() <= MAX_CERTIFICATE_PATH
|
||||
&& key_file.len() <= MAX_CERTIFICATE_PATH,
|
||||
"external certificate paths must be absolute paths up to 4096 bytes"
|
||||
);
|
||||
Ok(true)
|
||||
} else {
|
||||
safe_name(cert_file)?;
|
||||
safe_name(key_file)?;
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn configured_certificate_path(directory: &Path, value: &str) -> Result<PathBuf> {
|
||||
if Path::new(value).is_absolute() {
|
||||
ensure!(
|
||||
!value.is_empty() && value.len() <= MAX_CERTIFICATE_PATH,
|
||||
"external certificate path must be an absolute path up to 4096 bytes"
|
||||
);
|
||||
// Certbot's live/ entries are symlinks to archive/. Resolve them on every
|
||||
// reload, then open the final regular file with O_NOFOLLOW in read_pem.
|
||||
return fs::canonicalize(value)
|
||||
.with_context(|| format!("cannot resolve external certificate path {value}"));
|
||||
}
|
||||
safe_name(value)?;
|
||||
Ok(directory.join(value))
|
||||
}
|
||||
|
||||
fn read_configured_pem(
|
||||
directory: &Path,
|
||||
value: &str,
|
||||
private: bool,
|
||||
) -> Result<Zeroizing<Vec<u8>>> {
|
||||
let external = Path::new(value).is_absolute();
|
||||
let path = configured_certificate_path(directory, value)?;
|
||||
read_pem_with_policy(&path, private, external)
|
||||
}
|
||||
|
||||
fn read_pem(path: &Path, private: bool) -> Result<Zeroizing<Vec<u8>>> {
|
||||
read_pem_with_policy(path, private, false)
|
||||
}
|
||||
|
||||
fn read_pem_with_policy(
|
||||
path: &Path,
|
||||
private: bool,
|
||||
external_group_read: bool,
|
||||
) -> Result<Zeroizing<Vec<u8>>> {
|
||||
let mut options = OpenOptions::new();
|
||||
options.read(true);
|
||||
#[cfg(unix)]
|
||||
@@ -343,14 +405,23 @@ fn read_pem(path: &Path, private: bool) -> Result<Zeroizing<Vec<u8>>> {
|
||||
#[cfg(unix)]
|
||||
if private {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
ensure!(
|
||||
meta.permissions().mode() & 0o077 == 0,
|
||||
"private key permissions must be 600: {}",
|
||||
path.display()
|
||||
);
|
||||
let mode = meta.permissions().mode();
|
||||
if external_group_read {
|
||||
ensure!(
|
||||
mode & 0o137 == 0,
|
||||
"external private key may be owner/group-readable but must not be executable, group-writable, or world-accessible: {}",
|
||||
path.display()
|
||||
);
|
||||
} else {
|
||||
ensure!(
|
||||
mode & 0o077 == 0,
|
||||
"private key permissions must not grant group/world access: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
let _ = private;
|
||||
let _ = (private, external_group_read);
|
||||
let mut data = Zeroizing::new(Vec::new());
|
||||
file.take(MAX_PEM + 1).read_to_end(&mut data)?;
|
||||
ensure!(
|
||||
|
||||
@@ -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.28.0".into());
|
||||
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.28.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.28.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.28.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.28.0</b>"));
|
||||
assert!(body.contains("RustProxy <b>0.28.1</b>"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in new issue
Block a user