v0.25.4
This commit is contained in:
1 parent
e6604e41fc
commit
1dea7f55c8
49 files changed
+4499
-4025
No files matched your search
@@ -72,6 +72,7 @@ static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
|
||||
("docs/V0.24.8_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.24.8_AUDIT.md"))),
|
||||
("docs/V0.25.0_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.0_AUDIT.md"))),
|
||||
("docs/V0.25.1_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.1_AUDIT.md"))),
|
||||
("docs/V0.25.4_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.4_AUDIT.md"))),
|
||||
("docs/V0.25.3_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.3_AUDIT.md"))),
|
||||
("docs/V0.25.2_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.2_AUDIT.md"))),
|
||||
("docs/V0.25.0_SECURITY_PREPROD_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.0_SECURITY_PREPROD_AUDIT.md"))),
|
||||
|
||||
+171
-2
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.25.3",
|
||||
"version": "0.25.4",
|
||||
"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": [
|
||||
@@ -5046,7 +5046,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"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.",
|
||||
"security": [],
|
||||
"parameters": [
|
||||
{
|
||||
@@ -9179,6 +9179,175 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/config/history/restore": {
|
||||
"post": {
|
||||
"summary": "Restore a retained configuration revision as a new revision",
|
||||
"description": "Loads one retained configuration document, validates it through the normal full configuration update path, and saves it as a new revision. expected_revision must match the current configuration revision; the selected source revision must be older. Existing history is preserved.",
|
||||
"tags": [
|
||||
"Configuration"
|
||||
],
|
||||
"operationId": "post_api_config_history_restore",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Origin",
|
||||
"in": "header",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Exact http://HOST:ADMIN_PORT. Set automatically by same-origin browser POST."
|
||||
},
|
||||
{
|
||||
"name": "X-RustProxy-Request",
|
||||
"in": "header",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"AdminSession": [],
|
||||
"CsrfToken": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"revision",
|
||||
"expected_revision"
|
||||
],
|
||||
"properties": {
|
||||
"revision": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 0,
|
||||
"description": "Older retained revision to restore."
|
||||
},
|
||||
"expected_revision": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 0,
|
||||
"description": "Current revision observed by the caller."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Configuration restored and published as a new revision",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Envelope"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid restore request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Authentication required or revoked",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Host, origin or CSRF validation failed",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Requested revision is not retained",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Configuration changed; reload before restoring",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"413": {
|
||||
"description": "Request exceeds the endpoint body limit",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Restored configuration failed current validation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"description": "Bounded worker/session/rate limit",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Control-plane storage/worker unavailable",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,6 +339,10 @@ pub fn router(state: Arc<App>) -> Router {
|
||||
.route("/api/cache/cold/cleanup/{id}", get(cache_cold_cleanup_job))
|
||||
.route("/api/config", get(configuration).merge(put(update)))
|
||||
.route("/api/config/history", get(config_history))
|
||||
.route(
|
||||
"/api/config/history/restore",
|
||||
post(restore_config_history).layer(DefaultBodyLimit::max(4096)),
|
||||
)
|
||||
.route("/api/config/{section}", put(update_section))
|
||||
.route("/api/logs", get(logs))
|
||||
.route("/api/live-logs", get(live_logs_stream))
|
||||
|
||||
@@ -342,6 +342,13 @@ struct ConfigHistoryQuery {
|
||||
limit: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ConfigHistoryRestore {
|
||||
revision: u64,
|
||||
expected_revision: u64,
|
||||
}
|
||||
|
||||
async fn config_history(
|
||||
State(state): State<Arc<App>>,
|
||||
Query(query): Query<ConfigHistoryQuery>,
|
||||
@@ -363,6 +370,42 @@ async fn config_history(
|
||||
.map_err(auth::internal)?;
|
||||
Ok(Json(json!({"revisions": revisions})))
|
||||
}
|
||||
async fn restore_config_history(
|
||||
State(state): State<Arc<App>>,
|
||||
Extension(session): Extension<Session>,
|
||||
Json(input): Json<ConfigHistoryRestore>,
|
||||
) -> Result<Json<crate::config::Envelope>, ApiError> {
|
||||
let current_revision = state.config.load().envelope.revision;
|
||||
if current_revision != input.expected_revision {
|
||||
return Err(ApiError(
|
||||
StatusCode::CONFLICT,
|
||||
"configuration changed; reload before restoring".into(),
|
||||
));
|
||||
}
|
||||
if input.revision >= current_revision {
|
||||
return Err(ApiError(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"only an older configuration revision can be restored".into(),
|
||||
));
|
||||
}
|
||||
let detail = state
|
||||
.store
|
||||
.configuration_revision(input.revision)
|
||||
.await
|
||||
.map_err(auth::internal)?
|
||||
.ok_or_else(|| ApiError(StatusCode::NOT_FOUND, "configuration revision not found".into()))?;
|
||||
state
|
||||
.restore_as(
|
||||
Update {
|
||||
expected_revision: input.expected_revision,
|
||||
config: detail.config,
|
||||
},
|
||||
session.username,
|
||||
input.revision,
|
||||
)
|
||||
.await
|
||||
.map(Json)
|
||||
}
|
||||
async fn update(
|
||||
State(state): State<Arc<App>>,
|
||||
Extension(session): Extension<Session>,
|
||||
|
||||
@@ -312,6 +312,7 @@
|
||||
for (path, method) in [
|
||||
("/api/config", "PUT"),
|
||||
("/api/config/cache", "PUT"),
|
||||
("/api/config/history/restore", "POST"),
|
||||
("/api/acme/issue", "POST"),
|
||||
("/api/acme/renew", "POST"),
|
||||
("/api/docker/refresh", "POST"),
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"cookie"
|
||||
);
|
||||
assert!(spec["paths"].get("/api/config").is_some());
|
||||
assert!(spec["paths"].get("/api/config/history/restore").is_some());
|
||||
let response = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
@@ -170,3 +171,84 @@
|
||||
}
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn retained_configuration_revision_can_be_restored_as_a_new_revision() {
|
||||
let (router, store, _dir, secret, csrf) = fixture().await;
|
||||
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
|
||||
|
||||
let mut first = Config::default();
|
||||
first.server_header.value = "first".into();
|
||||
let response = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
request("/api/config")
|
||||
.method("PUT")
|
||||
.header("content-type", "application/json")
|
||||
.header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("x-csrf-token", &csrf)
|
||||
.body(Body::from(
|
||||
json!({"expected_revision":0,"config":first}).to_string(),
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(json_body(response).await["revision"], 1);
|
||||
|
||||
let mut second = Config::default();
|
||||
second.server_header.value = "second".into();
|
||||
let response = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
request("/api/config")
|
||||
.method("PUT")
|
||||
.header("content-type", "application/json")
|
||||
.header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("x-csrf-token", &csrf)
|
||||
.body(Body::from(
|
||||
json!({"expected_revision":1,"config":second}).to_string(),
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(json_body(response).await["revision"], 2);
|
||||
|
||||
let response = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
request("/api/config/history/restore")
|
||||
.method("POST")
|
||||
.header("content-type", "application/json")
|
||||
.header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("x-csrf-token", &csrf)
|
||||
.body(Body::from(
|
||||
json!({"revision":1,"expected_revision":2}).to_string(),
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let restored = json_body(response).await;
|
||||
assert_eq!(restored["revision"], 3);
|
||||
assert_eq!(restored["config"]["server_header"]["value"], "first");
|
||||
|
||||
let audit = store.recent_audit().await.unwrap();
|
||||
assert_eq!(audit[0].actor, "owner");
|
||||
assert_eq!(audit[0].action, "config.restore");
|
||||
assert_eq!(audit[0].detail, "source_revision=1 revision=3");
|
||||
let detail = store.configuration_revision(3).await.unwrap().unwrap();
|
||||
assert_eq!(detail.config.server_header.value, "first");
|
||||
assert_eq!(detail.previous_config.unwrap().server_header.value, "second");
|
||||
store.close().await;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
let configHistoryEntries = [];
|
||||
let configHistoryEntries = [], configHistorySelectedRevision = null;
|
||||
function historyDate(value, options = {}) {
|
||||
try { return new Intl.DateTimeFormat(locale(), options).format(new Date(Number(value) * 1000)); }
|
||||
catch { return new Date(Number(value) * 1000).toLocaleString(); }
|
||||
@@ -13,6 +13,12 @@ function historyValue(value) {
|
||||
const text = JSON.stringify(value, null, 2);
|
||||
return text.length > 6000 ? `${text.slice(0, 6000)}\n...` : text;
|
||||
}
|
||||
function syncConfigHistoryRestoreButton() {
|
||||
const button = $("config-history-restore"), revision = Number(configHistorySelectedRevision);
|
||||
if (!button) return;
|
||||
button.hidden = configHistorySelectedRevision === null;
|
||||
button.disabled = busy || !envelope || !Number.isSafeInteger(revision) || revision >= Number(envelope.revision);
|
||||
}
|
||||
function configDiff(before, after, path = "$", output = [], depth = 0) {
|
||||
if (output.length >= 200) return output;
|
||||
if (Object.is(before, after)) return output;
|
||||
@@ -29,8 +35,10 @@ function configDiff(before, after, path = "$", output = [], depth = 0) {
|
||||
function renderConfigRevisionDetail(detail) {
|
||||
const entry = detail?.revision;
|
||||
if (!entry) return;
|
||||
configHistorySelectedRevision = Number(entry.revision);
|
||||
$("config-history-detail-title").textContent = `${t("configHistoryRevision")} ${entry.revision}`;
|
||||
$("config-history-detail-meta").textContent = `${historyDate(entry.time, { dateStyle: "medium", timeStyle: "short" })} · ${entry.actor}`;
|
||||
syncConfigHistoryRestoreButton();
|
||||
document.querySelectorAll(".config-history-revision").forEach(button => button.classList.toggle("active", Number(button.dataset.revision) === Number(entry.revision)));
|
||||
const diff = configDiff(entry.previous_config, entry.config);
|
||||
const box = $("config-history-diff"); box.replaceChildren();
|
||||
@@ -95,7 +103,29 @@ function renderConfigHistory() {
|
||||
}
|
||||
if (!months.length) calendar.append(el("p", t("configHistoryEmpty"), "empty"));
|
||||
}
|
||||
async function restoreConfigRevision() {
|
||||
const revision = Number(configHistorySelectedRevision);
|
||||
if (!envelope || busy || !Number.isSafeInteger(revision) || revision >= Number(envelope.revision)) return;
|
||||
const expectedRevision = Number(envelope.revision);
|
||||
const message = t("configHistoryRestoreConfirm").replace("{revision}", revision).replace("{current}", expectedRevision);
|
||||
if (!(await appConfirm(message, { danger: true, confirmLabel: t("configHistoryRestoreAction") }))) return;
|
||||
const button = $("config-history-restore"), epoch = authEpoch;
|
||||
busy = true; syncConfigHistoryRestoreButton();
|
||||
try {
|
||||
const value = await api("/api/config/history/restore", { method: "POST", body: JSON.stringify({ revision, expected_revision: expectedRevision }) });
|
||||
if (!authenticated || epoch !== authEpoch) return;
|
||||
envelope = value;
|
||||
await refreshCertificateInventory();
|
||||
if (!authenticated || epoch !== authEpoch) return;
|
||||
renderConfig();
|
||||
toast(t("configHistoryRestored").replace("{revision}", revision).replace("{new_revision}", value.revision));
|
||||
await refresh(true);
|
||||
if (authenticated && epoch === authEpoch) await loadConfigHistory();
|
||||
} catch (error) { toast(error.message, true); if (error.status === 401) lock(); }
|
||||
finally { busy = false; if (button?.isConnected) syncConfigHistoryRestoreButton(); }
|
||||
}
|
||||
async function loadConfigHistory() {
|
||||
configHistorySelectedRevision = null; syncConfigHistoryRestoreButton();
|
||||
$("config-history-summary").textContent = t("configHistoryLoading");
|
||||
try {
|
||||
const result = await api("/api/config/history?limit=512"); configHistoryEntries = result.revisions || []; renderConfigHistory();
|
||||
@@ -105,3 +135,4 @@ async function loadConfigHistory() {
|
||||
$("config-history-open").addEventListener("click", () => { const dialog = $("config-history-dialog"); if (!dialog.open) dialog.showModal(); void loadConfigHistory(); });
|
||||
$("config-history-close").addEventListener("click", () => $("config-history-dialog").close());
|
||||
$("config-history-reload").addEventListener("click", () => void loadConfigHistory());
|
||||
$("config-history-restore").addEventListener("click", () => void restoreConfigRevision());
|
||||
@@ -6,7 +6,7 @@ function renderSettings() {
|
||||
if (!settingsDirty.has("response-form")) {
|
||||
const server = config.server_header || {}, compression = config.compression || {}, diagnostics = config.diagnostic_headers || {};
|
||||
$("expose-request-id").checked = !!diagnostics.request_id;
|
||||
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.25.3";
|
||||
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.25.4";
|
||||
for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name];
|
||||
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
|
||||
}
|
||||
|
||||
@@ -1472,7 +1472,7 @@
|
||||
<option data-i18n="serverCustom" 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/v0.25.3"
|
||||
id="server-value" maxlength="256" placeholder="RustProxy/v0.25.4"
|
||||
spellcheck="false" />
|
||||
<h3 class="subheading" data-i18n="diagnosticHeaders">Diagnostic headers</h3>
|
||||
<div class="switch-grid">
|
||||
@@ -2218,7 +2218,7 @@
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="page-footer"><span>RustProxy <b>0.25.3</b></span><span id="footer-status">API - /
|
||||
<footer class="page-footer"><span>RustProxy <b>0.25.4</b></span><span id="footer-status">API - /
|
||||
dropped: 0</span></footer>
|
||||
</div>
|
||||
</main>
|
||||
@@ -2370,7 +2370,7 @@
|
||||
<aside class="card config-history-detail">
|
||||
<div class="config-history-detail-head">
|
||||
<div><span class="small-label" data-i18n="configHistoryChanges">CHANGES</span><h3 id="config-history-detail-title">-</h3></div>
|
||||
<span class="fine" id="config-history-detail-meta"></span>
|
||||
<div class="config-history-detail-actions"><span class="fine" id="config-history-detail-meta"></span><button class="outline" data-i18n="configHistoryRestoreAction" id="config-history-restore" type="button" hidden disabled>Restore revision</button></div>
|
||||
</div>
|
||||
<div class="config-history-revisions" id="config-history-revisions"></div>
|
||||
<div class="config-history-diff" id="config-history-diff"><p class="empty" data-i18n="configHistorySelectDay">Select a day.</p></div>
|
||||
|
||||
@@ -7734,6 +7734,18 @@ button.config-history-day.is-selected {
|
||||
margin: 3px 0 0;
|
||||
}
|
||||
|
||||
.config-history-detail-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
align-items: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.config-history-detail-actions button {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.config-history-revisions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -7788,4 +7800,6 @@ button.config-history-day.is-selected {
|
||||
.config-history-layout { grid-template-columns: 1fr; overflow: auto; }
|
||||
.config-history-browser, .config-history-detail { overflow: visible; }
|
||||
.config-history-change-values { grid-template-columns: 1fr; }
|
||||
.config-history-detail-head { flex-direction: column; }
|
||||
.config-history-detail-actions { align-items: flex-start; text-align: left; }
|
||||
}
|
||||
@@ -181,6 +181,33 @@ impl Store {
|
||||
}
|
||||
/// Compare-and-swap and audit are one transaction. Publication is handled by App.
|
||||
pub async fn replace_as(&self, expected: u64, config: &Config, actor: &str) -> Result<bool> {
|
||||
self.replace_as_action(expected, config, actor, "config.apply", None)
|
||||
.await
|
||||
}
|
||||
pub(crate) async fn replace_as_restore(
|
||||
&self,
|
||||
expected: u64,
|
||||
config: &Config,
|
||||
actor: &str,
|
||||
source_revision: u64,
|
||||
) -> Result<bool> {
|
||||
self.replace_as_action(
|
||||
expected,
|
||||
config,
|
||||
actor,
|
||||
"config.restore",
|
||||
Some(source_revision),
|
||||
)
|
||||
.await
|
||||
}
|
||||
async fn replace_as_action(
|
||||
&self,
|
||||
expected: u64,
|
||||
config: &Config,
|
||||
actor: &str,
|
||||
audit_action: &str,
|
||||
source_revision: Option<u64>,
|
||||
) -> Result<bool> {
|
||||
let expected: i64 = expected.try_into().context("revision overflow")?;
|
||||
ensure!(expected < i64::MAX, "revision overflow");
|
||||
let document = serde_json::to_string(config)?;
|
||||
@@ -206,13 +233,11 @@ impl Store {
|
||||
.bind(history::CONFIG_HISTORY_CAPACITY)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
audit_tx(
|
||||
&mut tx,
|
||||
actor,
|
||||
"config.apply",
|
||||
&format!("revision={revision}"),
|
||||
)
|
||||
.await?;
|
||||
let audit_detail = source_revision.map_or_else(
|
||||
|| format!("revision={revision}"),
|
||||
|source| format!("source_revision={source} revision={revision}"),
|
||||
);
|
||||
audit_tx(&mut tx, actor, audit_action, &audit_detail).await?;
|
||||
tx.commit().await?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ use tracing_subscriber::prelude::*;
|
||||
#[command(
|
||||
name = "rust-proxy",
|
||||
version,
|
||||
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.25.3"
|
||||
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.25.4"
|
||||
)]
|
||||
struct Cli {
|
||||
#[arg(
|
||||
|
||||
+1
-1
@@ -226,7 +226,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.25.3 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
|
||||
println!("RustProxy live logs 0.25.4 - 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();
|
||||
|
||||
+34
-5
@@ -20,6 +20,22 @@ impl App {
|
||||
self: &Arc<Self>,
|
||||
update: Update,
|
||||
actor: String,
|
||||
) -> Result<config::Envelope, ApiError> {
|
||||
self.update_as_action(update, actor, None).await
|
||||
}
|
||||
pub async fn restore_as(
|
||||
self: &Arc<Self>,
|
||||
update: Update,
|
||||
actor: String,
|
||||
source_revision: u64,
|
||||
) -> Result<config::Envelope, ApiError> {
|
||||
self.update_as_action(update, actor, Some(source_revision)).await
|
||||
}
|
||||
async fn update_as_action(
|
||||
self: &Arc<Self>,
|
||||
update: Update,
|
||||
actor: String,
|
||||
source_revision: Option<u64>,
|
||||
) -> Result<config::Envelope, ApiError> {
|
||||
let slot = self.update_slots.clone().try_acquire_owned().map_err(|_| {
|
||||
ApiError(
|
||||
@@ -80,11 +96,24 @@ impl App {
|
||||
"configuration changed; reload before saving".into(),
|
||||
));
|
||||
}
|
||||
match this
|
||||
.store
|
||||
.replace_as(expected_revision, &prepared.envelope.config, &actor)
|
||||
.await
|
||||
{
|
||||
let persisted = match source_revision {
|
||||
Some(source) => {
|
||||
this.store
|
||||
.replace_as_restore(
|
||||
expected_revision,
|
||||
&prepared.envelope.config,
|
||||
&actor,
|
||||
source,
|
||||
)
|
||||
.await
|
||||
}
|
||||
None => {
|
||||
this.store
|
||||
.replace_as(expected_revision, &prepared.envelope.config, &actor)
|
||||
.await
|
||||
}
|
||||
};
|
||||
match persisted {
|
||||
Ok(true) => {}
|
||||
Ok(false) => {
|
||||
return Err(ApiError(
|
||||
|
||||
@@ -156,7 +156,7 @@ pub(super) fn fastcgi_params(
|
||||
}
|
||||
params.insert("HTTPS".into(), if context.tls { "on" } else { "off" }.into());
|
||||
params.insert("GATEWAY_INTERFACE".into(), "CGI/1.1".into());
|
||||
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.25.3".into());
|
||||
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.25.4".into());
|
||||
params.insert("REDIRECT_STATUS".into(), "200".into());
|
||||
if let Some(value) = headers.get(CONTENT_TYPE).and_then(|value| value.to_str().ok()) {
|
||||
params.insert("CONTENT_TYPE".into(), value.into());
|
||||
|
||||
@@ -180,7 +180,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.25.3</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.25.4</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.25.3</b>"));
|
||||
assert!(body.contains("RustProxy <b>0.25.4</b>"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in new issue
Block a user