Upgrade to Rocket v0.5.2 (#7890)

Upgrade Rocket to v0.5.2 which has updated all the crate dependencies.
Also adjusted some code which I changed with Rkt and during that found
some small bugs and fixed those too.

Adjusted the ETag Caching a bit.

Also updated other crates and GHA.

Signed-off-by: BlackDex <black.dex@gmail.com>
This commit is contained in:
Mathijs van Veluw authored and GitHub committed 2026-10-25 17:08:08 +01:00
1 parent e5a90968f2
commit 44c4853d37
11 files changed
+262 -260

No files matched your search

+1 -1
View File
@@ -30,7 +30,7 @@ pub use crate::api::{
},
web::catchers as web_catchers,
web::routes as web_routes,
web::{invalidate_css_cache, static_files},
web::{STATIC_FILES, invalidate_css_cache},
};
use crate::{
CONFIG,
+3 -6
View File
@@ -212,7 +212,7 @@ fn websockets_hub<'r>(
if last_received.elapsed() > WS_CLIENT_TIMEOUT {
break;
}
yield Message::Ping(create_ping());
yield Message::Ping(PING.to_vec());
}
}
}
@@ -295,7 +295,7 @@ fn anonymous_websockets_hub<'r>(ws: WebSocket, token: String, ip: ClientIp) -> R
if last_received.elapsed() > WS_CLIENT_TIMEOUT {
break;
}
yield Message::Ping(create_ping());
yield Message::Ping(PING.to_vec());
}
}
}
@@ -359,6 +359,7 @@ fn convert_option<T: Into<Value>>(option: Option<T>) -> Value {
const RECORD_SEPARATOR: u8 = 0x1e;
const INITIAL_RESPONSE: [u8; 3] = [0x7b, 0x7d, RECORD_SEPARATOR]; // {, }, <RS>
const PING: [u8; 3] = [0x02, 0x91, 0x06];
#[derive(Deserialize, Copy, Clone, Eq, PartialEq)]
struct InitialMessage<'a> {
@@ -691,10 +692,6 @@ fn create_anonymous_update(payload: Vec<(Value, Value)>, ut: UpdateType, user_id
serialize(&value)
}
fn create_ping() -> Vec<u8> {
serialize(&Value::Array(vec![6.into()]))
}
// https://github.com/bitwarden/server/blob/375af7c43b10d9da03525d41452f95de3f921541/src/Core/Enums/PushType.cs
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum UpdateType {
+62 -32
View File
@@ -1,13 +1,14 @@
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::{Arc, RwLock},
sync::{Arc, LazyLock, RwLock},
};
use rocket::{
Catcher, Route,
fs::NamedFile,
http::ContentType,
response::{Redirect, content::RawCss as Css, content::RawHtml as Html},
response::{Redirect, content::RawHtml as Html},
serde::json::Json,
};
use serde_json::Value;
@@ -79,12 +80,12 @@ pub fn invalidate_css_cache() {
}
#[get("/css/vaultwarden.css")]
fn vaultwarden_css() -> EtagCached<Css<String>> {
fn vaultwarden_css() -> EtagCached<String> {
// If reload_templates is false, and we already have the CSS Cached, return this
if !CONFIG.reload_templates()
&& let Some(cached) = CSS_CACHE.read().unwrap().as_ref()
{
return EtagCached::new(Css(cached.css.clone()), &cached.etag);
return EtagCached::new(cached.css.clone(), ContentType::CSS, &cached.etag);
}
// Else, there is either no cache, or reload_templates is true and we need to rebuild the CSS
@@ -147,7 +148,7 @@ fn vaultwarden_css() -> EtagCached<Css<String>> {
}
// Etag Caching will let the browser send us an etag to verify and send new content if needed
EtagCached::new(Css(cached.css.clone()), &cached.etag)
EtagCached::new(cached.css.clone(), ContentType::CSS, &cached.etag)
}
#[get("/")]
@@ -256,7 +257,7 @@ fn alive_head(_conn: DbConn) -> EmptyResult {
// This endpoint/function is used during development and development only.
// It allows to easily develop the admin interface by always loading the files from disk instead from a slice of bytes
// This will only be active during a debug build and only when `RELOAD_TEMPLATES` is set to `true`
// NOTE: Do not forget to add any new files added to the `static_files` function below!
// NOTE: Do not forget to add any new files added to the `STATIC_FILES` HashMap below!
#[cfg(debug_assertions)]
#[get("/vw_static/<filename>", rank = 1)]
pub async fn static_files_dev(filename: PathBuf) -> Option<NamedFile> {
@@ -276,32 +277,61 @@ pub async fn static_files_dev(filename: PathBuf) -> Option<NamedFile> {
None
}
pub struct StaticFile {
pub content_type: ContentType,
pub bytes: &'static [u8],
pub etag: String,
}
pub static STATIC_FILES: LazyLock<HashMap<&'static str, StaticFile>> = LazyLock::new(|| {
[
("404.png", ContentType::PNG, &include_bytes!("../static/images/404.png")[..]),
("mail-github.png", ContentType::PNG, &include_bytes!("../static/images/mail-github.png")[..]),
("logo-gray.png", ContentType::PNG, &include_bytes!("../static/images/logo-gray.png")[..]),
("error-x.svg", ContentType::SVG, &include_bytes!("../static/images/error-x.svg")[..]),
("hibp.png", ContentType::PNG, &include_bytes!("../static/images/hibp.png")[..]),
("vaultwarden-icon.png", ContentType::PNG, &include_bytes!("../static/images/vaultwarden-icon.png")[..]),
("vaultwarden-favicon.png", ContentType::PNG, &include_bytes!("../static/images/vaultwarden-favicon.png")[..]),
("404.css", ContentType::CSS, &include_bytes!("../static/scripts/404.css")[..]),
("admin.css", ContentType::CSS, &include_bytes!("../static/scripts/admin.css")[..]),
("admin.js", ContentType::JavaScript, &include_bytes!("../static/scripts/admin.js")[..]),
("admin_settings.js", ContentType::JavaScript, &include_bytes!("../static/scripts/admin_settings.js")[..]),
("admin_users.js", ContentType::JavaScript, &include_bytes!("../static/scripts/admin_users.js")[..]),
(
"admin_organizations.js",
ContentType::JavaScript,
&include_bytes!("../static/scripts/admin_organizations.js")[..],
),
(
"admin_diagnostics.js",
ContentType::JavaScript,
&include_bytes!("../static/scripts/admin_diagnostics.js")[..],
),
("bootstrap.css", ContentType::CSS, &include_bytes!("../static/scripts/bootstrap.css")[..]),
("bootstrap.bundle.js", ContentType::JavaScript, &include_bytes!("../static/scripts/bootstrap.bundle.js")[..]),
("jdenticon-3.3.0.js", ContentType::JavaScript, &include_bytes!("../static/scripts/jdenticon-3.3.0.js")[..]),
("datatables.js", ContentType::JavaScript, &include_bytes!("../static/scripts/datatables.js")[..]),
("datatables.css", ContentType::CSS, &include_bytes!("../static/scripts/datatables.css")[..]),
]
.into_iter()
.map(|(name, content_type, bytes)| {
(
name,
StaticFile {
content_type,
bytes,
etag: sha256_hex(bytes),
},
)
})
.collect()
});
#[get("/vw_static/<filename>", rank = 2)]
pub fn static_files(filename: &str) -> Result<(ContentType, &'static [u8]), Error> {
match filename {
"404.png" => Ok((ContentType::PNG, include_bytes!("../static/images/404.png"))),
"mail-github.png" => Ok((ContentType::PNG, include_bytes!("../static/images/mail-github.png"))),
"logo-gray.png" => Ok((ContentType::PNG, include_bytes!("../static/images/logo-gray.png"))),
"error-x.svg" => Ok((ContentType::SVG, include_bytes!("../static/images/error-x.svg"))),
"hibp.png" => Ok((ContentType::PNG, include_bytes!("../static/images/hibp.png"))),
"vaultwarden-icon.png" => Ok((ContentType::PNG, include_bytes!("../static/images/vaultwarden-icon.png"))),
"vaultwarden-favicon.png" => Ok((ContentType::PNG, include_bytes!("../static/images/vaultwarden-favicon.png"))),
"404.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/404.css"))),
"admin.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/admin.css"))),
"admin.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/admin.js"))),
"admin_settings.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/admin_settings.js"))),
"admin_users.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/admin_users.js"))),
"admin_organizations.js" => {
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/admin_organizations.js")))
}
"admin_diagnostics.js" => {
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/admin_diagnostics.js")))
}
"bootstrap.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/bootstrap.css"))),
"bootstrap.bundle.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap.bundle.js"))),
"jdenticon-3.3.0.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jdenticon-3.3.0.js"))),
"datatables.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))),
"datatables.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))),
_ => err!(format!("Static file not found: {filename}")),
pub fn static_files(filename: &str) -> Result<EtagCached<&'static [u8]>, Error> {
if let Some(file) = STATIC_FILES.get(filename) {
Ok(EtagCached::new(file.bytes, file.content_type.clone(), &file.etag))
} else {
err!(format!("Static file not found: {filename}"))
}
}
+2 -2
View File
@@ -425,12 +425,12 @@ macro_rules! err_json {
macro_rules! err_handler {
($expr:expr) => {{
error!(target: "auth", "Unauthorized Error: {}", $expr);
return ::rocket::request::Outcome::Error((rocket::http::Status::Unauthorized, $expr));
return ::rocket::request::Outcome::Error((::rocket::http::Status::Unauthorized, $expr));
}};
($usr_msg:expr, $log_value:expr) => {{
let usr_msg = $usr_msg;
let log_value = $log_value;
error!(target: "auth", "Unauthorized Error: {usr_msg}. {log_value}");
return ::rocket::request::Outcome::Error((rocket::http::Status::Unauthorized, usr_msg));
return ::rocket::request::Outcome::Error((::rocket::http::Status::Unauthorized, usr_msg));
}};
}
+2 -2
View File
@@ -738,8 +738,8 @@ async fn send_email(address: &str, subject: &str, body_html: String, body_text:
let smtp_from = Address::from_str(&CONFIG.smtp_from())?;
let body = if CONFIG.smtp_embed_images() {
let logo_gray_body = Body::new(crate::api::static_files("logo-gray.png").unwrap().1.to_vec());
let mail_github_body = Body::new(crate::api::static_files("mail-github.png").unwrap().1.to_vec());
let logo_gray_body = Body::new(crate::api::STATIC_FILES["logo-gray.png"].bytes.to_vec());
let mail_github_body = Body::new(crate::api::STATIC_FILES["mail-github.png"].bytes.to_vec());
MultiPart::alternative().singlepart(SinglePart::plain(body_text)).multipart(
MultiPart::related()
.singlepart(SinglePart::html(body_html))
+1 -1
View File
@@ -595,7 +595,7 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
.manage(Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS))
.attach(util::AppHeaders())
.attach(util::Cors())
.attach(util::BetterLogging(extra_debug))
.attach(util::BetterLogging::new(extra_debug, basepath))
.ignite()
.await?;
+59 -28
View File
@@ -259,14 +259,16 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for Cache
pub struct EtagCached<R> {
response: R,
content_type: ContentType,
etag: String,
}
impl<R> EtagCached<R> {
/// An `etag` response should always be quoted
pub fn new(response: R, etag: &str) -> Self {
pub fn new(response: R, content_type: ContentType, etag: &str) -> Self {
Self {
response,
content_type,
etag: format!("\"{etag}\""),
}
}
@@ -288,7 +290,8 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for EtagC
self.response.respond_to(request)?
};
// Both 200 (OK) and 304 (Not Modified) need to return the etag and cache-control
// Both 200 (OK) and 304 (Not Modified) need to return the Content-Type Etag and Cache-Control
res.set_header(self.content_type);
res.set_raw_header("Etag", self.etag);
res.set_raw_header("Cache-Control", "public, no-cache");
Ok(res)
@@ -299,19 +302,55 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for EtagC
// Effectively ignores, any static file route, and the alive endpoint
const LOGGED_ROUTES: [&str; 7] = ["/api", "/admin", "/identity", "/icons", "/attachments", "/events", "/notifications"];
// Boolean is extra debug, when true, we ignore the whitelist above and also print the mounts
pub struct BetterLogging(pub bool);
// When extra is true, we ignore the whitelist above and also print the mounts
pub struct BetterLogging {
extra: bool,
log_requests: bool,
log_response: bool,
domain_path: String,
}
impl BetterLogging {
pub fn new(extra: bool, domain_path: &str) -> Self {
Self {
extra,
log_requests: log_enabled!(target: "request", log::Level::Info),
log_response: log_enabled!(target: "response", log::Level::Info),
domain_path: rocket::http::RawStr::new(&domain_path).percent_decode_lossy().into_owned(),
}
}
fn log_path<'p>(&self, method: Method, path: &'p rocket::http::RawStr) -> Option<std::borrow::Cow<'p, str>> {
if !self.extra && method == Method::Options {
return None;
}
let path = path.percent_decode_lossy();
if self.extra {
return Some(path);
}
let subpath = path.strip_prefix(&self.domain_path).unwrap_or(&path);
LOGGED_ROUTES.iter().any(|r| subpath.starts_with(r)).then_some(path)
}
}
#[rocket::async_trait]
impl Fairing for BetterLogging {
fn info(&self) -> Info {
let mut kind = Kind::Liftoff;
if self.log_requests {
kind = kind | Kind::Request;
}
if self.log_response {
kind = kind | Kind::Response;
}
Info {
name: "Better Logging",
kind: Kind::Liftoff | Kind::Request | Kind::Response,
kind,
}
}
async fn on_liftoff(&self, rocket: &Rocket<Orbit>) {
if self.0 {
if self.extra {
info!(target: "routes", "Routes loaded:");
let mut routes: Vec<_> = rocket.routes().collect();
routes.sort_by_key(|r| r.uri.path());
@@ -335,36 +374,28 @@ impl Fairing for BetterLogging {
}
async fn on_request(&self, request: &mut Request<'_>, _data: &mut Data<'_>) {
let method = request.method();
if !self.0 && method == Method::Options {
return;
}
let uri = request.uri();
let uri_path = uri.path();
let uri_path_str = uri_path.url_decode_lossy();
let uri_subpath = uri_path_str.strip_prefix(&CONFIG.domain_path()).unwrap_or(&uri_path_str);
if self.0 || LOGGED_ROUTES.iter().any(|r| uri_subpath.starts_with(r)) {
match uri.query() {
Some(q) => info!(target: "request", "{method} {uri_path_str}?{}", &q[..q.len().min(30)]),
None => info!(target: "request", "{method} {uri_path_str}"),
let method = request.method();
let Some(path) = self.log_path(method, uri.path().raw()) else {
return;
};
match uri.query() {
Some(q) => {
let q = q.as_str();
info!(target: "request", "{method} {}?{}", path.escape_debug(), q[..q.floor_char_boundary(30)].escape_debug());
}
None => info!(target: "request", "{method} {}", path.escape_debug()),
}
}
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
if !self.0 && request.method() == Method::Options {
if self.log_path(request.method(), request.uri().path().raw()).is_none() {
return;
}
let uri_path = request.uri().path();
let uri_path_str = uri_path.url_decode_lossy();
let uri_subpath = uri_path_str.strip_prefix(&CONFIG.domain_path()).unwrap_or(&uri_path_str);
if self.0 || LOGGED_ROUTES.iter().any(|r| uri_subpath.starts_with(r)) {
let status = response.status();
if let Some(route) = request.route() {
info!(target: "response", "{route} => {status}");
} else {
info!(target: "response", "{status}");
}
let status = response.status();
match request.route() {
Some(r) => info!(target: "response", "{r} => {status}"),
None => info!(target: "response", "{status}"),
}
}
}