1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
//! Everything related to reading, writing and parsing of the `df_storyteller-config.json` file. //! //! Default configuration //! ```json //! { //! "local_address": "127.0.0.1", //! "server": { //! "address": "127.0.0.1", //! "port": 20350 //! }, //! "database":{ //! "service": "sqlite", //! "config": { //! "db_path": "df_st_database.db", //! "user": "df_storyteller", //! "password": "", //! "host": "localhost", //! "port": 5432, //! "database": "df_storyteller" //! } //! } //! } //! ``` //! //! //! The configuration file has a lot of options that can be used. //! Here is an example configuration example with all the possible fields: //! ```json //! { //! "local_address": "127.0.0.1", //! "server": { //! "address": "127.0.0.1", //! "port": 20350. //! "workers": 8, //! "keep_alive": 20, //! "log_level": "Normal", //! "secret_key": "FYyW1t+y.y+nBppoFx..$..VVVs5XrQD/yC.yHZFqZw=", //! "tls": { //! "certs": "/path/to/certs.pem", //! "private_key": "/path/to/key.pem" //! }, //! "limits": { //! "forms": 5242880, //! "json": 5242880 //! } //! }, //! "database":{ //! "service": "postgres", //! "uri": "postgres://df_storyteller:password123@localhost:5432/df_storyteller", //! "config": { //! "db_path": "df_st_database.db", //! "user": "df_storyteller", //! "password": "", //! "host": "localhost", //! "port": 5432, //! "database": "df_storyteller", //! "ssl_mode": "require", //! "ssl_cert": "~/.postgresql/server.crt", //! "ssl_key": "~/.postgresql/server.key" //! }, //! "pool_size": 10 //! } //! } //! ``` //! Descriptions for all the fields can be found in the structures. //! The top level of the config starts in [RootConfig](RootConfig) use failure::Error; #[allow(unused_imports)] use log::{debug, error, info, trace, warn}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs::File; use std::io::BufReader; use std::path::PathBuf; /// The top level of the config file of `df_storyteller-config.json` #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct RootConfig { /// IP on your local network (NAT). Usually something like "192.168.0.2". /// Other NAT subnets are: "10.0.0.2" or "172.16.0.2" /// You can find this address by searching a guide using the term "private ip". /// This is necessary if you want to access the API from other devices. /// Default is "127.0.0.1". /// A domain name is also allowed: "example.com" or "localhost" pub local_address: String, /// Configuration of the server. pub server: ServerConfig, /// Configuration of the database. pub database: DatabaseConfig, } impl Default for RootConfig { fn default() -> Self { Self { local_address: "127.0.0.1".to_owned(), server: ServerConfig::default(), database: DatabaseConfig::default(), } } } /// Everything in the configuration under /// `df_storyteller-config.json`.`server`. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct ServerConfig { /// Default is set to "127.0.0.1". /// To allow other devices to access the API use "0.0.0.0". /// Note: This comes with some advantages and security implications, /// like allowing traffic from other devices on the network (example phones). /// But if device is not behind NAT or port forwarding is set up, /// This can expose the interface to the internet and should be avoided! /// Using "localhost" or "127.0.0.1" can prevent these security implications. /// **DO NOT USE "0.0.0.0" WHEN YOU ARE ON PUBLIC WIFI!** #[serde(skip_serializing_if = "Option::is_none")] pub address: Option<String>, /// Default is set to 20350 #[serde(skip_serializing_if = "Option::is_none")] pub port: Option<u16>, /// Default is set to `None`, /// if None, uses Rocket default = [number_of_cpus * 2] #[serde(skip_serializing_if = "Option::is_none")] pub workers: Option<u16>, /// Default is set to `None` #[serde(skip_serializing_if = "Option::is_none")] pub keep_alive: Option<u32>, /// This only effects logging from Rocket (server), not DF_Storyteller /// Allowed values: "Critical", "Normal", "Debug" and "Off" /// Default is set to `None` #[serde(skip_serializing_if = "Option::is_none")] pub log_level: Option<String>, /// Secret key for private cookies. /// Should not be set in almost all cases. /// From Rocket Docs: /// > When manually specifying the secret key, /// > the value should a 256-bit base64 encoded string. /// > Such a string can be generated with the openssl command line tool: /// > `openssl rand -base64 32` /// /// If set this should be exactly 44 chars long. /// Default is `None`. #[serde(skip_serializing_if = "Option::is_none")] pub secret_key: Option<String>, /// Set TLS settings, if not set, no TLS is used (just HTTP, no HTTPS) /// Default is `None`. #[serde(skip_serializing_if = "Option::is_none")] pub tls: Option<TLSConfig>, /// Map from data type (string) to data limit (integer: bytes) /// The maximum size in bytes that should be accepted by a /// Rocket application for that data type. For instance, if the /// limit for "forms" is set to 256, only 256 bytes from an incoming /// form request will be read. /// Default is `None`. #[serde(skip_serializing_if = "Option::is_none")] pub limits: Option<LimitsConfig>, } impl Default for ServerConfig { fn default() -> Self { Self { address: Some("127.0.0.1".to_owned()), port: Some(20350), workers: None, keep_alive: None, log_level: None, secret_key: None, tls: None, limits: None, } } } /// Everything in the configuration under /// `df_storyteller-config.json`.`server.tls`. #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] pub struct TLSConfig { /// path to certificate chain in PEM format pub certs: String, /// path to private key for `tls.certs` in PEM format. pub private_key: String, } /// Everything in the configuration under /// `df_storyteller-config.json`.`server.limits`. #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] pub struct LimitsConfig { /// The maximum amount of data DF Storyteller API will accept for a given data type. /// For more info see: https://rocket.rs/v0.4/guide/configuration/#data-limits #[serde(flatten)] pub values: HashMap<String, u64>, } /// Everything in the configuration under /// `df_storyteller-config.json`.`database`. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct DatabaseConfig { /// Database service name, only "postgres" and "sqlite" are supported. /// Default is set to "sqlite" #[serde(skip_serializing_if = "Option::is_none")] pub service: Option<String>, /// Directly set the URI/URL for a database connection. /// If `uri` is set `config` will be ignored. /// SQLite example: `df_st_database.db` (just the filename or path to file) /// Postgres example: /// `postgres://df_storyteller:password123@localhost:5432/df_storyteller` /// For more info about Postgres connection URI /// [here](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING). #[serde(skip_serializing_if = "Option::is_none")] pub uri: Option<String>, /// Config used to create the connection with the database. /// This config is used to create a URI, so some characters /// (in password for example) might return errors. /// If `uri` is set the object will be ignored. pub config: DBURLConfig, /// When connecting to the database it will open multiple connections. /// Set the size of the pool of connections that will be opened. /// This is mostly be utilized when the API is running. #[serde(skip_serializing_if = "Option::is_none")] pub pool_size: Option<i64>, } impl Default for DatabaseConfig { fn default() -> Self { Self { service: Some("sqlite".to_owned()), uri: None, config: DBURLConfig::default(), pool_size: None, } } } /// Everything in the configuration under /// `df_storyteller-config.json`.`database.config`. #[derive(Serialize, Deserialize, Clone, PartialEq)] pub struct DBURLConfig { /// Path for the SQLite database file. /// Only used for SQLite service. /// Prefer to use the `.db` or `.sqlite`, /// but other extension will work. /// This file path is used both for writing to and reading from the database. /// Default is set to "df_st_database.db" pub db_path: Option<std::path::PathBuf>, /// User name to connect as. /// Only used for Postgres service. /// Default is set to "df_storyteller" pub user: Option<String>, /// Password to be used if the server demands password authentication. /// Only used for Postgres service. /// No default, this has to be set pub password: String, /// Name of host to connect to. /// Only used for Postgres service. /// Default is set to "localhost" pub host: Option<String>, /// Port number to connect to at the server host, /// or socket file name extension for Unix-domain connections. /// Only used for Postgres service. /// Default is set to `5432` pub port: Option<u16>, /// The database name. Defaults to be the same as the user name. /// Only used for Postgres service. /// Default is set to "df_storyteller" pub database: Option<String>, /// This option determines whether or with what priority a secure SSL TCP/IP /// connection will be negotiated with the server. /// Only used for Postgres service. /// Allowed options: "disable", "allow", "prefer", "require", "verify-ca" or "verify-full" /// Default is set to "prefer" #[serde(skip_serializing_if = "Option::is_none")] pub ssl_mode: Option<String>, /// This parameter specifies the file name of the client SSL certificate, /// replacing the default `~/.postgresql/postgresql.crt`. /// Only used for Postgres service. /// This parameter is ignored if an SSL connection is not made. #[serde(skip_serializing_if = "Option::is_none")] pub ssl_cert: Option<std::path::PathBuf>, /// This parameter specifies the location for the secret key used for the client certificate. /// It can either specify a file name that will be used instead of the /// default ~/.postgresql/postgresql.key, or it can specify a key obtained from an external /// "engine" (engines are OpenSSL loadable modules). /// Only used for Postgres service. /// This parameter is ignored if an SSL connection is not made. #[serde(skip_serializing_if = "Option::is_none")] pub ssl_key: Option<std::path::PathBuf>, } impl Default for DBURLConfig { fn default() -> Self { Self { db_path: Some(std::path::PathBuf::from("df_st_database.db")), user: Some("df_storyteller".to_owned()), password: "".to_owned(), host: Some("localhost".to_owned()), port: Some(5432), database: Some("df_storyteller".to_owned()), ssl_mode: None, ssl_cert: None, ssl_key: None, } } } impl std::fmt::Debug for DBURLConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("DBURLConfig") .field("db_path", &self.db_path) .field("user", &self.user) .field("password", &"[redacted]".to_owned()) .field("host", &self.host) .field("port", &self.port) .field("database", &self.database) .field("ssl_mode", &self.ssl_mode) .field("ssl_cert", &self.ssl_cert) .field("ssl_key", &self.ssl_key) .finish() } } /// Get a config file from disk and return the resulting configuration. /// If something goes wrong it will return the default configuration /// and output an error or warning. pub fn get_config(filename: &PathBuf) -> RootConfig { match read_json_file(filename) { Result::Ok(data) => data, Result::Err(err) => { if let Some(io_err) = err.downcast_ref::<std::io::Error>() { if io_err.kind() == std::io::ErrorKind::NotFound { warn!( "Config file not found: \"{}\"", filename.to_str().unwrap_or("<invalid filename>") ); } else { error!("Config file: {:?}", err); } } else { error!("Config file: {:?}", err); } info!("Continuing with default config"); RootConfig::default() } } } /// Store a given configuration into a file, in json format. pub fn store_config_to_file(config: RootConfig, filename: Option<&PathBuf>) { let default_file = PathBuf::from("./df_storyteller-config.json"); let filename = filename.unwrap_or(&default_file); let file = File::create(filename).unwrap(); serde_json::to_writer_pretty(file, &config).unwrap(); } /// Read a JSON file and Deserialize it into the expected Object. fn read_json_file<C: DeserializeOwned>(filename: &PathBuf) -> Result<C, Error> { let file = File::open(filename)?; let reader = BufReader::new(file); let parsed_result = &mut serde_json::de::Deserializer::from_reader(reader); let result: Result<C, _> = serde_path_to_error::deserialize(parsed_result); let parsed_object: C = match result { Ok(data) => data, Err(err) => { let path = err.path().to_string(); error!("Error: {} \nIn: {}", err, path); return Err(Error::from(err)); } }; Ok(parsed_object) } /// More info about the connection URL: /// https://www.postgresql.org/docs/9.4/libpq-connect.html#LIBPQ-CONNSTRING pub fn get_database_url(config: &RootConfig) -> Option<String> { if let Some(uri) = &config.database.uri { Some(uri.to_string()) } else { let dbconfig = config.database.config.clone(); // Construct url from `DBURLConfig`, use default if not set let service = config .database .service .clone() .unwrap_or_else(|| "sqlite".to_string()); if service == "postgres" { let user = dbconfig .user .unwrap_or_else(|| "df_storyteller".to_string()); let password = dbconfig.password; let host = dbconfig.host.unwrap_or_else(|| "localhost".to_string()); let port = dbconfig.port.unwrap_or(5432); let database = dbconfig .database .unwrap_or_else(|| "df_storyteller".to_string()); let ssl_mode = dbconfig.ssl_mode.unwrap_or_else(|| "prefer".to_string()); // Allow user to use ssl certificate to connect. if let Some(ssl_cert) = dbconfig.ssl_cert { let ssl_cert = ssl_cert.to_str().unwrap(); if let Some(ssl_key) = dbconfig.ssl_key { let ssl_key = ssl_key.to_str().unwrap(); return Some(format!("{}://{}:{}@{}:{}/{}?sslmode={}&sslcert={}&sslkey={}&application_name=DF_Storyteller", service, user, password, host, port, database, ssl_mode, ssl_cert, ssl_key)); } return Some(format!( "{}://{}:{}@{}:{}/{}?sslmode={}&sslcert={}&application_name=DF_Storyteller", service, user, password, host, port, database, ssl_mode, ssl_cert )); } Some(format!( "{}://{}:{}@{}:{}/{}?sslmode={}&application_name=DF_Storyteller", service, user, password, host, port, database, ssl_mode )) } else if service == "sqlite" { let path = match &dbconfig.db_path { Some(db_path) => db_path.to_str().unwrap(), None => "df_st_database.db", }; Some(path.to_owned()) } else { error!( "Database service is not supported: {}.\n\ Select \"sqlite\" or \"postgres\".", service ); None } } } /// This function returns a URL to the `postgres` database. /// This is used to change general settings and create database. pub fn get_db_system_url(config: &RootConfig) -> Option<String> { if let Some(uri) = &config.database.uri { Some(uri.to_string()) } else { let dbconfig = config.database.config.clone(); // Construct uri from `DBURLConfig`, use default if not set let service = config .database .service .clone() .unwrap_or_else(|| "postgres".to_string()); if service != "postgres" { warn!("The function `get_db_system_url` can only be used with postgres."); return None; } let user = dbconfig.user.unwrap_or_else(|| "postgres".to_string()); let password = dbconfig.password; let host = dbconfig.host.unwrap_or_else(|| "localhost".to_string()); let port = dbconfig.port.unwrap_or(5432); Some(format!( "{}://{}:{}@{}:{}/postgres", service, user, password, host, port )) } }