2018-12-30 23:34:31 +01:00
//
2019-01-19 21:36:34 +01:00
// Web Headers and caching
2018-12-30 23:34:31 +01:00
//
2025-01-09 18:37:23 +01:00
use std ::{ collections ::HashMap , io ::Cursor , path ::Path } ;
2018-02-10 01:00:55 +01:00
2024-01-27 02:43:26 +01:00
use num_traits ::ToPrimitive ;
2020-07-14 18:00:09 +02:00
use rocket ::{
fairing ::{ Fairing , Info , Kind } ,
2021-11-07 18:53:39 +01:00
http ::{ ContentType , Header , HeaderMap , Method , Status } ,
2020-07-14 18:00:09 +02:00
response ::{ self , Responder } ,
2021-11-07 18:53:39 +01:00
Data , Orbit , Request , Response , Rocket ,
2020-07-14 18:00:09 +02:00
} ;
2022-03-20 18:51:24 +01:00
use tokio ::{
runtime ::Handle ,
time ::{ sleep , Duration } ,
} ;
2021-12-28 17:24:42 +01:00
2020-02-04 22:14:50 +01:00
use crate ::CONFIG ;
2018-12-30 23:34:31 +01:00
pub struct AppHeaders ( ) ;
2018-12-23 22:37:02 +01:00
2021-11-07 18:53:39 +01:00
#[ rocket::async_trait ]
2018-12-23 22:37:02 +01:00
impl Fairing for AppHeaders {
fn info ( & self ) -> Info {
Info {
name : " Application Headers " ,
kind : Kind ::Response ,
}
}
2022-06-08 19:46:33 +02:00
async fn on_response < ' r > ( & self , req : & ' r Request < '_ > , res : & mut Response < ' r > ) {
2023-10-09 19:17:11 +02:00
let req_uri_path = req . uri ( ) . path ( ) ;
let req_headers = req . headers ( ) ;
// Check if this connection is an Upgrade/WebSocket connection and return early
// We do not want add any extra headers, this could cause issues with reverse proxies or CloudFlare
if req_uri_path . ends_with ( " notifications/hub " ) | | req_uri_path . ends_with ( " notifications/anonymous-hub " ) {
match ( req_headers . get_one ( " connection " ) , req_headers . get_one ( " upgrade " ) ) {
( Some ( c ) , Some ( u ) )
if c . to_lowercase ( ) . contains ( " upgrade " ) & & u . to_lowercase ( ) . contains ( " websocket " ) = >
{
// Remove headers which could cause websocket connection issues
res . remove_header ( " X-Frame-Options " ) ;
res . remove_header ( " X-Content-Type-Options " ) ;
2023-11-15 10:41:14 +01:00
res . remove_header ( " Permissions-Policy " ) ;
2023-10-09 19:17:11 +02:00
return ;
}
( _ , _ ) = > ( ) ,
}
}
2024-12-10 21:52:12 +01:00
// NOTE: When modifying or adding security headers be sure to also update the diagnostic checks in `src/static/scripts/admin_diagnostics.js` in `checkSecurityHeaders`
2022-06-08 19:46:33 +02:00
res . set_raw_header ( " Permissions-Policy " , " accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=() " ) ;
2018-12-23 22:37:02 +01:00
res . set_raw_header ( " Referrer-Policy " , " same-origin " ) ;
res . set_raw_header ( " X-Content-Type-Options " , " nosniff " ) ;
2024-12-10 21:52:12 +01:00
res . set_raw_header ( " X-Robots-Tag " , " noindex, nofollow " ) ;
2022-03-21 06:30:37 +01:00
// Obsolete in modern browsers, unsafe (XS-Leak), and largely replaced by CSP
res . set_raw_header ( " X-XSS-Protection " , " 0 " ) ;
2022-06-08 19:46:33 +02:00
// Do not send the Content-Security-Policy (CSP) Header and X-Frame-Options for the *-connector.html files.
// This can cause issues when some MFA requests needs to open a popup or page within the clients like WebAuthn, or Duo.
2023-10-09 19:17:11 +02:00
// This is the same behavior as upstream Bitwarden.
2022-06-08 19:46:33 +02:00
if ! req_uri_path . ends_with ( " connector.html " ) {
2022-06-20 16:26:41 +02:00
// # Frame Ancestors:
// Chrome Web Store: https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb
// Edge Add-ons: https://microsoftedge.microsoft.com/addons/detail/bitwarden-free-password/jbkfoedolllekgbhcbcoahefnbanhhlh?hl=en-US
// Firefox Browser Add-ons: https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/
// # img/child/frame src:
2023-10-09 19:17:11 +02:00
// Have I Been Pwned to allow those calls to work.
2022-06-20 16:26:41 +02:00
// # Connect src:
// Leaked Passwords check: api.pwnedpasswords.com
2023-01-10 09:41:35 +01:00
// 2FA/MFA Site check: api.2fa.directory
2022-06-20 16:26:41 +02:00
// # Mail Relay: https://bitwarden.com/blog/add-privacy-and-security-using-email-aliases-with-bitwarden/
2023-10-03 08:21:02 +02:00
// app.simplelogin.io, app.addy.io, api.fastmail.com, quack.duckduckgo.com
2022-06-08 19:46:33 +02:00
let csp = format! (
" default-src 'self'; \
2022-12-15 17:15:48 +01:00
base - uri ' self ' ; \
form - action ' self ' ; \
2022-10-12 06:25:29 +02:00
object - src ' self ' blob :; \
2023-02-01 03:26:23 +01:00
script - src ' self ' ' wasm - unsafe - eval ' ; \
2022-06-08 19:46:33 +02:00
style - src ' self ' ' unsafe - inline ' ; \
child - src ' self ' https ://*.duosecurity.com https://*.duofederal.com; \
frame - src ' self ' https ://*.duosecurity.com https://*.duofederal.com; \
2022-10-12 06:25:29 +02:00
frame - ancestors ' self ' \
chrome - extension ://nngceckbapebfimnlniiiahkandclblb \
chrome - extension ://jbkfoedolllekgbhcbcoahefnbanhhlh \
moz - extension ://* \
{ allowed_iframe_ancestors } ; \
img - src ' self ' data : \
2022-12-15 17:15:48 +01:00
https ://haveibeenpwned.com \
2022-10-12 06:25:29 +02:00
{ icon_service_csp } ; \
connect - src ' self ' \
2022-12-15 17:15:48 +01:00
https ://api.pwnedpasswords.com \
2023-01-10 09:41:35 +01:00
https ://api.2fa.directory \
2022-10-12 06:25:29 +02:00
https ://app.simplelogin.io/api/ \
2023-10-03 08:21:02 +02:00
https ://app.addy.io/api/ \
2022-10-12 06:25:29 +02:00
https ://api.fastmail.com/ \
2023-07-12 10:50:41 +02:00
https ://api.forwardemail.net \
2024-12-15 00:27:20 +01:00
{ allowed_connect_src } ; \
2022-10-12 06:25:29 +02:00
" ,
icon_service_csp = CONFIG . _icon_service_csp ( ) ,
2024-12-15 00:27:20 +01:00
allowed_iframe_ancestors = CONFIG . allowed_iframe_ancestors ( ) ,
allowed_connect_src = CONFIG . allowed_connect_src ( ) ,
2022-06-08 19:46:33 +02:00
) ;
res . set_raw_header ( " Content-Security-Policy " , csp ) ;
res . set_raw_header ( " X-Frame-Options " , " SAMEORIGIN " ) ;
} else {
// It looks like this header get's set somewhere else also, make sure this is not sent for these files, it will cause MFA issues.
res . remove_header ( " X-Frame-Options " ) ;
}
2018-12-23 22:37:02 +01:00
// Disable cache unless otherwise specified
if ! res . headers ( ) . contains ( " cache-control " ) {
res . set_raw_header ( " Cache-Control " , " no-cache, no-store, max-age=0 " ) ;
}
}
}
2021-03-27 15:26:32 +01:00
pub struct Cors ( ) ;
2019-09-01 13:00:12 +02:00
2021-03-27 15:26:32 +01:00
impl Cors {
2021-11-19 17:50:16 +01:00
fn get_header ( headers : & HeaderMap < '_ > , name : & str ) -> String {
2019-09-02 21:13:12 +02:00
match headers . get_one ( name ) {
Some ( h ) = > h . to_string ( ) ,
2022-11-04 12:56:02 +01:00
_ = > String ::new ( ) ,
2019-09-02 21:13:12 +02:00
}
}
2019-10-01 17:26:58 +02:00
2021-03-07 09:35:08 +01:00
// Check a request's `Origin` header against the list of allowed origins.
// If a match exists, return it. Otherwise, return None.
2021-11-19 17:50:16 +01:00
fn get_allowed_origin ( headers : & HeaderMap < '_ > ) -> Option < String > {
2021-03-27 15:26:32 +01:00
let origin = Cors ::get_header ( headers , " Origin " ) ;
2021-03-07 09:35:08 +01:00
let domain_origin = CONFIG . domain_origin ( ) ;
let safari_extension_origin = " file:// " ;
if origin = = domain_origin | | origin = = safari_extension_origin {
Some ( origin )
} else {
None
2019-10-01 17:26:58 +02:00
}
}
2019-09-02 21:13:12 +02:00
}
2021-11-07 18:53:39 +01:00
#[ rocket::async_trait ]
2021-03-27 15:26:32 +01:00
impl Fairing for Cors {
2019-09-01 13:00:12 +02:00
fn info ( & self ) -> Info {
Info {
2021-03-27 15:26:32 +01:00
name : " Cors " ,
2019-12-06 22:19:07 +01:00
kind : Kind ::Response ,
2019-09-01 13:00:12 +02:00
}
}
2021-11-07 18:53:39 +01:00
async fn on_response < ' r > ( & self , request : & ' r Request < '_ > , response : & mut Response < ' r > ) {
2019-09-02 21:13:12 +02:00
let req_headers = request . headers ( ) ;
2021-03-27 15:26:32 +01:00
if let Some ( origin ) = Cors ::get_allowed_origin ( req_headers ) {
2021-03-07 09:35:08 +01:00
response . set_header ( Header ::new ( " Access-Control-Allow-Origin " , origin ) ) ;
}
2019-09-02 21:13:12 +02:00
2021-03-07 09:35:08 +01:00
// Preflight request
2019-10-01 17:26:58 +02:00
if request . method ( ) = = Method ::Options {
2021-03-27 15:26:32 +01:00
let req_allow_headers = Cors ::get_header ( req_headers , " Access-Control-Request-Headers " ) ;
let req_allow_method = Cors ::get_header ( req_headers , " Access-Control-Request-Method " ) ;
2019-09-01 13:00:12 +02:00
2019-09-23 07:44:44 +02:00
response . set_header ( Header ::new ( " Access-Control-Allow-Methods " , req_allow_method ) ) ;
2019-09-02 21:13:12 +02:00
response . set_header ( Header ::new ( " Access-Control-Allow-Headers " , req_allow_headers ) ) ;
2019-09-01 13:00:12 +02:00
response . set_header ( Header ::new ( " Access-Control-Allow-Credentials " , " true " ) ) ;
2019-09-02 21:13:12 +02:00
response . set_status ( Status ::Ok ) ;
2019-09-01 13:00:12 +02:00
response . set_header ( ContentType ::Plain ) ;
2021-11-07 18:53:39 +01:00
response . set_sized_body ( Some ( 0 ) , Cursor ::new ( " " ) ) ;
2019-09-01 13:00:12 +02:00
}
}
}
2021-12-28 17:24:42 +01:00
pub struct Cached < R > {
response : R ,
is_immutable : bool ,
ttl : u64 ,
}
2019-01-19 21:36:34 +01:00
impl < R > Cached < R > {
2021-12-28 17:24:42 +01:00
pub fn long ( response : R , is_immutable : bool ) -> Cached < R > {
Self {
response ,
is_immutable ,
ttl : 604800 , // 7 days
}
2019-01-19 21:36:34 +01:00
}
2021-12-28 17:24:42 +01:00
pub fn short ( response : R , is_immutable : bool ) -> Cached < R > {
Self {
response ,
is_immutable ,
ttl : 600 , // 10 minutes
}
2021-03-27 14:30:40 +01:00
}
2021-12-28 17:24:42 +01:00
pub fn ttl ( response : R , ttl : u64 , is_immutable : bool ) -> Cached < R > {
Self {
response ,
is_immutable ,
2021-12-28 22:54:09 +01:00
ttl ,
2021-12-28 17:24:42 +01:00
}
2019-01-19 21:36:34 +01:00
}
}
2021-11-07 18:53:39 +01:00
impl < ' r , R : ' r + Responder < ' r , 'static > + Send > Responder < ' r , 'static > for Cached < R > {
fn respond_to ( self , request : & ' r Request < '_ > ) -> response ::Result < 'static > {
let mut res = self . response . respond_to ( request ) ? ;
2021-12-28 17:24:42 +01:00
let cache_control_header = if self . is_immutable {
format! ( " public, immutable, max-age= {} " , self . ttl )
} else {
format! ( " public, max-age= {} " , self . ttl )
} ;
2021-11-07 18:53:39 +01:00
res . set_raw_header ( " Cache-Control " , cache_control_header ) ;
2021-12-28 17:24:42 +01:00
2024-09-23 20:25:32 +02:00
let time_now = Local ::now ( ) ;
2024-03-19 19:47:30 +01:00
let expiry_time = time_now + chrono ::TimeDelta ::try_seconds ( self . ttl . try_into ( ) . unwrap ( ) ) . unwrap ( ) ;
2021-11-07 18:53:39 +01:00
res . set_raw_header ( " Expires " , format_datetime_http ( & expiry_time ) ) ;
Ok ( res )
2019-01-19 21:36:34 +01:00
}
}
2020-02-19 06:27:00 +01:00
// Log all the routes from the main paths list, and the attachments endpoint
2019-12-06 22:19:07 +01:00
// Effectively ignores, any static file route, and the alive endpoint
2023-04-30 17:18:12 +02:00
const LOGGED_ROUTES : [ & str ; 7 ] = [ " /api " , " /admin " , " /identity " , " /icons " , " /attachments " , " /events " , " /notifications " ] ;
2019-12-06 22:19:07 +01:00
// Boolean is extra debug, when true, we ignore the whitelist above and also print the mounts
pub struct BetterLogging ( pub bool ) ;
2021-11-07 18:53:39 +01:00
#[ rocket::async_trait ]
2019-12-06 22:19:07 +01:00
impl Fairing for BetterLogging {
fn info ( & self ) -> Info {
Info {
name : " Better Logging " ,
2021-11-07 18:53:39 +01:00
kind : Kind ::Liftoff | Kind ::Request | Kind ::Response ,
2019-12-06 22:19:07 +01:00
}
}
2021-11-07 18:53:39 +01:00
async fn on_liftoff ( & self , rocket : & Rocket < Orbit > ) {
2019-12-06 22:19:07 +01:00
if self . 0 {
info! ( target : " routes " , " Routes loaded: " ) ;
2020-02-04 22:14:50 +01:00
let mut routes : Vec < _ > = rocket . routes ( ) . collect ( ) ;
2023-05-06 19:46:55 +02:00
routes . sort_by_key ( | r | r . uri . path ( ) ) ;
2020-02-04 22:14:50 +01:00
for route in routes {
2019-12-06 22:19:07 +01:00
if route . rank < 0 {
info! ( target : " routes " , " {:<6} {} " , route . method , route . uri ) ;
} else {
info! ( target : " routes " , " {:<6} {} [{}] " , route . method , route . uri , route . rank ) ;
}
}
}
let config = rocket . config ( ) ;
2021-04-06 22:54:42 +02:00
let scheme = if config . tls_enabled ( ) {
" https "
} else {
" http "
} ;
2019-12-06 22:19:07 +01:00
let addr = format! ( " {} :// {} : {} " , & scheme , & config . address , & config . port ) ;
info! ( target : " start " , " Rocket has launched from {} " , addr ) ;
}
2021-11-07 18:53:39 +01:00
async fn on_request ( & self , request : & mut Request < '_ > , _data : & mut Data < '_ > ) {
2019-12-06 22:55:29 +01:00
let method = request . method ( ) ;
if ! self . 0 & & method = = Method ::Options {
return ;
}
2019-12-29 15:29:46 +01:00
let uri = request . uri ( ) ;
let uri_path = uri . path ( ) ;
2021-11-07 18:53:39 +01:00
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 ) ;
2020-02-19 06:27:00 +01:00
if self . 0 | | LOGGED_ROUTES . iter ( ) . any ( | r | uri_subpath . starts_with ( r ) ) {
2019-12-29 15:29:46 +01:00
match uri . query ( ) {
2021-11-07 18:53:39 +01:00
Some ( q ) = > info! ( target : " request " , " {} {}?{} " , method , uri_path_str , & q [ .. q . len ( ) . min ( 30 ) ] ) ,
None = > info! ( target : " request " , " {} {} " , method , uri_path_str ) ,
2019-12-29 15:29:46 +01:00
} ;
2019-12-06 22:19:07 +01:00
}
}
2021-11-07 18:53:39 +01:00
async fn on_response < ' r > ( & self , request : & ' r Request < '_ > , response : & mut Response < ' r > ) {
2019-12-06 22:55:29 +01:00
if ! self . 0 & & request . method ( ) = = Method ::Options {
return ;
}
2020-07-21 07:33:13 +02:00
let uri_path = request . uri ( ) . path ( ) ;
2021-11-07 18:53:39 +01:00
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 ) ;
2020-02-19 06:27:00 +01:00
if self . 0 | | LOGGED_ROUTES . iter ( ) . any ( | r | uri_subpath . starts_with ( r ) ) {
2019-12-06 22:19:07 +01:00
let status = response . status ( ) ;
2021-11-07 18:53:39 +01:00
if let Some ( ref route ) = request . route ( ) {
info! ( target : " response " , " {} => {} " , route , status )
2019-12-06 22:19:07 +01:00
} else {
2021-11-07 18:53:39 +01:00
info! ( target : " response " , " {} " , status )
2019-12-06 22:19:07 +01:00
}
}
}
}
2024-01-27 02:43:26 +01:00
pub fn get_display_size ( size : i64 ) -> String {
2021-11-05 19:18:54 +01:00
const UNITS : [ & str ; 6 ] = [ " bytes " , " KB " , " MB " , " GB " , " TB " , " PB " ] ;
2024-01-27 02:43:26 +01:00
// If we're somehow too big for a f64, just return the size in bytes
let Some ( mut size ) = size . to_f64 ( ) else {
return format! ( " {size} bytes " ) ;
} ;
2018-02-15 00:40:34 +01:00
let mut unit_counter = 0 ;
loop {
if size > 1024. {
size / = 1024. ;
unit_counter + = 1 ;
} else {
break ;
}
2018-12-30 23:34:31 +01:00
}
2018-02-15 00:40:34 +01:00
2020-05-22 12:10:56 +02:00
format! ( " {:.2} {} " , size , UNITS [ unit_counter ] )
2018-02-15 00:40:34 +01:00
}
2018-12-07 14:32:40 +01:00
pub fn get_uuid ( ) -> String {
uuid ::Uuid ::new_v4 ( ) . to_string ( )
}
2018-12-30 23:34:31 +01:00
//
// String util methods
//
2018-02-10 01:00:55 +01:00
2018-12-30 23:34:31 +01:00
use std ::str ::FromStr ;
2018-02-10 01:00:55 +01:00
2022-09-24 18:27:13 +02:00
#[ inline ]
2018-02-10 01:00:55 +01:00
pub fn upcase_first ( s : & str ) -> String {
let mut c = s . chars ( ) ;
match c . next ( ) {
None = > String ::new ( ) ,
Some ( f ) = > f . to_uppercase ( ) . collect ::< String > ( ) + c . as_str ( ) ,
}
}
2022-09-24 18:27:13 +02:00
#[ inline ]
pub fn lcase_first ( s : & str ) -> String {
let mut c = s . chars ( ) ;
match c . next ( ) {
None = > String ::new ( ) ,
Some ( f ) = > f . to_lowercase ( ) . collect ::< String > ( ) + c . as_str ( ) ,
}
}
2020-07-14 18:00:09 +02:00
pub fn try_parse_string < S , T > ( string : Option < S > ) -> Option < T >
2018-12-30 23:34:31 +01:00
where
S : AsRef < str > ,
T : FromStr ,
{
2020-07-14 18:00:09 +02:00
if let Some ( Ok ( value ) ) = string . map ( | s | s . as_ref ( ) . parse ::< T > ( ) ) {
2018-02-10 01:00:55 +01:00
Some ( value )
} else {
None
}
}
2018-12-30 23:34:31 +01:00
//
// Env methods
//
2018-09-13 20:59:51 +02:00
use std ::env ;
2021-03-31 22:18:35 +02:00
pub fn get_env_str_value ( key : & str ) -> Option < String > {
2022-12-29 14:11:52 +01:00
let key_file = format! ( " {key} _FILE " ) ;
2020-11-29 02:31:49 +01:00
let value_from_env = env ::var ( key ) ;
let value_file = env ::var ( & key_file ) ;
match ( value_from_env , value_file ) {
2022-12-29 14:11:52 +01:00
( Ok ( _ ) , Ok ( _ ) ) = > panic! ( " You should not define both {key} and {key_file} ! " ) ,
2020-11-29 02:31:49 +01:00
( Ok ( v_env ) , Err ( _ ) ) = > Some ( v_env ) ,
2024-03-17 15:11:20 +01:00
( Err ( _ ) , Ok ( v_file ) ) = > match std ::fs ::read_to_string ( v_file ) {
2020-11-29 02:31:49 +01:00
Ok ( content ) = > Some ( content . trim ( ) . to_string ( ) ) ,
2022-12-29 14:11:52 +01:00
Err ( e ) = > panic! ( " Failed to load {key} : {e:?} " ) ,
2020-11-29 02:31:49 +01:00
} ,
2021-03-31 22:18:35 +02:00
_ = > None ,
2020-11-29 02:31:49 +01:00
}
}
2018-12-30 23:34:31 +01:00
pub fn get_env < V > ( key : & str ) -> Option < V >
where
V : FromStr ,
{
2020-11-29 02:31:49 +01:00
try_parse_string ( get_env_str_value ( key ) )
2018-09-13 20:59:51 +02:00
}
2020-01-20 22:28:54 +01:00
pub fn get_env_bool ( key : & str ) -> Option < bool > {
2021-11-05 19:18:54 +01:00
const TRUE_VALUES : & [ & str ] = & [ " true " , " t " , " yes " , " y " , " 1 " ] ;
const FALSE_VALUES : & [ & str ] = & [ " false " , " f " , " no " , " n " , " 0 " ] ;
2020-11-29 02:31:49 +01:00
match get_env_str_value ( key ) {
Some ( val ) if TRUE_VALUES . contains ( & val . to_lowercase ( ) . as_ref ( ) ) = > Some ( true ) ,
Some ( val ) if FALSE_VALUES . contains ( & val . to_lowercase ( ) . as_ref ( ) ) = > Some ( false ) ,
2020-01-20 22:28:54 +01:00
_ = > None ,
}
}
2018-12-30 23:34:31 +01:00
//
// Date util methods
//
2018-02-10 01:00:55 +01:00
2020-12-12 07:47:54 +01:00
use chrono ::{ DateTime , Local , NaiveDateTime , TimeZone } ;
2018-02-10 01:00:55 +01:00
2020-12-12 07:47:54 +01:00
/// Formats a UTC-offset `NaiveDateTime` in the format used by Bitwarden API
/// responses with "date" fields (`CreationDate`, `RevisionDate`, etc.).
pub fn format_date ( dt : & NaiveDateTime ) -> String {
2024-11-09 19:58:10 +01:00
dt . and_utc ( ) . to_rfc3339_opts ( chrono ::SecondsFormat ::Micros , true )
}
/// Validates and formats a RFC3339 timestamp
/// If parsing fails it will return the start of the unix datetime
pub fn validate_and_format_date ( dt : & str ) -> String {
match DateTime ::parse_from_rfc3339 ( dt ) {
Ok ( dt ) = > dt . to_rfc3339_opts ( chrono ::SecondsFormat ::Micros , true ) ,
_ = > String ::from ( " 1970-01-01T00:00:00.000000Z " ) ,
}
2020-12-12 07:47:54 +01:00
}
/// Formats a `DateTime<Local>` using the specified format string.
///
/// For a `DateTime<Local>`, the `%Z` specifier normally formats as the
/// time zone's UTC offset (e.g., `+00:00`). In this function, if the
/// `TZ` environment variable is set, then `%Z` instead formats as the
/// abbreviation for that time zone (e.g., `UTC`).
pub fn format_datetime_local ( dt : & DateTime < Local > , fmt : & str ) -> String {
// Try parsing the `TZ` environment variable to enable formatting `%Z` as
// a time zone abbreviation.
if let Ok ( tz ) = env ::var ( " TZ " ) {
2021-11-05 19:18:54 +01:00
if let Ok ( tz ) = tz . parse ::< chrono_tz ::Tz > ( ) {
2020-12-12 07:47:54 +01:00
return dt . with_timezone ( & tz ) . format ( fmt ) . to_string ( ) ;
}
}
// Otherwise, fall back to formatting `%Z` as a UTC offset.
dt . format ( fmt ) . to_string ( )
}
2018-02-10 01:00:55 +01:00
2020-12-12 07:47:54 +01:00
/// Formats a UTC-offset `NaiveDateTime` as a datetime in the local time zone.
///
/// This function basically converts the `NaiveDateTime` to a `DateTime<Local>`,
/// and then calls [format_datetime_local](crate::util::format_datetime_local).
pub fn format_naive_datetime_local ( dt : & NaiveDateTime , fmt : & str ) -> String {
format_datetime_local ( & Local . from_utc_datetime ( dt ) , fmt )
2018-02-10 01:00:55 +01:00
}
2018-06-01 00:18:50 +02:00
2021-12-29 17:17:38 +01:00
/// Formats a `DateTime<Local>` as required for HTTP
///
/// https://httpwg.org/specs/rfc7231.html#http.date
pub fn format_datetime_http ( dt : & DateTime < Local > ) -> String {
2023-08-28 16:48:42 +02:00
let expiry_time = DateTime ::< chrono ::Utc > ::from_naive_utc_and_offset ( dt . naive_utc ( ) , chrono ::Utc ) ;
2021-12-29 17:17:38 +01:00
// HACK: HTTP expects the date to always be GMT (UTC) rather than giving an
// offset (which would always be 0 in UTC anyway)
2021-12-29 17:29:42 +01:00
expiry_time . to_rfc2822 ( ) . replace ( " +0000 " , " GMT " )
2021-12-29 17:17:38 +01:00
}
2022-11-20 19:15:45 +01:00
pub fn parse_date ( date : & str ) -> NaiveDateTime {
2024-11-09 19:58:10 +01:00
DateTime ::parse_from_rfc3339 ( date ) . unwrap ( ) . naive_utc ( )
2022-11-20 19:15:45 +01:00
}
2021-02-27 04:40:12 +01:00
//
// Deployment environment methods
//
2024-02-02 21:44:19 +01:00
/// Returns true if the program is running in Docker, Podman or Kubernetes.
pub fn is_running_in_container ( ) -> bool {
Path ::new ( " /.dockerenv " ) . exists ( )
| | Path ::new ( " /run/.containerenv " ) . exists ( )
| | Path ::new ( " /run/secrets/kubernetes.io " ) . exists ( )
| | Path ::new ( " /var/run/secrets/kubernetes.io " ) . exists ( )
2021-02-27 04:40:12 +01:00
}
2024-02-02 21:44:19 +01:00
/// Simple check to determine on which container base image vaultwarden is running.
2021-10-08 00:01:24 +02:00
/// We build images based upon Debian or Alpine, so these we check here.
2024-02-02 21:44:19 +01:00
pub fn container_base_image ( ) -> & 'static str {
2021-10-08 00:01:24 +02:00
if Path ::new ( " /etc/debian_version " ) . exists ( ) {
2022-12-28 20:05:10 +01:00
" Debian "
2021-10-08 00:01:24 +02:00
} else if Path ::new ( " /etc/alpine-release " ) . exists ( ) {
2022-12-28 20:05:10 +01:00
" Alpine "
2021-10-08 00:01:24 +02:00
} else {
2022-12-28 20:05:10 +01:00
" Unknown "
2021-10-08 00:01:24 +02:00
}
}
2024-09-01 15:52:29 +02:00
#[ derive(Deserialize) ]
struct WebVaultVersion {
version : String ,
}
pub fn get_web_vault_version ( ) -> String {
let version_files = [
format! ( " {} /vw-version.json " , CONFIG . web_vault_folder ( ) ) ,
format! ( " {} /version.json " , CONFIG . web_vault_folder ( ) ) ,
] ;
for version_file in version_files {
if let Ok ( version_str ) = std ::fs ::read_to_string ( & version_file ) {
if let Ok ( version ) = serde_json ::from_str ::< WebVaultVersion > ( & version_str ) {
return String ::from ( version . version . trim_start_matches ( 'v' ) ) ;
}
}
}
String ::from ( " Version file missing " )
}
2018-12-30 23:34:31 +01:00
//
// Deserialization methods
//
2018-06-01 00:18:50 +02:00
2018-06-12 23:01:14 +02:00
use std ::fmt ;
2018-06-01 00:18:50 +02:00
2018-06-12 23:01:14 +02:00
use serde ::de ::{ self , DeserializeOwned , Deserializer , MapAccess , SeqAccess , Visitor } ;
2024-04-06 13:55:10 +02:00
use serde_json ::Value ;
2018-07-12 21:46:50 +02:00
pub type JsonMap = serde_json ::Map < String , Value > ;
2018-06-01 00:18:50 +02:00
2021-11-05 19:18:54 +01:00
#[ derive(Serialize, Deserialize) ]
2024-06-23 21:31:02 +02:00
pub struct LowerCase < T : DeserializeOwned > {
#[ serde(deserialize_with = " lowercase_deserialize " ) ]
2018-06-12 23:01:14 +02:00
#[ serde(flatten) ]
pub data : T ,
}
2024-06-23 21:31:02 +02:00
impl Default for LowerCase < Value > {
fn default ( ) -> Self {
Self {
data : Value ::Null ,
}
}
}
2018-12-30 23:34:31 +01:00
// https://github.com/serde-rs/serde/issues/586
2024-06-23 21:31:02 +02:00
pub fn lowercase_deserialize < ' de , T , D > ( deserializer : D ) -> Result < T , D ::Error >
2018-12-30 23:34:31 +01:00
where
T : DeserializeOwned ,
D : Deserializer < ' de > ,
2018-06-01 00:18:50 +02:00
{
2024-06-23 21:31:02 +02:00
let d = deserializer . deserialize_any ( LowerCaseVisitor ) ? ;
2018-06-12 23:01:14 +02:00
T ::deserialize ( d ) . map_err ( de ::Error ::custom )
2018-06-01 00:18:50 +02:00
}
2024-06-23 21:31:02 +02:00
struct LowerCaseVisitor ;
2018-06-12 23:01:14 +02:00
2024-06-23 21:31:02 +02:00
impl < ' de > Visitor < ' de > for LowerCaseVisitor {
2018-06-12 23:01:14 +02:00
type Value = Value ;
2021-11-19 17:50:16 +01:00
fn expecting ( & self , formatter : & mut fmt ::Formatter < '_ > ) -> fmt ::Result {
2018-06-12 23:01:14 +02:00
formatter . write_str ( " an object or an array " )
}
fn visit_map < A > ( self , mut map : A ) -> Result < Self ::Value , A ::Error >
2018-12-30 23:34:31 +01:00
where
A : MapAccess < ' de > ,
2018-06-12 23:01:14 +02:00
{
2018-07-12 21:46:50 +02:00
let mut result_map = JsonMap ::new ( ) ;
2018-06-12 23:01:14 +02:00
while let Some ( ( key , value ) ) = map . next_entry ( ) ? {
2024-06-23 21:31:02 +02:00
result_map . insert ( _process_key ( key ) , convert_json_key_lcase_first ( value ) ) ;
2018-06-12 23:01:14 +02:00
}
Ok ( Value ::Object ( result_map ) )
}
fn visit_seq < A > ( self , mut seq : A ) -> Result < Self ::Value , A ::Error >
2018-12-30 23:34:31 +01:00
where
A : SeqAccess < ' de > ,
{
2018-06-12 23:01:14 +02:00
let mut result_seq = Vec ::< Value > ::new ( ) ;
while let Some ( value ) = seq . next_element ( ) ? {
2024-06-23 21:31:02 +02:00
result_seq . push ( convert_json_key_lcase_first ( value ) ) ;
2018-06-12 23:01:14 +02:00
}
Ok ( Value ::Array ( result_seq ) )
}
}
2023-10-09 19:17:11 +02:00
// Inner function to handle a special case for the 'ssn' key.
2021-11-05 19:18:54 +01:00
// This key is part of the Identity Cipher (Social Security Number)
2018-06-12 23:01:14 +02:00
fn _process_key ( key : & str ) -> String {
match key . to_lowercase ( ) . as_ref ( ) {
2024-06-23 21:31:02 +02:00
" ssn " = > " ssn " . into ( ) ,
2024-09-23 20:25:32 +02:00
_ = > lcase_first ( key ) ,
2018-12-12 22:15:54 +01:00
}
}
2024-06-23 21:31:02 +02:00
#[ derive(Clone, Debug, Deserialize) ]
2024-01-27 02:43:26 +01:00
#[ serde(untagged) ]
pub enum NumberOrString {
Number ( i64 ) ,
String ( String ) ,
}
impl NumberOrString {
pub fn into_string ( self ) -> String {
match self {
NumberOrString ::Number ( n ) = > n . to_string ( ) ,
NumberOrString ::String ( s ) = > s ,
}
}
#[ allow(clippy::wrong_self_convention) ]
pub fn into_i32 ( & self ) -> Result < i32 , crate ::Error > {
use std ::num ::ParseIntError as PIE ;
match self {
NumberOrString ::Number ( n ) = > match n . to_i32 ( ) {
Some ( n ) = > Ok ( n ) ,
None = > err! ( " Number does not fit in i32 " ) ,
} ,
NumberOrString ::String ( s ) = > {
s . parse ( ) . map_err ( | e : PIE | crate ::Error ::new ( " Can't convert to number " , e . to_string ( ) ) )
}
}
}
#[ allow(clippy::wrong_self_convention) ]
pub fn into_i64 ( & self ) -> Result < i64 , crate ::Error > {
use std ::num ::ParseIntError as PIE ;
match self {
NumberOrString ::Number ( n ) = > Ok ( * n ) ,
NumberOrString ::String ( s ) = > {
s . parse ( ) . map_err ( | e : PIE | crate ::Error ::new ( " Can't convert to number " , e . to_string ( ) ) )
}
}
}
}
2018-12-12 22:15:54 +01:00
//
// Retry methods
//
2022-05-20 23:39:47 +02:00
pub fn retry < F , T , E > ( mut func : F , max_tries : u32 ) -> Result < T , E >
2018-12-12 22:15:54 +01:00
where
2022-05-20 23:39:47 +02:00
F : FnMut ( ) -> Result < T , E > ,
2018-12-12 22:15:54 +01:00
{
let mut tries = 0 ;
loop {
match func ( ) {
ok @ Ok ( _ ) = > return ok ,
err @ Err ( _ ) = > {
tries + = 1 ;
if tries > = max_tries {
return err ;
}
2023-03-24 22:07:50 +01:00
Handle ::current ( ) . block_on ( sleep ( Duration ::from_millis ( 500 ) ) ) ;
2018-12-12 22:15:54 +01:00
}
}
2018-06-12 23:01:14 +02:00
}
2018-06-01 00:18:50 +02:00
}
2020-10-03 22:31:52 +02:00
2022-05-20 23:39:47 +02:00
pub async fn retry_db < F , T , E > ( mut func : F , max_tries : u32 ) -> Result < T , E >
2020-10-03 22:31:52 +02:00
where
2022-05-20 23:39:47 +02:00
F : FnMut ( ) -> Result < T , E > ,
2020-10-03 22:31:52 +02:00
E : std ::error ::Error ,
{
let mut tries = 0 ;
loop {
match func ( ) {
ok @ Ok ( _ ) = > return ok ,
Err ( e ) = > {
tries + = 1 ;
if tries > = max_tries & & max_tries > 0 {
return Err ( e ) ;
}
warn! ( " Can't connect to database, retrying: {:?} " , e ) ;
2022-03-20 18:51:24 +01:00
sleep ( Duration ::from_millis ( 1_000 ) ) . await ;
2020-10-03 22:31:52 +02:00
}
}
}
}
2021-04-06 22:04:37 +02:00
2022-09-24 18:27:13 +02:00
pub fn convert_json_key_lcase_first ( src_json : Value ) -> Value {
match src_json {
Value ::Array ( elm ) = > {
let mut new_array : Vec < Value > = Vec ::with_capacity ( elm . len ( ) ) ;
for obj in elm {
new_array . push ( convert_json_key_lcase_first ( obj ) ) ;
}
Value ::Array ( new_array )
}
Value ::Object ( obj ) = > {
let mut json_map = JsonMap ::new ( ) ;
2024-06-23 21:31:02 +02:00
for ( key , value ) in obj . into_iter ( ) {
2022-09-24 18:27:13 +02:00
match ( key , value ) {
( key , Value ::Object ( elm ) ) = > {
2024-06-23 21:31:02 +02:00
let inner_value = convert_json_key_lcase_first ( Value ::Object ( elm ) ) ;
json_map . insert ( _process_key ( & key ) , inner_value ) ;
2022-09-24 18:27:13 +02:00
}
( key , Value ::Array ( elm ) ) = > {
let mut inner_array : Vec < Value > = Vec ::with_capacity ( elm . len ( ) ) ;
for inner_obj in elm {
2024-06-23 21:31:02 +02:00
inner_array . push ( convert_json_key_lcase_first ( inner_obj ) ) ;
2022-09-24 18:27:13 +02:00
}
2024-06-23 21:31:02 +02:00
json_map . insert ( _process_key ( & key ) , Value ::Array ( inner_array ) ) ;
2022-09-24 18:27:13 +02:00
}
( key , value ) = > {
2024-06-23 21:31:02 +02:00
json_map . insert ( _process_key ( & key ) , value ) ;
2022-09-24 18:27:13 +02:00
}
}
}
Value ::Object ( json_map )
}
value = > value ,
}
}
2024-01-01 15:44:02 +01:00
/// Parses the experimental client feature flags string into a HashMap.
pub fn parse_experimental_client_feature_flags ( experimental_client_feature_flags : & str ) -> HashMap < String , bool > {
2024-08-07 21:55:58 +02:00
let feature_states = experimental_client_feature_flags . split ( ',' ) . map ( | f | ( f . trim ( ) . to_owned ( ) , true ) ) . collect ( ) ;
2024-01-01 15:44:02 +01:00
feature_states
}
2024-04-27 20:25:34 +02:00
/// TODO: This is extracted from IpAddr::is_global, which is unstable:
/// https://doc.rust-lang.org/nightly/std/net/enum.IpAddr.html#method.is_global
/// Remove once https://github.com/rust-lang/rust/issues/27709 is merged
#[ allow(clippy::nonminimal_bool) ]
#[ cfg(any(not(feature = " unstable " ), test)) ]
pub fn is_global_hardcoded ( ip : std ::net ::IpAddr ) -> bool {
match ip {
std ::net ::IpAddr ::V4 ( ip ) = > {
! ( ip . octets ( ) [ 0 ] = = 0 // "This network"
| | ip . is_private ( )
| | ( ip . octets ( ) [ 0 ] = = 100 & & ( ip . octets ( ) [ 1 ] & 0b1100_0000 = = 0b0100_0000 ) ) //ip.is_shared()
| | ip . is_loopback ( )
| | ip . is_link_local ( )
// addresses reserved for future protocols (`192.0.0.0/24`)
| | ( ip . octets ( ) [ 0 ] = = 192 & & ip . octets ( ) [ 1 ] = = 0 & & ip . octets ( ) [ 2 ] = = 0 )
| | ip . is_documentation ( )
| | ( ip . octets ( ) [ 0 ] = = 198 & & ( ip . octets ( ) [ 1 ] & 0xfe ) = = 18 ) // ip.is_benchmarking()
| | ( ip . octets ( ) [ 0 ] & 240 = = 240 & & ! ip . is_broadcast ( ) ) //ip.is_reserved()
| | ip . is_broadcast ( ) )
}
std ::net ::IpAddr ::V6 ( ip ) = > {
! ( ip . is_unspecified ( )
| | ip . is_loopback ( )
// IPv4-mapped Address (`::ffff:0:0/96`)
| | matches! ( ip . segments ( ) , [ 0 , 0 , 0 , 0 , 0 , 0xffff , _ , _ ] )
// IPv4-IPv6 Translat. (`64:ff9b:1::/48`)
| | matches! ( ip . segments ( ) , [ 0x64 , 0xff9b , 1 , _ , _ , _ , _ , _ ] )
// Discard-Only Address Block (`100::/64`)
| | matches! ( ip . segments ( ) , [ 0x100 , 0 , 0 , 0 , _ , _ , _ , _ ] )
// IETF Protocol Assignments (`2001::/23`)
| | ( matches! ( ip . segments ( ) , [ 0x2001 , b , _ , _ , _ , _ , _ , _ ] if b < 0x200 )
& & ! (
// Port Control Protocol Anycast (`2001:1::1`)
u128 ::from_be_bytes ( ip . octets ( ) ) = = 0x2001_0001_0000_0000_0000_0000_0000_0001
// Traversal Using Relays around NAT Anycast (`2001:1::2`)
| | u128 ::from_be_bytes ( ip . octets ( ) ) = = 0x2001_0001_0000_0000_0000_0000_0000_0002
// AMT (`2001:3::/32`)
| | matches! ( ip . segments ( ) , [ 0x2001 , 3 , _ , _ , _ , _ , _ , _ ] )
// AS112-v6 (`2001:4:112::/48`)
| | matches! ( ip . segments ( ) , [ 0x2001 , 4 , 0x112 , _ , _ , _ , _ , _ ] )
// ORCHIDv2 (`2001:20::/28`)
| | matches! ( ip . segments ( ) , [ 0x2001 , b , _ , _ , _ , _ , _ , _ ] if ( 0x20 ..= 0x2F ) . contains ( & b ) )
) )
| | ( ( ip . segments ( ) [ 0 ] = = 0x2001 ) & & ( ip . segments ( ) [ 1 ] = = 0xdb8 ) ) // ip.is_documentation()
| | ( ( ip . segments ( ) [ 0 ] & 0xfe00 ) = = 0xfc00 ) //ip.is_unique_local()
| | ( ( ip . segments ( ) [ 0 ] & 0xffc0 ) = = 0xfe80 ) ) //ip.is_unicast_link_local()
}
}
}
#[ cfg(not(feature = " unstable " )) ]
pub use is_global_hardcoded as is_global ;
#[ cfg(feature = " unstable " ) ]
#[ inline(always) ]
pub fn is_global ( ip : std ::net ::IpAddr ) -> bool {
ip . is_global ( )
}
/// These are some tests to check that the implementations match
/// The IPv4 can be all checked in 30 seconds or so and they are correct as of nightly 2023-07-17
/// The IPV6 can't be checked in a reasonable time, so we check over a hundred billion random ones, so far correct
/// Note that the is_global implementation is subject to change as new IP RFCs are created
///
/// To run while showing progress output:
/// cargo +nightly test --release --features sqlite,unstable -- --nocapture --ignored
#[ cfg(test) ]
#[ cfg(feature = " unstable " ) ]
mod tests {
use super ::* ;
use std ::net ::IpAddr ;
#[ test ]
#[ ignore ]
fn test_ipv4_global ( ) {
for a in 0 .. u8 ::MAX {
println! ( " Iter: {} /255 " , a ) ;
for b in 0 .. u8 ::MAX {
for c in 0 .. u8 ::MAX {
for d in 0 .. u8 ::MAX {
let ip = IpAddr ::V4 ( std ::net ::Ipv4Addr ::new ( a , b , c , d ) ) ;
assert_eq! ( ip . is_global ( ) , is_global_hardcoded ( ip ) , " IP mismatch: {} " , ip )
}
}
}
}
}
#[ test ]
#[ ignore ]
fn test_ipv6_global ( ) {
use rand ::Rng ;
std ::thread ::scope ( | s | {
for t in 0 .. 16 {
let handle = s . spawn ( move | | {
let mut v = [ 0 u8 ; 16 ] ;
let mut rng = rand ::thread_rng ( ) ;
for i in 0 .. 20 {
println! ( " Thread {t} Iter: {i} /50 " ) ;
for _ in 0 .. 500_000_000 {
rng . fill ( & mut v ) ;
let ip = IpAddr ::V6 ( std ::net ::Ipv6Addr ::from ( v ) ) ;
assert_eq! ( ip . is_global ( ) , is_global_hardcoded ( ip ) , " IP mismatch: {ip} " ) ;
}
}
} ) ;
}
} ) ;
}
}