df_st_core/config.rs
1//! Everything related to reading, writing and parsing of the `df_storyteller-config.json` file.
2//!
3//! Default configuration (default paths might be different depending on the OS)
4//! ```json
5//! {
6//!   "local_address": "127.0.0.1",
7//!   "server": {
8//!     "address": "127.0.0.1",
9//!     "port": 20350
10//!   },
11//!   "database":{
12//!     "service": "sqlite",
13//!     "config": {
14//!       "db_path": "df_st_database.db",
15//!       "user": "df_storyteller",
16//!       "password": "",
17//!       "host": "localhost",
18//!       "port": 5432,
19//!       "database": "df_storyteller"
20//!     }
21//!   }
22//! }
23//! ```
24//!
25//!
26//! The configuration file has a lot of options that can be used.
27//! Here is an example configuration example with all the possible fields:
28//! ```json
29//! {
30//!   "local_address": "127.0.0.1",
31//!   "server": {
32//!     "address": "127.0.0.1",
33//!     "port": 20350.
34//!     "workers": 8,
35//!     "keep_alive": 20,
36//!     "log_level": "Normal",
37//!     "secret_key": "FYyW1t+y.y+nBppoFx..$..VVVs5XrQD/yC.yHZFqZw=",
38//!     "tls": {
39//!       "certs": "/path/to/certs.pem",
40//!       "private_key": "/path/to/key.pem"
41//!     },
42//!     "limits": {
43//!       "forms": 5242880,
44//!       "json": 5242880
45//!     }
46//!   },
47//!   "database":{
48//!     "service": "postgres",
49//!     "uri": "postgres://df_storyteller:password123@localhost:5432/df_storyteller",
50//!     "config": {
51//!       "db_path": "df_st_database.db",
52//!       "user": "df_storyteller",
53//!       "password": "",
54//!       "host": "localhost",
55//!       "port": 5432,
56//!       "database": "df_storyteller",
57//!       "ssl_mode": "require",
58//!       "ssl_cert": "~/.postgresql/server.crt",
59//!       "ssl_key": "~/.postgresql/server.key"
60//!     },
61//!     "pool_size": 10
62//!   }
63//! }
64//! ```
65//! Descriptions for all the fields can be found in the structures.
66//! The top level of the config starts in [RootConfig](RootConfig)
67
68use anyhow::Error;
69#[allow(unused_imports)]
70use log::{debug, error, info, trace, warn};
71use serde::de::DeserializeOwned;
72use serde::{Deserialize, Serialize};
73use std::collections::HashMap;
74use std::fs::File;
75use std::io::BufReader;
76use std::path::PathBuf;
77
78static CONFIG_FILENAME: &str = "df_storyteller-config.json";
79static SQLITE_DB_FILENAME: &str = "df_st_database.db";
80
81#[derive(Clone, Debug, PartialEq)]
82pub enum DBService {
83    Postgres,
84    SQLite,
85    Unknown,
86}
87
88/// The top level of the config file of `df_storyteller-config.json`
89#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
90pub struct RootConfig {
91    /// IP on your local network (NAT). Usually something like "192.168.0.2".
92    /// Other NAT subnets are: "10.0.0.2" or "172.16.0.2"
93    /// You can find this address by searching a guide using the term "private ip".
94    /// This is necessary if you want to access the API from other devices.
95    /// Default is "127.0.0.1".
96    /// A domain name is also allowed: "example.com" or "localhost"
97    pub local_address: String,
98    /// Configuration of the server.
99    pub server: ServerConfig,
100    /// Configuration of the database.
101    pub database: DatabaseConfig,
102}
103
104impl Default for RootConfig {
105    fn default() -> Self {
106        Self {
107            local_address: "127.0.0.1".to_owned(),
108            server: ServerConfig::default(),
109            database: DatabaseConfig::default(),
110        }
111    }
112}
113
114/// Everything in the configuration under
115/// `df_storyteller-config.json`.`server`.
116#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
117pub struct ServerConfig {
118    /// Default is set to "127.0.0.1".
119    /// To allow other devices to access the API use "0.0.0.0".
120    /// Note: This comes with some advantages and security implications,
121    /// like allowing traffic from other devices on the network (example phones).
122    /// But if device is not behind NAT or port forwarding is set up,
123    /// This can expose the interface to the internet and should be avoided!
124    /// Using "localhost" or "127.0.0.1" can prevent these security implications.
125    /// **DO NOT USE "0.0.0.0" WHEN YOU ARE ON PUBLIC WIFI!**
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub address: Option<String>,
128    /// Default is set to 20350
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub port: Option<u16>,
131    /// Default is set to `None`,
132    /// if None, uses Rocket default = [number_of_cpus * 2]
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub workers: Option<u16>,
135    /// Default is set to `None`
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub keep_alive: Option<u32>,
138    /// This only effects logging from Rocket (server), not DF_Storyteller
139    /// Allowed values: "Critical", "Normal", "Debug" and "Off"
140    /// Default is set to `None`
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub log_level: Option<String>,
143    /// Secret key for private cookies.
144    /// Should not be set in almost all cases.
145    /// From Rocket Docs:
146    /// > When manually specifying the secret key,
147    /// > the value should a 256-bit base64 encoded string.
148    /// > Such a string can be generated with the openssl command line tool:
149    /// > `openssl rand -base64 32`
150    ///
151    /// If set this should be exactly 44 chars long.
152    /// Default is `None`.
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub secret_key: Option<String>,
155    /// Set TLS settings, if not set, no TLS is used (just HTTP, no HTTPS)
156    /// Default is `None`.
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub tls: Option<TLSConfig>,
159    /// Map from data type (string) to data limit (integer: bytes)
160    /// The maximum size in bytes that should be accepted by a
161    /// Rocket application for that data type. For instance, if the
162    /// limit for "forms" is set to 256, only 256 bytes from an incoming
163    /// form request will be read.
164    /// Default is `None`.
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub limits: Option<LimitsConfig>,
167}
168
169impl Default for ServerConfig {
170    fn default() -> Self {
171        Self {
172            address: Some("127.0.0.1".to_owned()),
173            port: Some(20350),
174            workers: None,
175            keep_alive: None,
176            log_level: None,
177            secret_key: None,
178            tls: None,
179            limits: None,
180        }
181    }
182}
183
184/// Everything in the configuration under
185/// `df_storyteller-config.json`.`server.tls`.
186#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
187pub struct TLSConfig {
188    /// path to certificate chain in PEM format
189    pub certs: String,
190    /// path to private key for `tls.certs` in PEM format.
191    pub private_key: String,
192}
193
194/// Everything in the configuration under
195/// `df_storyteller-config.json`.`server.limits`.
196#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
197pub struct LimitsConfig {
198    /// The maximum amount of data DF Storyteller API will accept for a given data type.
199    /// For more info see: https://rocket.rs/v0.4/guide/configuration/#data-limits
200    #[serde(flatten)]
201    pub values: HashMap<String, u64>,
202}
203
204/// Everything in the configuration under
205/// `df_storyteller-config.json`.`database`.
206#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
207pub struct DatabaseConfig {
208    /// Database service name, only "postgres" and "sqlite" are supported.
209    /// Default is set to "sqlite"
210    /// This setting is currently not in use as of 0.3.0 and value is ignored.
211    /// It might be used again later.
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub service: Option<String>,
214    /// Directly set the URI/URL for a database connection.
215    /// If `uri` is set `config` will be ignored.
216    /// SQLite example: `df_st_database.db` (just the filename or path to file)
217    /// Postgres example:
218    /// `postgres://df_storyteller:password123@localhost:5432/df_storyteller`
219    /// For more info about Postgres connection URI
220    /// [here](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING).
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub uri: Option<String>,
223    /// Config used to create the connection with the database.
224    /// This config is used to create a URI, so some characters
225    /// (in password for example) might return errors.
226    /// If `uri` is set the object will be ignored.
227    pub config: DBURLConfig,
228    /// When connecting to the database it will open multiple connections.
229    /// Set the size of the pool of connections that will be opened.
230    /// This is mostly be utilized when the API is running.
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub pool_size: Option<i64>,
233}
234
235impl Default for DatabaseConfig {
236    fn default() -> Self {
237        Self {
238            service: Some("sqlite".to_owned()),
239            uri: None,
240            config: DBURLConfig::default(),
241            pool_size: None,
242        }
243    }
244}
245
246/// Everything in the configuration under
247/// `df_storyteller-config.json`.`database.config`.
248#[derive(Serialize, Deserialize, Clone, PartialEq)]
249pub struct DBURLConfig {
250    /// Path for the SQLite database file.
251    /// Only used for SQLite service.
252    /// Prefer to use the `.db` or `.sqlite`,
253    /// but other extension will work too.
254    /// This file path is used both for writing to and reading from the database.
255    /// Default is set to:
256    /// Linux: `/home/<username>/.config/dfstoryteller/df_st_database.db`
257    /// Windows: `C:\Users\<username>\AppData\Roaming\DF Storyteller\DF Storyteller\config\df_st_database.db`
258    /// macOS: `/Users/<username>/Library/Application Support/com.DF-Storyteller.DF-Storyteller/df_st_database.db`
259    pub db_path: Option<std::path::PathBuf>,
260    /// User name to connect as.
261    /// Only used for Postgres service.
262    /// Default is set to "df_storyteller"
263    pub user: Option<String>,
264    /// Password to be used if the server demands password authentication.
265    /// Only used for Postgres service.
266    /// No default, this has to be set
267    pub password: String,
268    /// Name of host to connect to.
269    /// Only used for Postgres service.
270    /// Default is set to "localhost"
271    pub host: Option<String>,
272    /// Port number to connect to at the server host,
273    /// or socket file name extension for Unix-domain connections.
274    /// Only used for Postgres service.
275    /// Default is set to `5432`
276    pub port: Option<u16>,
277    /// The database name. Defaults to be the same as the user name.
278    /// Only used for Postgres service.
279    /// Default is set to "df_storyteller"
280    pub database: Option<String>,
281    /// This option determines whether or with what priority a secure SSL TCP/IP
282    /// connection will be negotiated with the server.
283    /// Only used for Postgres service.
284    /// Allowed options: "disable", "allow", "prefer", "require", "verify-ca" or "verify-full"
285    /// Default is set to "prefer"
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub ssl_mode: Option<String>,
288    /// This parameter specifies the file name of the client SSL certificate,
289    /// replacing the default `~/.postgresql/postgresql.crt`.
290    /// Only used for Postgres service.
291    /// This parameter is ignored if an SSL connection is not made.
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub ssl_cert: Option<std::path::PathBuf>,
294    /// This parameter specifies the location for the secret key used for the client certificate.
295    /// It can either specify a file name that will be used instead of the
296    /// default ~/.postgresql/postgresql.key, or it can specify a key obtained from an external
297    /// "engine" (engines are OpenSSL loadable modules).
298    /// Only used for Postgres service.
299    /// This parameter is ignored if an SSL connection is not made.
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub ssl_key: Option<std::path::PathBuf>,
302}
303
304impl Default for DBURLConfig {
305    fn default() -> Self {
306        Self {
307            db_path: Some(get_default_store_sqlite_db_path()),
308            user: Some("df_storyteller".to_owned()),
309            password: "".to_owned(),
310            host: Some("localhost".to_owned()),
311            port: Some(5432),
312            database: Some("df_storyteller".to_owned()),
313            ssl_mode: None,
314            ssl_cert: None,
315            ssl_key: None,
316        }
317    }
318}
319
320impl std::fmt::Debug for DBURLConfig {
321    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322        f.debug_struct("DBURLConfig")
323            .field("db_path", &self.db_path)
324            .field("user", &self.user)
325            .field("password", &"[redacted]".to_owned())
326            .field("host", &self.host)
327            .field("port", &self.port)
328            .field("database", &self.database)
329            .field("ssl_mode", &self.ssl_mode)
330            .field("ssl_cert", &self.ssl_cert)
331            .field("ssl_key", &self.ssl_key)
332            .finish()
333    }
334}
335
336/// Get the config file from disk
337/// Use current folder first, otherwise use global config
338/// If no file, use default config
339/// Current folder is: `./df_storyteller-config.json`
340/// Global folder depends on the OS:
341/// Linux: `~/.config/dfstoryteller/df_storyteller-config.json`
342/// Windows: `C:\Users\<username>\AppData\Roaming\DF Storyteller\DF Storyteller\config\df_storyteller-config.json`
343/// macOS: `/Users/<username>/Library/Application Support/com.DF-Storyteller.DF-Storyteller/df_storyteller-config.json`
344pub fn load_config() -> RootConfig {
345    // Check current folder
346    let work_dir_config_path = PathBuf::from(format!("./{}", CONFIG_FILENAME));
347    if work_dir_config_path.is_file() {
348        // file exist, so use it
349        trace!(
350            "Using current working dir config file: `{}`",
351            work_dir_config_path
352                .to_str()
353                .unwrap_or("<invalid filename>")
354        );
355        return get_config(&work_dir_config_path);
356    }
357    // Check global config
358    if let Some(mut project_config_path) = get_project_dir() {
359        project_config_path.push(CONFIG_FILENAME);
360        if project_config_path.is_file() {
361            trace!(
362                "Using global config file: `{}`",
363                project_config_path.to_str().unwrap_or("<invalid filename>")
364            );
365            return get_config(&project_config_path);
366        }
367    }
368    // Use default config
369    info!("Continuing with default config");
370    RootConfig::default()
371}
372
373fn get_project_dir() -> Option<PathBuf> {
374    if let Some(project_dirs) =
375        directories::ProjectDirs::from("com", "DF Storyteller", "DF Storyteller")
376    {
377        Some(project_dirs.config_dir().to_path_buf())
378    } else {
379        None
380    }
381}
382
383/// Get a config file from disk and return the resulting configuration.
384/// If something goes wrong it will return the default configuration
385/// and output an error or warning.
386pub fn get_config(filename: &PathBuf) -> RootConfig {
387    match read_json_file(filename) {
388        Result::Ok(data) => data,
389        Result::Err(err) => {
390            if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
391                if io_err.kind() == std::io::ErrorKind::NotFound {
392                    warn!(
393                        "Config file not found: \"{}\"",
394                        filename.to_str().unwrap_or("<invalid filename>")
395                    );
396                } else {
397                    error!("Config file: {:?}", err);
398                }
399            } else {
400                error!("Config file: {:?}", err);
401            }
402
403            info!("Continuing with default config");
404            RootConfig::default()
405        }
406    }
407}
408
409/// Get default path to store SQLite DB file(s)
410/// Use project path, or default path, in that order.
411/// Default folder depends on the OS:
412/// Linux: `/home/<username>/.config/dfstoryteller/df_st_database.db`
413/// Windows: `C:\Users\<username>\AppData\Roaming\DF Storyteller\DF Storyteller\config\df_st_database.db`
414/// macOS: `/Users/<username>/Library/Application Support/com.DF-Storyteller.DF-Storyteller/df_st_database.db`
415pub fn get_default_store_sqlite_db_path() -> PathBuf {
416    let default_file = PathBuf::from("./");
417    let mut project_file = get_project_dir().unwrap_or(default_file);
418    project_file.push(SQLITE_DB_FILENAME);
419    project_file
420}
421
422/// Get default path to store config
423/// Use project path, or default path, in that order.
424/// Default folder depends on the OS:
425/// Linux: `~/.config/dfstoryteller/df_storyteller-config.json`
426/// Windows: `C:\Users\<username>\AppData\Roaming\DF Storyteller\DF Storyteller\config\df_storyteller-config.json`
427/// macOS: `/Users/<username>/Library/Application Support/com.DF-Storyteller.DF-Storyteller/df_storyteller-config.json`
428fn get_default_store_config_path() -> PathBuf {
429    let default_file = PathBuf::from("./");
430    let mut project_file = get_project_dir().unwrap_or(default_file);
431    project_file.push(CONFIG_FILENAME);
432    project_file
433}
434
435pub fn get_current_use_config_path() -> Option<PathBuf> {
436    // Check if current working dir file
437    let work_dir_config_path = PathBuf::from(format!("./{}", CONFIG_FILENAME));
438    if work_dir_config_path.is_file() {
439        return Some(work_dir_config_path);
440    }
441    // Check global config
442    if let Some(mut project_config_path) = get_project_dir() {
443        project_config_path.push(CONFIG_FILENAME);
444        if project_config_path.is_file() {
445            return Some(project_config_path);
446        }
447    }
448    None
449}
450
451/// Store a given configuration into a file, in json format.
452pub fn store_config_to_file(config: RootConfig, filename: Option<&PathBuf>) {
453    let default_path = get_default_store_config_path();
454    // Use given parameter path, or project path, or default path, in that order
455    let filename = filename.unwrap_or(&default_path);
456    info!(
457        "Storing/replacing config in file: `{}`",
458        filename.to_str().unwrap_or("<invalid filename>")
459    );
460    match std::fs::create_dir_all(filename.parent().unwrap()) {
461        Ok(_) => {}
462        Err(err) => {
463            error!(
464                "Folders to this path could not be created: `{}` because of error: {}",
465                filename.to_str().unwrap_or("<invalid filename>"),
466                err.to_string(),
467            );
468            panic!("Folders to this path could not be created"); // TODO add to git issue
469        }
470    };
471    // Check if file already exists
472    if filename.is_file() {
473        // Create backup
474        create_backup(filename);
475    }
476    let file = match File::create(filename) {
477        Ok(value) => value,
478        Err(err) => {
479            error!(
480                "Config file could not be created: `{}` because of error: {}",
481                filename.to_str().unwrap_or("<invalid filename>"),
482                err.to_string(),
483            );
484            panic!("Config file could not be created"); // TODO add to git issue
485        }
486    };
487    match serde_json::to_writer_pretty(file, &config) {
488        Ok(_) => {}
489        Err(err) => {
490            error!(
491                "Could not write config to file: `{:#?}` because of error: {}",
492                config,
493                err.to_string()
494            );
495            panic!("Could not write config to file"); // TODO add to git issue
496        }
497    };
498}
499
500fn create_backup(file: &PathBuf) {
501    let mut back_up_path = file.clone();
502    back_up_path.set_extension("old.json");
503    info!(
504        "Backing up old config to: `{}`",
505        back_up_path.to_str().unwrap_or("<invalid filename>")
506    );
507    // TODO add to git issue
508    std::fs::copy(&file, &back_up_path).expect("Could not backup config file.");
509}
510
511/// Read a JSON file and Deserialize it into the expected Object.
512fn read_json_file<C: DeserializeOwned>(filename: &PathBuf) -> Result<C, Error> {
513    let file = File::open(filename)?;
514    let reader = BufReader::new(file);
515    let parsed_result = &mut serde_json::de::Deserializer::from_reader(reader);
516    let result: Result<C, _> = serde_path_to_error::deserialize(parsed_result);
517    let parsed_object: C = match result {
518        Ok(data) => data,
519        Err(err) => {
520            let path = err.path().to_string();
521            error!("Error: {} \nIn: {}", err, path);
522            return Err(Error::from(err));
523        }
524    };
525    Ok(parsed_object)
526}
527
528/// More info about the connection URL:
529/// https://www.postgresql.org/docs/9.4/libpq-connect.html#LIBPQ-CONNSTRING
530pub fn get_database_url(config: &RootConfig, db_service: &DBService) -> Option<String> {
531    if let Some(uri) = &config.database.uri {
532        Some(uri.to_string())
533    } else {
534        let db_config = config.database.config.clone();
535        // Construct url from `DBURLConfig`, use default if not set
536        match db_service {
537            DBService::Postgres => {
538                let user = db_config
539                    .user
540                    .unwrap_or_else(|| "df_storyteller".to_string());
541                let password = db_config.password;
542                let host = db_config.host.unwrap_or_else(|| "localhost".to_string());
543                let port = db_config.port.unwrap_or(5432);
544                let database = db_config
545                    .database
546                    .unwrap_or_else(|| "df_storyteller".to_string());
547                let ssl_mode = db_config.ssl_mode.unwrap_or_else(|| "prefer".to_string());
548                let service = "postgres";
549
550                // Allow user to use ssl certificate to connect.
551                if let Some(ssl_cert) = db_config.ssl_cert {
552                    let ssl_cert = ssl_cert.to_str().unwrap();
553                    if let Some(ssl_key) = db_config.ssl_key {
554                        let ssl_key = ssl_key.to_str().unwrap();
555                        return Some(format!("{}://{}:{}@{}:{}/{}?sslmode={}&sslcert={}&sslkey={}&application_name=DF_Storyteller",
556                            service, user, password, host, port, database, ssl_mode, ssl_cert, ssl_key));
557                    }
558                    return Some(format!(
559                        "{}://{}:{}@{}:{}/{}?sslmode={}&sslcert={}&application_name=DF_Storyteller",
560                        service, user, password, host, port, database, ssl_mode, ssl_cert
561                    ));
562                }
563
564                Some(format!(
565                    "{}://{}:{}@{}:{}/{}?sslmode={}&application_name=DF_Storyteller",
566                    service, user, password, host, port, database, ssl_mode
567                ))
568            }
569            DBService::SQLite => {
570                let path = match &db_config.db_path {
571                    Some(db_path) => db_path.to_str().unwrap(),
572                    None => SQLITE_DB_FILENAME,
573                };
574                Some(path.to_owned())
575            }
576            DBService::Unknown => {
577                error!(
578                    "Database service is not supported.\n\
579                    Select \"sqlite\" or \"postgres\"."
580                );
581                unreachable!("No DB service selected");
582            }
583        }
584    }
585}
586
587/// This function returns a URL to the `postgres` database.
588/// This is used to change general settings and create database.
589pub fn get_db_system_url(config: &RootConfig, db_service: &DBService) -> Option<String> {
590    if let Some(uri) = &config.database.uri {
591        Some(uri.to_string())
592    } else {
593        let dbconfig = config.database.config.clone();
594        // Construct uri from `DBURLConfig`, use default if not set
595        if db_service != &DBService::Postgres {
596            unreachable!("Using Postgres function without Postgres feature enabled.")
597        }
598        let service = "postgres";
599        let user = dbconfig.user.unwrap_or_else(|| "postgres".to_string());
600        let password = dbconfig.password;
601        let host = dbconfig.host.unwrap_or_else(|| "localhost".to_string());
602        let port = dbconfig.port.unwrap_or(5432);
603
604        Some(format!(
605            "{}://{}:{}@{}:{}/postgres",
606            service, user, password, host, port
607        ))
608    }
609}