darkwing/server/services/browser_profile_services/
mod.rspub mod args;
pub mod config;
use anyhow::Context;
use async_trait::async_trait;
use mockall::automock;
use std::sync::Arc;
use time::format_description;
use tokio::task::JoinSet;
use tracing::{debug, info};
use crate::server::{
dtos::{start_dto::PreliminaryData, user_dto::UserRole},
utils::php_like::aes128_encrypt,
};
pub use config::consts::*;
pub use config::*;
use crate::{
config::DarkwingConfig,
database::{
browser_profile::{DynBrowserProfileRepository, DynProxyRepository},
browser_profile_access::DynBrowserProfileAccessRepository,
team::DynSettingsRepository,
},
server::{
dtos::{
bookmark_dto::BookmarkFullData,
browser_profile_dto::{BrowserProfileFullData, MiniBrowserProfile},
proxy_dto::ProxyFullData,
settings_dto::SettingsFullData,
start_dto::{Os, StartRequest},
user_dto::ResponseUserDto,
},
error::{AppResult, Error},
},
};
use super::database_encryption_services::DynDatabaseEncryption;
pub type DynBrowserProfileService =
Arc<dyn BrowserProfileServiceTrait + Send + Sync>;
#[automock]
#[async_trait]
pub trait BrowserProfileServiceTrait {
async fn check_access(
&self,
user: ResponseUserDto,
browser_profile_id: u64,
) -> AppResult<bool>;
async fn get_mini_by_id(
&self,
browser_profile_id: i64,
) -> AppResult<MiniBrowserProfile>;
async fn get_by_id_with_fingerprint(
&self,
browser_profile_id: i64,
) -> AppResult<BrowserProfileFullData>;
async fn get_preliminary_data(
&self,
user_id: i64,
team_id: i64,
browser_profile_id: i64,
) -> AppResult<PreliminaryData>;
fn get_mock_profile(platform: Os) -> BrowserProfileFullData
where
Self: Sized;
fn create_config(
bp: &BrowserProfileFullData,
request: StartRequest,
token: String,
) -> AppResult<BrowserProfileConfig>
where
Self: Sized;
fn generate_storage_path(
&self,
browser_profile: MiniBrowserProfile,
) -> AppResult<String>;
async fn update_datadir_hash(
&self,
browser_profile_id: i64,
hash: String,
) -> AppResult<()>;
}
#[derive(Clone)]
pub struct BrowserProfileService {
config: Arc<DarkwingConfig>,
browser_profile_access_repository: DynBrowserProfileAccessRepository,
browser_profile_repository: DynBrowserProfileRepository,
settings_repository: DynSettingsRepository,
proxy_repository: DynProxyRepository,
database_encryption: DynDatabaseEncryption,
}
impl BrowserProfileService {
const STORAGE_PATH_FIRST_IV: &[u8; 16] = b"1234123123123123";
const STORAGE_PATH_SECOND_IV: &[u8; 16] = b"1234111231231231";
const S3_PATH_ENCRYPTION_KEY_LENGTH: usize = 16;
pub fn new(
config: Arc<DarkwingConfig>,
browser_profile_access_repository: DynBrowserProfileAccessRepository,
browser_profile_repository: DynBrowserProfileRepository,
settings_repository: DynSettingsRepository,
proxy_repository: DynProxyRepository,
database_encryption: DynDatabaseEncryption,
) -> Self {
Self {
config,
browser_profile_access_repository,
browser_profile_repository,
settings_repository,
proxy_repository,
database_encryption,
}
}
}
#[async_trait]
impl BrowserProfileServiceTrait for BrowserProfileService {
async fn check_access(
&self,
user: ResponseUserDto,
browser_profile_id: u64,
) -> AppResult<bool> {
info!(
"retrieving browser_profile_access for user_id: {:?} and browser_profile_id: {:?}",
user.id, browser_profile_id
);
let mini = self.get_mini_by_id(browser_profile_id as i64).await?;
if user.role == UserRole::Admin && mini.team_id == user.team_id {
return Ok(true);
}
if user.role == UserRole::Teamlead
&& mini.team_id == user.team_id
&& self
.browser_profile_access_repository
.get_teamlead_id_by_browser_profile(mini)
.await?
.is_some()
{
return Ok(true);
}
let browser_profile_access = self
.browser_profile_access_repository
.get_by_user_id_and_browser_profile_id(
user.id as i64,
browser_profile_id as i64,
)
.await?;
if browser_profile_access.is_none_or(|access| access.view != 1) {
return Ok(false);
}
let transfer = self
.browser_profile_repository
.is_pending_transfer(browser_profile_id as i64)
.await?;
Ok(!transfer)
}
async fn get_mini_by_id(
&self,
browser_profile_id: i64,
) -> AppResult<MiniBrowserProfile> {
let browser_profile = self
.browser_profile_repository
.get_mini_by_id(browser_profile_id)
.await?;
Ok(browser_profile)
}
async fn get_preliminary_data(
&self,
user_id: i64,
team_id: i64,
browser_profile_id: i64,
) -> AppResult<PreliminaryData> {
let browser_profile = self
.browser_profile_repository
.get_preliminary_by_id(browser_profile_id)
.await?;
let proxy = match browser_profile.proxy_id {
Some(proxy_id) => {
match self.proxy_repository.maybe_get_by_id(proxy_id).await? {
Some(proxy) => Some(
ProxyFullData::new(proxy, self.database_encryption.clone()).await?,
),
None => None,
}
}
None => None,
};
let homepages = self
.browser_profile_repository
.get_homepages_by_browser_profile_id(browser_profile_id)
.await?;
let bookmarks = self
.browser_profile_repository
.get_bookmarks(
user_id,
team_id,
browser_profile.clone().main_website.into(),
)
.await?;
debug!("fetched bookmarks: {:?}", bookmarks);
let bookmarks = futures::future::try_join_all(
bookmarks
.into_iter()
.map(|b| BookmarkFullData::new(b, self.database_encryption.clone())),
)
.await?;
debug!("decoded bookmarks: {:?}", bookmarks);
let extensions = self
.browser_profile_repository
.get_extensions(user_id, team_id)
.await?;
let extensions = extensions.into_iter().map(|e| e.into()).collect();
Ok(PreliminaryData {
browser_profile,
proxy,
homepages,
bookmarks,
extensions,
})
}
async fn get_by_id_with_fingerprint(
&self,
browser_profile_id: i64,
) -> AppResult<BrowserProfileFullData> {
let browser_profile = self
.browser_profile_repository
.get_by_id_with_fingerprint(browser_profile_id)
.await
.map_err(Error::from)?;
let homepages = self
.browser_profile_repository
.get_homepages_by_browser_profile_id(browser_profile_id)
.await?;
let settings = self
.settings_repository
.get_by_team_id_and_user_id(
browser_profile.team_id,
browser_profile.user_id,
)
.await?;
let mut join_set = JoinSet::new();
for setting in settings {
let database_encryption = self.database_encryption.clone();
join_set.spawn(async move {
SettingsFullData::new(setting, database_encryption).await
});
}
let mut settings = Vec::new();
while let Some(result) = join_set.join_next().await {
settings.push(result.context("Failed to decrypt settings")??);
}
let proxy = self
.proxy_repository
.maybe_get_by_id(browser_profile.proxy_id)
.await?;
let proxy = match proxy {
Some(proxy) => {
Some(ProxyFullData::new(proxy, self.database_encryption.clone()).await?)
}
None => None,
};
BrowserProfileFullData::new(browser_profile, settings, proxy, homepages)
.await
}
fn get_mock_profile(platform: Os) -> BrowserProfileFullData {
match platform {
Os::MacOS => MACOS_MOCK_PROFILE.clone(),
Os::Windows => WINDOWS_MOCK_PROFILE.clone(),
Os::Linux => LINUX_MOCK_PROFILE.clone(),
}
}
fn create_config(
bp: &BrowserProfileFullData,
request: StartRequest,
token: String,
) -> AppResult<BrowserProfileConfig> {
BrowserProfileConfig::new(bp, request, token)
}
fn generate_storage_path(
&self,
browser_profile: MiniBrowserProfile,
) -> AppResult<String> {
let key = &self.config.s3_path_encryption_key;
let key: [u8; Self::S3_PATH_ENCRYPTION_KEY_LENGTH] =
match key.as_bytes()[0..Self::S3_PATH_ENCRYPTION_KEY_LENGTH].try_into() {
Ok(key) => key,
Err(_) => {
return Err(Error::InternalServerErrorWithContext(
"S3 path encryption key is not valid".to_string(),
));
}
};
let mysql_date_time = format_description::parse(
"[year]-[month]-[day] [hour]:[minute]:[second]",
)?;
let created_at_str = browser_profile.created_at.format(&mysql_date_time)?;
let created_at_encode =
aes128_encrypt(&key, &created_at_str, Self::STORAGE_PATH_FIRST_IV)?;
let id_encode = aes128_encrypt(
&key,
&browser_profile.id.to_string(),
Self::STORAGE_PATH_SECOND_IV,
)?;
Ok(format!(
"{}/{}/{}.datadir.zip",
created_at_encode, id_encode, id_encode
))
}
async fn update_datadir_hash(
&self,
browser_profile_id: i64,
hash: String,
) -> AppResult<()> {
self
.browser_profile_repository
.update_datadir_hash(browser_profile_id, hash)
.await
.map_err(Error::from)
}
}
#[cfg(test)]
mod tests {
use crate::{
database::{
browser_profile::{MockBrowserProfileRepository, MockProxyRepository},
browser_profile_access::MockBrowserProfileAccessRepository,
team::MockSettingsRepository,
},
server::services::database_encryption_services::MockDatabaseEncryptionTrait,
};
use pretty_assertions::assert_eq;
use time::OffsetDateTime;
use super::*;
fn get_mock_service() -> BrowserProfileService {
let mut config = DarkwingConfig::default();
config.s3_path_encryption_key = "XU_xib*l6@1lcyo%CW28gIcnuPFss#Zc8|I9L9o9m$^h+$mURrIMkzbO8DfXR7D2+1ZBez&HdLyO3os2&#pP?WDhfSvds#f-wa*gNr6nsm4=vUVpde%7xGy_|oqyAtsI".into();
BrowserProfileService::new(
Arc::new(config),
Arc::new(MockBrowserProfileAccessRepository::new()),
Arc::new(MockBrowserProfileRepository::new()),
Arc::new(MockSettingsRepository::new()),
Arc::new(MockProxyRepository::new()),
Arc::new(MockDatabaseEncryptionTrait::new()),
)
}
#[test]
fn test_generate_storage_path() {
let expected = "4369141a059ea5fd08f12c7242fd6de31706bf891338ca89cce519f7f2505b6a/4c20d864bd7fc2399d8cb9f7287f1ea7b0fa09c1601f6a22146af414fef85e95/4c20d864bd7fc2399d8cb9f7287f1ea7b0fa09c1601f6a22146af414fef85e95.datadir.zip";
let service = get_mock_service();
let mut profile: MiniBrowserProfile = MACOS_MOCK_PROFILE.clone().into();
profile.created_at = OffsetDateTime::from_unix_timestamp(1337).unwrap();
let actual = service.generate_storage_path(profile).unwrap();
assert_eq!(expected, actual);
}
#[test]
fn test_generate_storage_path_real_data() {
let expected = "d85cbc83fdc37d6f829d80484cd9767ba490db01dbbd5f455ff8a932b82be6da/9ec08e06850c1cc92f5bfd29867d36bbf8a6426a3f25b104d1d3656c4f825465/9ec08e06850c1cc92f5bfd29867d36bbf8a6426a3f25b104d1d3656c4f825465.datadir.zip";
let service = get_mock_service();
let mut profile: MiniBrowserProfile = MACOS_MOCK_PROFILE.clone().into();
profile.created_at =
OffsetDateTime::from_unix_timestamp(1731427011).unwrap();
profile.id = 503638751;
let actual = service.generate_storage_path(profile).unwrap();
assert_eq!(expected, actual);
}
}