2019-01-25 18:23:51 +01:00
use std ::process ::exit ;
use std ::sync ::RwLock ;
2019-02-02 01:09:21 +01:00
use crate ::error ::Error ;
2019-02-04 01:37:25 +01:00
use crate ::util ::get_env ;
2019-02-02 01:09:21 +01:00
2019-01-25 18:23:51 +01:00
lazy_static! {
2019-02-02 01:09:21 +01:00
pub static ref CONFIG : Config = Config ::load ( ) . unwrap_or_else ( | e | {
println! ( " Error loading config: \n \t {:?} \n " , e ) ;
exit ( 12 )
} ) ;
2019-02-04 01:37:25 +01:00
pub static ref CONFIG_FILE : String = get_env ( " CONFIG_FILE " ) . unwrap_or ( " data/config.json " . into ( ) ) ;
2019-01-25 18:23:51 +01:00
}
macro_rules ! make_config {
2019-02-04 01:37:25 +01:00
( $( $( #[ doc = $doc:literal ] ) + $name :ident : $ty :ty , $editable :literal , $none_action :ident $(, $default :expr ) ? ) ; + $( ; ) ? ) = > {
2019-01-25 18:23:51 +01:00
2019-02-02 01:09:21 +01:00
pub struct Config { inner : RwLock < Inner > }
2019-01-25 18:23:51 +01:00
2019-02-02 01:09:21 +01:00
struct Inner {
templates : Handlebars ,
2019-02-02 16:47:27 +01:00
config : ConfigItems ,
2019-02-03 00:22:18 +01:00
_env : ConfigBuilder ,
_usr : ConfigBuilder ,
2019-01-25 18:23:51 +01:00
}
2019-02-03 00:22:18 +01:00
#[ derive(Debug, Clone, Default, Deserialize, Serialize) ]
2019-02-02 16:47:27 +01:00
pub struct ConfigBuilder {
2019-02-03 00:22:18 +01:00
$(
#[ serde(skip_serializing_if = " Option::is_none " ) ]
$name : Option < $ty >
) , +
2019-02-02 16:47:27 +01:00
}
impl ConfigBuilder {
fn from_env ( ) -> Self {
dotenv ::dotenv ( ) . ok ( ) ;
let mut builder = ConfigBuilder ::default ( ) ;
$(
2019-02-03 00:22:18 +01:00
builder . $name = get_env ( & stringify! ( $name ) . to_uppercase ( ) ) ;
2019-02-02 16:47:27 +01:00
) +
builder
}
fn from_file ( path : & str ) -> Result < Self , Error > {
use crate ::util ::read_file_string ;
let config_str = read_file_string ( path ) ? ;
serde_json ::from_str ( & config_str ) . map_err ( Into ::into )
}
2019-02-03 00:22:18 +01:00
/// Merges the values of both builders into a new builder.
/// If both have the same element, `other` wins.
fn merge ( & self , other : & Self ) -> Self {
let mut builder = self . clone ( ) ;
2019-02-02 16:47:27 +01:00
$(
2019-02-03 00:22:18 +01:00
if let v @ Some ( _ ) = & other . $name {
builder . $name = v . clone ( ) ;
2019-02-02 16:47:27 +01:00
}
) +
2019-02-03 00:22:18 +01:00
builder
2019-02-02 16:47:27 +01:00
}
2019-02-03 00:22:18 +01:00
/// Returns a new builder with all the elements from self,
/// except those that are equal in both sides
fn remove ( & self , other : & Self ) -> Self {
let mut builder = ConfigBuilder ::default ( ) ;
$(
if & self . $name ! = & other . $name {
builder . $name = self . $name . clone ( ) ;
}
) +
builder
}
fn build ( & self ) -> ConfigItems {
2019-02-02 16:47:27 +01:00
let mut config = ConfigItems ::default ( ) ;
let _domain_set = self . domain . is_some ( ) ;
$(
2019-02-03 00:22:18 +01:00
config . $name = make_config! { @ build self . $name . clone ( ) , & config , $none_action , $( $default ) ? } ;
2019-02-02 16:47:27 +01:00
) +
config . domain_set = _domain_set ;
config
}
}
2019-02-03 00:22:18 +01:00
#[ derive(Debug, Clone, Default) ]
pub struct ConfigItems { $( pub $name : make_config ! { @ type $ty , $none_action } ) , + }
2019-02-02 01:09:21 +01:00
#[ allow(unused) ]
impl Config {
$(
2019-02-03 00:22:18 +01:00
pub fn $name ( & self ) -> make_config ! { @ type $ty , $none_action } {
2019-02-02 01:09:21 +01:00
self . inner . read ( ) . unwrap ( ) . config . $name . clone ( )
}
) +
pub fn load ( ) -> Result < Self , Error > {
2019-02-03 00:22:18 +01:00
// Loading from env and file
let _env = ConfigBuilder ::from_env ( ) ;
2019-02-04 01:37:25 +01:00
let _usr = ConfigBuilder ::from_file ( & CONFIG_FILE ) . unwrap_or_default ( ) ;
2019-02-02 01:09:21 +01:00
2019-02-03 00:22:18 +01:00
// Create merged config, config file overwrites env
let builder = _env . merge ( & _usr ) ;
2019-02-02 16:47:27 +01:00
2019-02-03 00:22:18 +01:00
// Fill any missing with defaults
2019-02-02 16:47:27 +01:00
let config = builder . build ( ) ;
validate_config ( & config ) ? ;
2019-02-02 01:09:21 +01:00
Ok ( Config {
inner : RwLock ::new ( Inner {
templates : load_templates ( & config . templates_folder ) ,
config ,
2019-02-03 00:22:18 +01:00
_env ,
_usr ,
2019-02-02 01:09:21 +01:00
} ) ,
} )
2019-01-25 18:23:51 +01:00
}
2019-02-03 00:22:18 +01:00
pub fn prepare_json ( & self ) -> serde_json ::Value {
let cfg = {
let inner = & self . inner . read ( ) . unwrap ( ) ;
inner . _env . merge ( & inner . _usr )
} ;
2019-01-25 18:23:51 +01:00
2019-02-03 00:22:18 +01:00
fn _get_form_type ( rust_type : & str ) -> & 'static str {
match rust_type {
" String " = > " text " ,
" bool " = > " checkbox " ,
_ = > " number "
}
}
2019-02-02 16:47:27 +01:00
2019-02-04 01:37:25 +01:00
fn _get_doc ( doc : & str ) -> serde_json ::Value {
let mut split = doc . split ( " |> " ) . map ( str ::trim ) ;
json! ( {
" group " : split . next ( ) ,
" name " : split . next ( ) ,
" description " : split . next ( )
} )
}
2019-02-03 00:22:18 +01:00
json! ( [ $( {
" editable " : $editable ,
" name " : stringify ! ( $name ) ,
" value " : cfg . $name ,
" default " : make_config ! { @ default & cfg , $none_action , $( $default ) ? } ,
" type " : _get_form_type ( stringify! ( $ty ) ) ,
2019-02-04 01:37:25 +01:00
" doc " : _get_doc ( concat! ( $( $doc ) , + ) ) ,
2019-02-03 00:22:18 +01:00
} , ) + ] )
}
2019-02-02 16:47:27 +01:00
}
} ;
2019-02-03 00:22:18 +01:00
// Wrap the optionals in an Option type
( @ type $ty :ty , option ) = > { Option < $ty > } ;
( @ type $ty :ty , $id :ident ) = > { $ty } ;
// Generate the values depending on none_action
( @ build $value :expr , $config :expr , option , ) = > { $value } ;
( @ build $value :expr , $config :expr , def , $default :expr ) = > { $value . unwrap_or ( $default ) } ;
( @ build $value :expr , $config :expr , auto , $default_fn :expr ) = > { {
2019-02-02 16:47:27 +01:00
match $value {
2019-02-02 01:09:21 +01:00
Some ( v ) = > v ,
None = > {
2019-02-02 16:47:27 +01:00
let f : & Fn ( & ConfigItems ) -> _ = & $default_fn ;
f ( $config )
2019-02-02 01:09:21 +01:00
}
}
2019-02-03 00:22:18 +01:00
} } ;
// Get a default value
( @ default $config :expr , option , ) = > { serde_json ::Value ::Null } ;
( @ default $config :expr , def , $default :expr ) = > { $default } ;
( @ default $config :expr , auto , $default_fn :expr ) = > { {
let f : & Fn ( ConfigItems ) -> _ = & $default_fn ;
f ( $config . build ( ) )
} } ;
2019-02-02 16:47:27 +01:00
2019-02-02 01:09:21 +01:00
}
2019-01-25 18:23:51 +01:00
2019-02-04 01:37:25 +01:00
//STRUCTURE:
// /// Group |> Friendly Name |> Description (Optional)
// name: type, is_editable, none_action, <default_value (Optional)>
//
2019-02-03 00:22:18 +01:00
// Where none_action applied when the value wasn't provided and can be:
// def: Use a default value
// auto: Value is auto generated based on other values
// option: Value is optional
2019-02-02 01:09:21 +01:00
make_config! {
2019-02-04 01:37:25 +01:00
/// folders |> Data folder |> Main data folder
2019-02-03 00:22:18 +01:00
data_folder : String , false , def , " data " . to_string ( ) ;
2019-02-02 01:09:21 +01:00
2019-02-04 01:37:25 +01:00
/// folders |> Database URL
2019-02-03 00:22:18 +01:00
database_url : String , false , auto , | c | format! ( " {} / {} " , c . data_folder , " db.sqlite3 " ) ;
2019-02-04 01:37:25 +01:00
/// folders |> Icon chache folder
2019-02-03 00:22:18 +01:00
icon_cache_folder : String , false , auto , | c | format! ( " {} / {} " , c . data_folder , " icon_cache " ) ;
2019-02-04 01:37:25 +01:00
/// folders |> Attachments folder
2019-02-03 00:22:18 +01:00
attachments_folder : String , false , auto , | c | format! ( " {} / {} " , c . data_folder , " attachments " ) ;
2019-02-04 01:37:25 +01:00
/// folders |> Templates folder
2019-02-03 00:22:18 +01:00
templates_folder : String , false , auto , | c | format! ( " {} / {} " , c . data_folder , " templates " ) ;
2019-02-04 01:37:25 +01:00
/// folders |> Session JWT key
2019-02-03 00:22:18 +01:00
rsa_key_filename : String , false , auto , | c | format! ( " {} / {} " , c . data_folder , " rsa_key " ) ;
2019-02-02 01:09:21 +01:00
2019-02-04 01:37:25 +01:00
/// ws |> Enable websocket notifications
2019-02-03 00:22:18 +01:00
websocket_enabled : bool , false , def , false ;
2019-02-04 01:37:25 +01:00
/// ws |> Websocket address
2019-02-03 00:22:18 +01:00
websocket_address : String , false , def , " 0.0.0.0 " . to_string ( ) ;
2019-02-04 01:37:25 +01:00
/// ws |> Websocket port
2019-02-03 00:22:18 +01:00
websocket_port : u16 , false , def , 3012 ;
2019-02-02 01:09:21 +01:00
2019-02-04 01:37:25 +01:00
/// folders |> Web vault folder
2019-02-03 00:22:18 +01:00
web_vault_folder : String , false , def , " web-vault/ " . to_string ( ) ;
2019-02-04 01:37:25 +01:00
/// settings |> Enable web vault
web_vault_enabled : bool , false , def , true ;
2019-02-02 01:09:21 +01:00
2019-02-04 01:37:25 +01:00
/// icons |> Positive icon cache expiry |> Number of seconds to consider that an already cached icon is fresh. After this period, the icon will be redownloaded
2019-02-03 00:22:18 +01:00
icon_cache_ttl : u64 , true , def , 2_592_000 ;
2019-02-04 01:37:25 +01:00
/// icons |> Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again.
2019-02-03 00:22:18 +01:00
icon_cache_negttl : u64 , true , def , 259_200 ;
2019-02-02 01:09:21 +01:00
2019-02-04 01:37:25 +01:00
/// settings |> Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from $ICON_CACHE_FOLDER,
/// but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
/// otherwise it will delete them and they won't be downloaded again.
2019-02-03 00:22:18 +01:00
disable_icon_download : bool , true , def , false ;
2019-02-04 01:37:25 +01:00
/// settings |> Allow new signups |> Controls if new users can register. Note that while this is disabled, users could still be invited
2019-02-03 00:22:18 +01:00
signups_allowed : bool , true , def , true ;
2019-02-04 01:37:25 +01:00
/// settings |> Allow invitations |> Controls whether users can be invited by organization admins, even when signups are disabled
2019-02-03 00:22:18 +01:00
invitations_allowed : bool , true , def , true ;
2019-02-04 01:37:25 +01:00
/// settings |> Password iterations |> Number of server-side passwords hashing iterations. The changes only apply when a user changes their password. Not recommended to lower the value
2019-02-03 00:22:18 +01:00
password_iterations : i32 , true , def , 100_000 ;
2019-02-04 01:37:25 +01:00
/// settings |> Show password hints |> Controls if the password hint should be shown directly in the web page. Otherwise, if email is disabled, there is no way to see the password hint
2019-02-03 00:22:18 +01:00
show_password_hint : bool , true , def , true ;
2019-02-02 01:09:21 +01:00
2019-02-04 01:37:25 +01:00
/// settings |> Domain URL |> This needs to be set to the URL used to access the server, including 'http[s]://' and port, if it's different than the default. Some server functions don't work correctly without this value
2019-02-03 00:22:18 +01:00
domain : String , true , def , " http://localhost " . to_string ( ) ;
2019-02-04 01:37:25 +01:00
/// private |> Domain set
2019-02-03 00:22:18 +01:00
domain_set : bool , false , def , false ;
2019-02-02 01:09:21 +01:00
2019-02-04 01:37:25 +01:00
/// settings |> Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request. ONLY use this during development, as it can slow down the server
2019-02-03 00:22:18 +01:00
reload_templates : bool , true , def , false ;
2019-02-02 01:09:21 +01:00
2019-02-04 01:37:25 +01:00
/// log |> Enable extended logging
2019-02-03 00:22:18 +01:00
extended_logging : bool , false , def , true ;
2019-02-04 01:37:25 +01:00
/// log |> Log file path
2019-02-03 00:22:18 +01:00
log_file : String , false , option ;
2019-02-02 01:09:21 +01:00
2019-02-04 01:37:25 +01:00
/// settings |> Admin page token |> The token used to authenticate in this very same page. Changing it here won't deauthorize the current session
2019-02-03 00:22:18 +01:00
admin_token : String , true , option ;
2019-02-02 01:09:21 +01:00
2019-02-04 01:37:25 +01:00
/// yubico |> Yubico Client ID
2019-02-03 00:22:18 +01:00
yubico_client_id : String , true , option ;
2019-02-04 01:37:25 +01:00
/// yubico |> Yubico secret Key
2019-02-03 00:22:18 +01:00
yubico_secret_key : String , true , option ;
2019-02-04 01:37:25 +01:00
/// yubico |> Yubico Server
2019-02-03 00:22:18 +01:00
yubico_server : String , true , option ;
2019-02-02 01:09:21 +01:00
2019-02-04 01:37:25 +01:00
// TODO: Remove SMTP from name once groups work
/// mail |> SMTP Host
2019-02-03 00:22:18 +01:00
smtp_host : String , true , option ;
2019-02-04 01:37:25 +01:00
/// mail |> Enable SMTP SSL
2019-02-03 00:22:18 +01:00
smtp_ssl : bool , true , def , true ;
2019-02-04 01:37:25 +01:00
/// mail |> SMTP Port
2019-02-03 00:22:18 +01:00
smtp_port : u16 , true , auto , | c | if c . smtp_ssl { 587 } else { 25 } ;
2019-02-04 01:37:25 +01:00
/// mail |> SMTP From Address
2019-02-03 00:22:18 +01:00
smtp_from : String , true , def , String ::new ( ) ;
2019-02-04 01:37:25 +01:00
/// mail |> SMTP From Name
2019-02-03 00:22:18 +01:00
smtp_from_name : String , true , def , " Bitwarden_RS " . to_string ( ) ;
2019-02-04 01:37:25 +01:00
/// mail |> SMTP Username
2019-02-03 00:22:18 +01:00
smtp_username : String , true , option ;
2019-02-04 01:37:25 +01:00
/// mail |> SMTP Password
2019-02-03 00:22:18 +01:00
smtp_password : String , true , option ;
2019-02-02 01:09:21 +01:00
}
2019-01-25 18:23:51 +01:00
2019-02-02 16:47:27 +01:00
fn validate_config ( cfg : & ConfigItems ) -> Result < ( ) , Error > {
if cfg . yubico_client_id . is_some ( ) ! = cfg . yubico_secret_key . is_some ( ) {
err! ( " Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` need to be set for Yubikey OTP support " )
}
if cfg . smtp_host . is_some ( ) = = cfg . smtp_from . is_empty ( ) {
err! ( " Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support " )
}
if cfg . smtp_username . is_some ( ) ! = cfg . smtp_password . is_some ( ) {
err! ( " Both `SMTP_USERNAME` and `SMTP_PASSWORD` need to be set to enable email authentication " )
}
Ok ( ( ) )
}
2019-02-02 01:09:21 +01:00
impl Config {
2019-02-02 16:47:27 +01:00
pub fn update_config ( & self , other : ConfigBuilder ) -> Result < ( ) , Error > {
2019-02-03 00:22:18 +01:00
// Remove default values
let builder = other . remove ( & self . inner . read ( ) . unwrap ( ) . _env ) ;
2019-02-02 16:47:27 +01:00
2019-02-03 00:22:18 +01:00
// Serialize now before we consume the builder
let config_str = serde_json ::to_string_pretty ( & builder ) ? ;
2019-02-02 17:45:25 +01:00
2019-02-03 00:22:18 +01:00
// Prepare the combined config
let config = {
let env = & self . inner . read ( ) . unwrap ( ) . _env ;
env . merge ( & builder ) . build ( )
} ;
validate_config ( & config ) ? ;
// Save both the user and the combined config
{
let mut writer = self . inner . write ( ) . unwrap ( ) ;
writer . config = config ;
writer . _usr = builder ;
}
2019-02-02 16:47:27 +01:00
2019-02-02 17:45:25 +01:00
//Save to file
use std ::{ fs ::File , io ::Write } ;
2019-02-04 01:37:25 +01:00
let mut file = File ::create ( & * CONFIG_FILE ) ? ;
2019-02-02 17:45:25 +01:00
file . write_all ( config_str . as_bytes ( ) ) ? ;
2019-02-02 16:47:27 +01:00
Ok ( ( ) )
}
2019-02-03 00:22:18 +01:00
pub fn private_rsa_key ( & self ) -> String {
format! ( " {} .der " , CONFIG . rsa_key_filename ( ) )
}
pub fn private_rsa_key_pem ( & self ) -> String {
format! ( " {} .pem " , CONFIG . rsa_key_filename ( ) )
}
pub fn public_rsa_key ( & self ) -> String {
format! ( " {} .pub.der " , CONFIG . rsa_key_filename ( ) )
}
2019-02-02 01:09:21 +01:00
pub fn mail_enabled ( & self ) -> bool {
self . inner . read ( ) . unwrap ( ) . config . smtp_host . is_some ( )
}
2019-01-25 18:23:51 +01:00
2019-02-02 01:09:21 +01:00
pub fn render_template < T : serde ::ser ::Serialize > (
& self ,
name : & str ,
data : & T ,
) -> Result < String , crate ::error ::Error > {
if CONFIG . reload_templates ( ) {
warn! ( " RELOADING TEMPLATES " ) ;
let hb = load_templates ( CONFIG . templates_folder ( ) . as_ref ( ) ) ;
hb . render ( name , data ) . map_err ( Into ::into )
} else {
let hb = & CONFIG . inner . read ( ) . unwrap ( ) . templates ;
hb . render ( name , data ) . map_err ( Into ::into )
}
}
2019-01-25 18:23:51 +01:00
}
2019-02-03 00:22:18 +01:00
use handlebars ::{
Context , Handlebars , Helper , HelperDef , HelperResult , Output , RenderContext , RenderError , Renderable ,
} ;
2019-01-25 18:23:51 +01:00
fn load_templates ( path : & str ) -> Handlebars {
let mut hb = Handlebars ::new ( ) ;
// Error on missing params
hb . set_strict_mode ( true ) ;
2019-02-03 00:22:18 +01:00
hb . register_helper ( " case " , Box ::new ( CaseHelper ) ) ;
2019-01-25 18:23:51 +01:00
macro_rules ! reg {
( $name :expr ) = > { {
let template = include_str! ( concat! ( " static/templates/ " , $name , " .hbs " ) ) ;
hb . register_template_string ( $name , template ) . unwrap ( ) ;
} } ;
}
// First register default templates here
reg! ( " email/invite_accepted " ) ;
reg! ( " email/invite_confirmed " ) ;
reg! ( " email/pw_hint_none " ) ;
reg! ( " email/pw_hint_some " ) ;
reg! ( " email/send_org_invite " ) ;
reg! ( " admin/base " ) ;
reg! ( " admin/login " ) ;
reg! ( " admin/page " ) ;
// And then load user templates to overwrite the defaults
// Use .hbs extension for the files
// Templates get registered with their relative name
hb . register_templates_directory ( " .hbs " , path ) . unwrap ( ) ;
hb
}
2019-02-03 00:22:18 +01:00
#[ derive(Clone, Copy) ]
pub struct CaseHelper ;
impl HelperDef for CaseHelper {
fn call < ' reg : ' rc , ' rc > (
& self ,
h : & Helper < ' reg , ' rc > ,
r : & ' reg Handlebars ,
ctx : & Context ,
rc : & mut RenderContext < ' reg > ,
out : & mut Output ,
) -> HelperResult {
let param = h
. param ( 0 )
. ok_or_else ( | | RenderError ::new ( " Param not found for helper \" case \" " ) ) ? ;
let value = param . value ( ) . clone ( ) ;
if h . params ( ) . iter ( ) . skip ( 1 ) . any ( | x | x . value ( ) = = & value ) {
h . template ( ) . map ( | t | t . render ( r , ctx , rc , out ) ) . unwrap_or ( Ok ( ( ) ) )
} else {
Ok ( ( ) )
}
}
}