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
use oauth2::{ClientId, ClientSecret};
use std::sync::Arc;
use std::{collections::HashMap, env, path::PathBuf};
use std::{fs::File, io::Read, process::exit};
use structopt::StructOpt;

/// Credentials granted by GitHub for the OAuth application.
/// Generated these by creating an application at
/// <https://github.com/settings/applications/new/>.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GithubOauthConfig {
    /// The GitHub OAuth application client id.
    pub client_id: ClientId,
    /// The GitHub OAuth application client secret.
    pub client_secret: ClientSecret,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DiscordConfig {
    /// The Discord application client id.
    pub client_id: ClientId,

    /// The Discord OAuth2 application client secret.
    pub client_secret: ClientSecret,

    /// The bot token granted by discord used to authenticate with the discord
    /// bot API.
    pub bot_token: String,

    /// The RCOS Discord Guild ID.
    pub rcos_guild_id: String,
}

impl DiscordConfig {
    /// Get the RCOS Discord Guild ID as a `u64`.
    pub fn rcos_guild_id(&self) -> u64 {
        self.rcos_guild_id
            .as_str()
            .parse::<u64>()
            .expect("Malformed RCOS Guild ID")
    }
}

/// The config of the server instance.
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
struct TelescopeConfig {
    /// Set the log level.
    /// See <https://docs.rs/env_logger/0.8.1/env_logger/> for reference.
    log_level: Option<String>,

    /// GitHub OAuth application credentials.
    github_credentials: Option<GithubOauthConfig>,

    /// Discord application config and credentials.
    discord_config: Option<DiscordConfig>,

    /// The URL of the RCOS central API (in the OpenAPI Spec via RCOS-data).
    api_url: Option<String>,

    /// The JWT secret used to authenticate with the central API.
    jwt_secret: Option<String>,

    /// Profiles. These can be used and specified at runtime to override values
    /// defined globally. Profiles are scoped and can have sub profiles.
    profile: Option<HashMap<String, TelescopeConfig>>,

    /// The URL that Telescope is running at. This is used in Discord embeds
    /// and the Open Graph Protocol meta tags. Should not end with a slash.
    telescope_url: Option<String>,
}

/// A concrete config found by searching the specified profile and parents
/// for items from the narrowest up.
///
/// The fields of this struct should match up closely to the fields of the
/// TelescopeConfig struct.
#[derive(Serialize, Debug)]
pub struct ConcreteConfig {
    /// The log level. Private because the logger is initialized in this module.
    log_level: String,
    /// The GitHub OAuth Application Credentials.
    pub github_credentials: GithubOauthConfig,
    /// The Discord Config and Credentials.
    pub discord_config: DiscordConfig,
    /// The url of the RCOS API that telescope will read and write to.
    pub api_url: String,
    /// The domain that telescope is available at. Should not end with a slash.
    pub telescope_url: String,
    /// The JWT secret used to authenticate with the central API.
    pub jwt_secret: String,
}

impl TelescopeConfig {
    /// Make the profile concrete by reverse searching profiles.
    fn make_concrete(&self, profile: Vec<String>) -> ConcreteConfig {
        // check profile exists.
        let mut scope = self;
        for part in &profile {
            if scope
                .profile
                .as_ref()
                .map(|map| map.contains_key(part))
                .unwrap_or(false)
            {
                scope = scope.profile.as_ref().unwrap().get(part).unwrap();
            } else {
                eprintln!(
                    "Profile path {:?} not found in config. missing part {}.",
                    profile, part
                );
                exit(1)
            }
        }

        let profile_slice = &profile[..];
        ConcreteConfig {
            log_level: self
                .reverse_lookup(profile_slice, |c| c.log_level.clone())
                .expect("Could not resolve log level."),
            github_credentials: self
                .reverse_lookup(profile_slice, |c| c.github_credentials.clone())
                .expect("Could not resolve GitHub OAuth credentials."),
            discord_config: self
                .reverse_lookup(profile_slice, |c| c.discord_config.clone())
                .expect("Could not resolve Discord credentials"),
            api_url: self
                .reverse_lookup(profile_slice, |c| c.api_url.clone())
                .expect("Could not resolve RCOS central API URL."),
            jwt_secret: self
                .reverse_lookup(profile_slice, |c| c.jwt_secret.clone())
                .expect("Could not resolve JWT secret."),
            telescope_url: self
                .reverse_lookup(profile_slice, |c| c.telescope_url.clone())
                .expect("Could not resolve Telescope URl."),
        }
    }

    /// Reverse lookup a property using an extractor.
    ///
    /// Assume profile is valid and exists.
    fn reverse_lookup<T: Clone>(
        &self,
        profile_slice: &[String],
        extractor: impl Fn(&Self) -> Option<T> + Copy,
    ) -> Option<T> {
        if profile_slice.len() >= 2 {
            let child_path = &profile_slice[1..];
            let child = self
                .profile
                .as_ref()
                .unwrap()
                .get(&profile_slice[0])
                .unwrap();
            // Recursively call the reverse lookup into the child profile.
            // This will resolve the deepest profile first, down to the
            // shallowest one.
            child
                .reverse_lookup(child_path, extractor)
                .or(extractor(self))
        } else if profile_slice.len() == 1 {
            extractor(
                self.profile
                    .as_ref()
                    .unwrap()
                    .get(&profile_slice[0])
                    .unwrap(),
            )
            .or(extractor(self))
        } else {
            extractor(self)
        }
    }
}

// The name, about, version, and authors are given by cargo.
/// Stores the configuration of the telescope server. An instance of this is created and stored in
/// a lazy static before the server is launched.
#[derive(Debug, Serialize, StructOpt)]
#[structopt(about = "The RCOS webapp", rename_all = "screaming-snake")]
struct CommandLine {
    /// The config file for this Telescope instance. See config_example.toml
    /// for more details.
    #[structopt(short = "c", long = "config", env, default_value = "config.toml")]
    config_file: PathBuf,
    /// What profile (if any) to use from the config file.
    ///
    /// Subprofiles can be specified using a '.' delimiter, e.g.
    /// 'dev.local'
    #[structopt(short = "p", long = "profile", env)]
    profile: Option<String>,
}

lazy_static! {
    /// Global web server configuration.
    pub static ref CONFIG: Arc<ConcreteConfig> = Arc::new(cli());
}

/// After the global configuration is initialized, log it as info.
pub fn init() {
    let cfg: &ConcreteConfig = &*CONFIG;

    // initialize logger.
    env_logger::builder().parse_filters(&cfg.log_level).init();

    info!("Starting up...");
    info!("telescope {}", env!("CARGO_PKG_VERSION"));
    trace!("Config: \n{}", serde_json::to_string_pretty(cfg).unwrap());
}

/// Get the global configuration.
pub fn global_config() -> Arc<ConcreteConfig> {
    CONFIG.clone()
}

/// Digest and handle arguments from the command line. Read arguments from environment
/// variables where necessary. Construct and return the configuration specified.
/// Initializes logging and returns config.
fn cli() -> ConcreteConfig {
    // Set env vars from a ".env" file if available.
    dotenv::dotenv().ok();

    // Get the command line args.
    let commandline: CommandLine = CommandLine::from_args();

    // Read the config file into a string.
    let mut confing_file_string = String::new();
    File::open(&commandline.config_file)
        .map_err(|e| {
            eprintln!(
                "Could not open config file at {}: {}",
                commandline.config_file.display(),
                e
            );
            e
        })
        .unwrap()
        .read_to_string(&mut confing_file_string)
        .map_err(|e| {
            eprintln!(
                "Could not read config file at {}: {}",
                commandline.config_file.display(),
                e
            );
            e
        })
        .unwrap();

    // Parse the config file into an object.
    let parsed = toml::from_str::<TelescopeConfig>(confing_file_string.as_str())
        .map_err(|e| {
            eprintln!("Error deserializing config file: {}", e);
            e
        })
        .unwrap();

    // Extract the profile from the command line args or default to empty.
    let profile_path: Vec<String> = commandline
        .profile
        .map(|s| s.split(".").map(|p| p.to_string()).collect())
        .unwrap_or(Vec::new());

    return parsed.make_concrete(profile_path);
}