/* Copyright 2021 Dominik George <dominik.george@teckids.org> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use crate::BASE_NAME; use lazy_static::lazy_static; use std::collections::HashMap; use std::convert::From; use std::time::SystemTime; use libc::{geteuid, seteuid, getpwnam, uid_t}; use std::ffi::CString; use oauth2::basic::BasicTokenResponse; use std::env; use std::fs; use std::io; use std::path::PathBuf; use xdg::{BaseDirectories,BaseDirectoriesError}; use serde::{Deserialize, Serialize}; use serde_json; const TOKEN_DEFAULT_EXPIRES: u64 = 24 * 60 * 60; const USER_TOKEN_FILENAME: &str = "user_token.json"; #[derive(Serialize, Deserialize)] struct UserToken { access_token: String, expires_at: u64, refresh_token: Option<String>, } impl UserToken { fn is_expired(&self) -> bool { match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { Ok(d) => d.as_secs() >= self.expires_at, Err(_) => true } } } impl From<BasicTokenResponse> for UserToken { fn from(response: BasicTokenResponse) -> Self { UserToken { access_token: response.access_token.secret().to_string(), expires_at: match response.expires_in { Some(duration) => duration, None => TOKEN_DEFAULT_EXPIRES } + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).ok().unwrap().as_secs(), refresh_token: match response.refresh_token { Some(t) => Some(t.secret().to_string()), None => None } } } } struct Cache { user_tokens: HashMap<String, UserToken>, original_euid: uid_t, } impl Cache { pub fn new() -> Cache { Cache { user_tokens: HashMap::new(), original_euid: geteuid() } } fn drop_privileges(&self, username: String) -> Result<uid_t, String> { let nam = match CString::new(username) { Ok(nam) => nam, Err(_) => return Err("Invalid username in lookup".to_string()) }; let target_euid = (*getpwnam(nam.as_ptr())).pw_uid; if target_euid == self.original_euid { debug!("No need to drop privileges, already running as {}", username); return Ok(self.original_euid); } else if self.original_euid == 0 { let res = seteuid(target_euid); if res == 0 { debug!("Successfully dropped privileges to {}", username); return Ok(target_euid); } else { error!("Could not drop privileges to {}", username); return Err("Failed to drop privileges".to_string()); } } error!("Not running as root or target user, cannot drop privileges"); return Err("Dropping privileges not supported".to_string()); } fn restore_privileges(&self) { debug!("Restoring privileges"); seteuid(self.original_euid); } fn get_user_xdg_base_directories(&self, username: String) -> Result<BaseDirectories, BaseDirectoriesError> { let saved_home = env::var_os("HOME"); let nam = match CString::new(username) { Ok(nam) => nam, Err(_) => CString::new("nobody").ok().unwrap() }; let user_home = CString::from_raw((*getpwnam(nam.as_ptr())).pw_dir).to_str().unwrap(); env::set_var("HOME", user_home); debug!("Home directory for {} is {}", username, user_home); let base_dirs = BaseDirectories::with_prefix(BASE_NAME)?; if saved_home != None { env::set_var("HOME", saved_home.unwrap()); } else { env::remove_var("HOME"); } match self.drop_privileges(username) { Ok(_) => match base_dirs.create_cache_directory(BASE_NAME) { Ok(v) => { info!("Created XDG cache directory for user {}", username); Some(v) }, Err(e) => { error!("Error creating XDG cache directory for user {}: {}", username, e); None } } Err(_) => None }; self.restore_privileges(); return Ok(base_dirs); } fn place_user_cache_file(&self, username: String, filename: &str) -> Result<PathBuf, io::Error> { match self.get_user_xdg_base_directories(username) { Ok(b) => b.place_cache_file(filename), Err(e) => Err(io::Error::new(io::ErrorKind::NotFound, e)) } } pub fn load_user_token(&self, owner: String) -> Option<&UserToken> { let token = self.user_tokens.get(&owner); if token.is_none() || token.unwrap().is_expired() { debug!("No recent token for {} in memory, trying to load from file", owner); self.user_tokens.remove(&owner); self.drop_privileges(owner).ok(); let new_token = match self.place_user_cache_file(owner, USER_TOKEN_FILENAME) { Ok(path) => match load_json::<UserToken>(path) { Ok(read_token) => { if !read_token.is_expired() { debug!("Found valid token for {} in file", owner); self.user_tokens.insert(owner, read_token); Some(&read_token) } else { debug!("Token in file for {} is expired.", owner); None } }, Err(e) => None }, Err(e) => None }; self.restore_privileges(); new_token } else { debug!("Found valid token for {} in memory", owner); token } } pub fn save_user_token(&self, owner: String, token: UserToken) -> Result<(), io::Error> { self.user_tokens.insert(owner, token); debug!("Saved token for {} in memory", owner); // Try to write user's token cache file let res = match self.drop_privileges(owner) { Ok(_) => match self.place_user_cache_file(owner, USER_TOKEN_FILENAME) { Ok(path) => { debug!("Storing token for {} in cache file", owner); save_json(path, token) }, Err(e) => Err(e) }, Err(e) => Err(io::Error::new(io::ErrorKind::PermissionDenied, e)) }; self.restore_privileges(); return res; } pub fn delete_user_token(&self, owner: String) { self.user_tokens.remove(&owner); debug!("Token for {} removed from memory", owner); // Try to remove user's token cache file self.drop_privileges(owner).ok(); match self.place_user_cache_file(owner, USER_TOKEN_FILENAME) { Ok(path) => { debug!("Deleting cache file for {}", owner); fs::remove_file(path) }, Err(e) => Err(e) }; self.restore_privileges(); } pub fn cleanup_tokens(&self) { for (owner, token) in self.user_tokens { if token.is_expired() { self.delete_user_token(owner); debug!("Deleted expired token for {}", owner); } } } } fn load_json<'de, O: Deserialize<'de>>(path: PathBuf) -> Result<O, io::Error> { let json = fs::read_to_string(path)?; match serde_json::from_str(&json) { Ok(o) => Ok(o), Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)) } } fn save_json<O: Serialize>(path: PathBuf, obj: O) -> Result<(), io::Error> { let json = match serde_json::to_string(&obj) { Ok(j) => j, Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)) }; fs::write(path, json) } lazy_static! { pub static ref CACHE: Cache = Cache::new(); }